【C++杂货铺】运算符重载(二)

简介: 【C++杂货铺】运算符重载(二)

3.2 重载+、+=

有时我们需要知道几天之后的日期,比如我想知道100天后的日期,此时就需要用当前的日期加上100,但是一个日期类型和一个整型可以相加嘛?答案是肯定的,可以通过重载+来实现。运算符重载只规定必须有一个类类型参数,并没有说重载双目操作符必须要两个类型一样的参数。

📖获取某月的天数

日期加天数,要实现日期的进位,即:当当前日期是这个月的最后一天时,再加一天月份就要进一,当当前的日期是12月31日时,再加一天年份就要进一,因此可以先实现一个函数,用来获取当前月份的天数,在每加一天后,判断月份是否需要进位。

int GetDay(int year, int month)//获取某一月的天数
{
  static int arr[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
  if (month == 2 && (((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0)))
  {
    return 29;
  }
  return arr[month];
}

除了2月,每个月的天数都是固定的,因此可以设置一个数组来存放每个月的天数,并且以月份作为下标,对应存储该月的天数,这种方法类似于哈希映射。这里还有两个小细节,第一个:把数组设置成静态,因为这个函数会重复调用多次,把数组设置成静态,它第一次创建之后,一直到程序结束都还在,可以避免函数调用时重复的创建数组。第二点:把month == 2放在前面判断,因为只有当2月的时候才需要判断是否是闰年,如果不是2月就不用判断是不是闰年。

📖重载+

Date Date::operator+(int x)
{
  if(x < 0)//天数为负的时候
  {
    return *this - (-x);//复用-
  }
  /Date tmp = *this;
  //Date tmp(*this);//和上面等价,都是调用拷贝构造函数
  tmp._day = _day + x;
  while (tmp._day > GetDay(tmp._year, tmp._month))
  {
    tmp._day = tmp._day - GetDay(tmp._year, tmp._month);
    tmp._month++;
    if (tmp._month == 13)
    {
      tmp._year++;
      tmp._month = 1;
    }
  }
  return tmp;//
}

注意:要计算a+b的结果,a是不能改变的,因此一个日期加天数,不能改变原本的日期,也就是不能修改this指针指向的内容,所以我们要先利用拷贝构造函数创建一个和*this一模一样的对象,对应上面代码中的tmp,在该对象的基础上去加天数。出了作用域tmp对象会销毁,所以不能传引用返回。

📖重载+=

+=和+很像,区别在于+=是在原来是日期上进行修改,即直接对this指针指向的日期做修改,所以我们对上面的代码稍作修改就可以得到+=。

Date& Date::operator+=(int x)
{
  if (x < 0)//当天数为负
  {
    return *this -= -x;//复用-=
  }
  _day += x;
  while (_day > GetDay(_year, _month))
  {
    _day = _day - GetDay(_year, _month);
    _month++;
    if (_month == 13)
    {
      _year++;
      _month = 1;
    }
  }
  return *this;
}

小Tips:加一个负的天数,就是算多少天以前的日期,所以,当天数为负的时候,可以复用下面的-=。

📖+和+=之间的复用

可以发现,+和+=的实现方法十分相似,那是否可以考虑复用呢?答案是肯定的,他俩其中的一方都可以去复用另一方。

+去复用+=:

Date Date::operator+(int x)
{
  /Date tmp = *this;
  //Date tmp(*this);//和上面等价,都是调用拷贝构造函数
  tmp += x;
  return tmp;//
}

+=去复用+

Date& Date::operator+=(int x)
{
  *this = *this + x;//这里是调用赋值运算符重载
  return *this;
}

注意:上面的两种复用,只能存在一个,不能同时都去复用,同时存在会出现你调用我,我调用你的死穴。

既然只能存在一个,那到底该让谁去复用呢?答案是:让+去复用+=。因为,+=原本的实现过程中并没有调用拷贝构造去创建新的对象,而+原本的实现过程中,会去调用拷贝构造函数创建新的对象,并且是以值传递的方式返回的,期间又会调用拷贝构造。如果让+=去复用+,原本还无需调用拷贝构造,复用后反而还要调用拷贝构造创建新对象,造成了没必要的浪费。

3.3 重载-、-=

有时我们也需要知道,多少天以前的日期,此时就需要重载-,它的两个操作数分别是日期和天数,其次,我们有时还想知道两个日期之间隔了多少天,这也需要重载-,但此时的两个操作数都是日期。两个-重载构成了函数重载。

📖重载日期-天数

有了上面的经验,我们可以先重载-=,再让-去复用-=即可,日期减天数,就是要实现日期的借位。

Date Date::operator-(int x) 
{
  Date tmp(*this);
  return tmp -= x;//复用-=
}

📖重载-=

Date& operator-=(int x)
{
  if (x < 0)//天数天数小于0
  {
    return *this += -x;//复用+=
  }
  _day -= x;
  while (_day <= 0)
  {
    _month--;
    if (_month == 0)
    {
      _month = 12;
      _year--;
    }
    _day += GetDay(_year, _month);
  }
  return *this;
}

📖重载日期-日期

日期-日期,它的形参是一个日期对象,计算的结果是两个日期之间的天数,所以返回值是int,要像知道两个日期之间相隔的天数,可以设置一个计数器,让小日期一直加到大日期,就可以知道两个日期之间相隔的天数。

int operator-(const Date& d) 
{
  Date max = *this;//存放大日期
  Date min = d;//存放小日期
  int flag = 1;
  if (*this < d)
  {
    max = d;
    min = *this;
    flag = -1;
  }
  int n = 0;
  while (max != min)
  {
    --max;
    ++n;
  }
  return n * flag;
}

3.4 重载++、--

++、--操作符,无论前置还是后置,都是一元运算符,为了让前置和后置形成正确的重载,C++规定:后置重载的时候多增加一个int类型的参数,但是当使用后置,调用运算符重载函数时该参数不用传递,编译器自动传递。

📖重载前置++

//前置++,返回++之后的值
Date& Date::operator++()
{
  return *this += 1;//直接复用+=
}

📖重载后置++

//后置++,返回加之前的值
Date Date::operator++(int)//编译器会把有int的视为后置++
{
  Date tmp(*this);
  *this += 1;//复用+=
  return tmp;
}

📖重载前置--

Date& operator--()
{
  return *this -= 1;//复用了-=
}

📖重载后置--

Date operator--(int)
{
  Date tmp(*this);
  *this -= 1;//复用了-=
  return tmp;
}

对比前置和后置可以发现,后置会调用两次拷贝构造函数,一次在创建tmp的时候,另一次在函数返回的时候。而前置则没有调用拷贝构造,所以前置的效率相比后置会高那么一点。

3.5 重载<<、>>

同理,对于自定义类型,编译器仍然不知道如何打印,所以要想通过<<去直接打印日期类对象,需要我们对<<运算符进行重载。

📖重识cout、cin

我们在使用C++进行输入输出的时候,会用到cin和cout,它们俩本质上都是对象,cin是istream类实例化的对象,cout是ostream类实例化的对象。

image.png


内置类型可以直接使用<<、>>,本质上是因为库中进行运算符重载。而<<、>>不用像C语言的printf和scanf那样,int对应%d,float对应%f,是因为运算符重载本质上是函数,对这些不同的内置类型,分别进行了封装,在运算符重载的基础上又实现了函数重载,所以<<、>>支持自动识别类型。

3aea8b5f5a52457d8337528aeadf9969.png


2190857d524a4cf48232c4cb6398bf34.png

📖<<为什么不能重载成成员函数

要实现对日期类的<<,要对<<进行重载。但是<<和其他的运算符有所不同,上面重载的所有运算符,为了保证类的封装性,都重载成了类的成员函数,但是<<不行,因为我们平时的使用习惯是cout << d1,前面说过,对于一个双目运算符的重载,它的左操作数会传递给运算符重载函数的第一个形参,右操作数会传递给运算符重载函数的第二个形参,也就是说cout会传递给第一个形参,日期类对象d2会传递给第二个形参,如果运算符重载函数是类的成员函数的话,那么它的第一个形参是默认的this指针,该指针是日期类类型的指针,和cout的类型不匹配,当然也有解决办法,那就是输出一个日期类对象的时候,写成d1 << cout,此时就相当于d1.operator(cout),会把d1的地址传给this指针,形参再用一个ostream类型的对象来接收cout即可,但是这样的使用方式,显然是不合常理的。

📖将<<重载成全局函数

正确的做法是,把<<重载成全局函数,此时函数形参就没有默认的this指针,我们可以根据需要来设置形参的顺序,第一个形参用ostream类对象来接收cout,第二个形参用Date日期类对象来接收d1。

//重载成全局的
ostream& operator<< (ostream& out, const Date& d)
{
  out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
  return out;
}

注意:形参out不能加const修饰,因为我们就是要往out里面写东西,加了const意味着out不能修改。其次为了实现连续的输出,返回值是ostream类型的对象out,因为此时出了作用域out还在,所以可以用引用返回。

因为该运算符重载函数写在全局,默认情况下,在该函数内部是无法访问到日期类的私有成员变量,为了解决这个问题,可以把该运算符重载函数设置成友元函数,或者在类里面写私有成员变量的Get方法(Java常用)。

friend ostream& operator<< (ostream& out, Date& d);

友元函数只需要配合上friend关键字,在日期类里面加上一条声明即可,此时在该函数体就可以使用对象中的私有成员变量。该声明不受类中访问限定符的限制。

📖重载>>

同理,>>也应该重载成全局的。

istream& operator>> (istream& in, Date& d)
{
  in >> d._year >> d._month >> d._day;
  return in;
}

注意:两个形参in和d都不能用const修饰,前者是因为in本质上是一个对象,在进行流插入的时候,会改变对象里面的一些状态值,而后者是因为,我们就是希望通过流插入往d里面写入数据,所以也不能加const修饰。

小Tips:C++中的流插入和流提取可以完美的支持自定义类型的输入输出,而C语言的scanf和printf只能支持内置类型,这就是C++相较于C语言的一个优势。

四、const成员

将const修饰的成员函数称为const成员函数,const修饰类的成员函数,实际上修饰的是该成员函数隐含的*this,表明该成员函数中不能修改调用该函数的对象中的任何成员。这样一来,不仅普通对象可以调用该成员函数(权限的缩小),const对象也能调用该成员函数(权限的平移)。经过const修饰的成员函数,它的形参this的类型就是:const T* const this。

bool Date::operator<(const Date& d) const//用const修饰
{
  if (_year < d._year)
  {
    return true;
  }
  else if (_year == d._year && _month < d._month)
  {
    return true;
  }
  else if (_year == d._year && _month < d._month && _day < d._day)
  {
    return true;
  }
  else
  {
    return false;
  }
}

对于所有的关系运算符重载函数,都应该加const修饰,因为它们不会改变对象本身。

📖总结:

并不是所有的成员函数都要加const修饰,要修改对象成员变量的函数,是不能加const修饰的,例如:重载的+=、-=等,而成员函数中如果没有修改对象的成员变量,可以考虑加上const修饰,这样不仅普通对象可以调用该成员函数(权限的缩小),const对象也能调用该成员函数(权限的平移)。

五、取地址及const取地址操作符重载

Date* operator&()
{
  cout << "Date* operator&()" << endl;
  return this;
}
const Date* operator&() const
{
  cout << "const Date* operator&() const" << endl;
  return this;
}
int main()
{
  Date d1(2023, 7, 22);
  const Date d2(2023, 7, 22);
  cout << &d1 << endl;
  cout << "--------" << endl;
  cout << &d2 << endl;
  return 0;
}

8cb3af2cda434458a98fd26875a1d97d.png

这俩取地址运算符重载函数,又构成函数重载,因为它们的默认形参this指针的类型不同,一个用const修饰了,另一个没有。const对象会去调用const修饰的取地址运算符重载函数。

小Tips:这两个&重载,属于类的默认成员函数,我们不写编译器会自动生成,所以这两个运算符重载一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容。

🎁结语:

 今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下,您的支持就是春人前进的动力!


目录
相关文章
|
编译器 C++
C++进阶之路:何为运算符重载、赋值运算符重载与前后置++重载(类与对象_中篇)
C++进阶之路:何为运算符重载、赋值运算符重载与前后置++重载(类与对象_中篇)
142 1
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
316 5
C++(十五) 运算符重载
C++中的运算符重载允许对已有运算符的功能进行重新定义,从而扩展语言功能、简化代码并提升效率。重载遵循特定语法,如 `friend 类名 operator 运算符(参数)`。重载时需注意不可新增或改变运算符数量、语义、优先级、结合性和返回类型。常见示例包括双目运算符 `+=` 和单目运算符 `-` 及 `++`。输入输出流运算符 `&lt;&lt;` 和 `&gt;&gt;` 也可重载。部分运算符只能作为成员函数重载。
|
存储 编译器 C++
【C++】:拷贝构造函数和赋值运算符重载
【C++】:拷贝构造函数和赋值运算符重载
199 1
|
C++ 索引
C++核心技术要点《运算符重载》
C++核心技术要点《运算符重载》
165 2
|
自然语言处理 程序员 C++
C++基础知识(五:运算符重载)
运算符重载是C++中的一项强大特性,它允许程序员为自定义类型(如类或结构体)重新定义标准运算符的行为,使得这些运算符能够适用于自定义类型的操作。这样做可以增强代码的可读性和表达力,使得代码更接近自然语言,同时保持了面向对象编程的封装性。
177 0
|
编译器 C++
【C++】详解运算符重载,赋值运算符重载,++运算符重载
【C++】详解运算符重载,赋值运算符重载,++运算符重载
|
编译器 C++
【C++】类和对象③(类的默认成员函数:赋值运算符重载)
在C++中,运算符重载允许为用户定义的类型扩展运算符功能,但不能创建新运算符如`operator@`。重载的运算符必须至少有一个类类型参数,且不能改变内置类型运算符的含义。`.*::sizeof?`不可重载。赋值运算符`=`通常作为成员函数重载,确保封装性,如`Date`类的`operator==`。赋值运算符应返回引用并检查自我赋值。当未显式重载时,编译器提供默认实现,但这可能不足以处理资源管理。拷贝构造和赋值运算符在对象复制中有不同用途,需根据类需求定制实现。正确实现它们对避免数据错误和内存问题至关重要。接下来将探讨更多操作符重载和默认成员函数。
c++进阶篇(一)——运算符重载
c++进阶篇(一)——运算符重载
110 0