【C++初阶:类和对象(中篇)】构造函数 | 析构函数 | 拷贝构造函数 | 赋值运算符重载 下

简介: 【C++初阶:类和对象(中篇)】构造函数 | 析构函数 | 拷贝构造函数 | 赋值运算符重载

五、赋值运算符重载

💦 运算符重载

❗ 引入 ❕

class Date
{
public:
  Date(int year = 0, int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day; 
  }
private:
  int _year;
  int _month;
  int _day;
};
int main()
{
  Date d1(2021, 10, 11);
  Date d2(2020, 11, 11);
  Date d3(2021, 11, 11);
  //d1 == d2;//err
  return 0;
}

📝 说明

d1 == d2 ❓

运算符默认都是给内置类型变量用的。

自定义类型的变量想用这些运算符,得自己运算符重载。

运算符重载指的是需要自己写一个函数实现这里运算符的行为。


C++ 为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,函数里的实现就是行为。它也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。

函数名字为:关键字 operator 后面接需要重载的运算符符号。

函数原型:返回值类型 operator 操作符 (参数列表)

注意:对于参数列表的个数有几个是根据操作符来确定的,比如 == 就需要两个参数;参数类型就是你要操作的对象类型;返回值类型就是运算符运算后的返回值。

class Date
{
public:
  Date(int year = 0, int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day; 
  }
//private:
public://这里破坏封装使得可以在类外访问成员
  int _year;
  int _month;
  int _day;
};
bool operator==(const Date& x1, const Date& x2)
{
  return x1._year == x2._year
    && x1._month == x2._month
    && x1._day == x2._day;
}
int main()
{
  Date d1(2021, 10, 11);
  Date d2(2020, 11, 11);
  Date d3(2021, 11, 11);
  operator==(d1, d2);//可以这样调用,但是这样可读性很差,还不如写一个函数
  d1 == d2;//同上,如果没有重载会报错,如果重载了它会转换为 operator==(d1, d2);
  return 0;
}

📝 说明

怎么解决上述代码中利用破坏了封装,使得运算符重载的函数可以访问私有成员 ❓

  1、友,但不推荐。

  2、将函数重载写在类里,写成一个成员函数。

class Date
{
public:
  Date(int year = 0, int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day; 
  }
  //bool operator==(const Date& x1, const Date& x2)//err
  //{
  //  return x1._year == x2._year
  //    && x1._month == x2._month
  //    && x1._day == x2._day;
  //}
  //bool operator==(Date* this, const Date& x)
  bool operator==(const Date& x)//ok
  {
    return _year == x._year
      && _month == x._month
      && _day == x._day;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main()
{
  Date d1(2021, 10, 11);
  Date d2(2020, 11, 11);
  Date d3(2021, 11, 11);
  d1.operator==(d2);
  d1 == d2;//同上,编译器会自动识别转换为d1.operator==(d2); -> d1.operator(&d1, d2);
  return 0;
}

📝 说明

当我们把函数重载写进类里时,error C2804:operator== 的参数太多了 ❓

这里的矛盾点是运算符重载函数的参数是根据运算符来确定的但是对于类里的函数成员它会有一个 this 指针。解决方法就是少写一个参数。

对于d1 == d2 ❓

编译器会自动识别转换,如果是全局函数那么它会转换成 operator==(d1, d2);;如果是成员函数那么它会转换成 d1.operator(d2);。不管是全局还是成员一般我们都是直接写 d1 == d2。

观察 d1.operator==(d2); 和 d1 == d2; 的汇编代码 ❗

我们不一定需要会写汇编,但是需要勉强看懂

❗ 照虎画猫写一个d1<d2 ❕

class Date
{
public:
  Date(int year = 0, int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day; 
  }
  bool operator<(const Date& x)//ok
  {
    if(_year < x._year)
      return true;
    else if(_year == x._year && _month < x._month)
      return true;
    else if(_year == x._year && _month == x._month && _day < x._day)
      return true;
    else
      return false;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main()
{
  Date d1(2021, 10, 11);
  Date d2(2020, 11, 11);
  Date d3(2021, 11, 11);
  cout << d1 < d2 << endl;//err
  cout << (d1 < d2) << endl;//ok,d1.operator<(d2); -> d1.operator<(&d1, d2);
  return 0;
}

📝 说明

上面使用 cout 输出结果时,出现了问题。<< 是流插入运算符,它的优先级比较高 (从上面看比 <、= 高,比 () 低),这里 cout 会先输出 d1,然后再 < ???


⚠ 注意

1️⃣ 不能通过连接其他符号来创建新的操作符:比如 operator@。

2️⃣ 重载操作符必须有一个类类型或者枚举类型的操作数。

3️⃣ 用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不能改变其含义。

4️⃣ 作为类成员的重载函数时,其形参看起来比操作数数目少 1 成员函数的操作符有一个默认的形参 this,限定为第一个形参。

5️⃣ .* 、:: 、sizeof 、?: 、. ,注意以上 5 个运算符不能重载。这个经常在笔试选择题中出现,注意 * 是可以重载的,这里不能重载的是 .*。

💦 赋值运算符重载

上面我们重载了 == 这个符号,这里我们要重载的是 = 符号。

class Date
{
public:
  Date(int year = 0, int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day; 
  }
  void operator=(const Date& x)
  {
    _year = x._year;
    _month = x._month;
    _day = x._day;  
  }
private:
  int _year;
  int _month;
  int _day;
};
int main()
{
  Date d1(2021, 10, 11);
  Date d2(2020, 11, 11);
  Date d3(2021, 11, 11);
  //赋值运算符重载
  d1 = d3;
  //拷贝构造
  Date d4(d3);
  Date d5 = d3;//拷贝构造还是赋值运算符重载??? ———— 这里是拷贝构造,因为这里是在实例化对象,并且它同 Date d5(d3);
  return 0;
}

📝 说明

可以使用拷贝构造替代赋值运算符重载 ❓

拷贝构造和赋值运算符重载的场景不一样:拷贝构造是用于一个对象准备定义时,用另一个对象来初始化它;赋值运算符重载是用于两个已经定义出来的对象间的拷贝复制。

对于上面写的赋值运算符重载写的不够好 ❓

我们说我们去重载运算符,就是控制这个运算符的行为。

1、从赋值运算符的特性来说,它要能支持连续赋值 —— d1 = d2 = d3;。

int i = 1;
int j = 2;
int k = 3;
i = j = k;//连续赋值(从右至左),注意连续赋值是有返回值的。这里最后i去接收j=k的返回值

2、d1 = d1 自己赋值给自己,可以,但是没意义,还有可能会破坏之前的拷贝。所以赋值重载中为了防止这种情况它还要控制一下条件。

class Date
{
public:
  Date(int year = 0, int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day; 
  }
  //d2 = d3, d1 = d2
  Date& operator=(const Date& x)
  {
    if(this != &x)
    {
      _year = x._year;
      _month = x._month;
      _day = x._day;      
    }
    return *this;//返回d2,返回d1
  }
private:
  int _year;
  int _month;
  int _day;
};
int main()
{
  Date d1(2021, 10, 11);
  Date d2(2020, 11, 11);
  Date d3(2021, 11, 11);
  d1 = d2 = d3;
  d1 = d1;
  return 0;
}

📝 说明

在这里我们再来思考一下,对于this指针为什么仅仅可以在类成员函数{}里使用 ❓

从上面就可以看出,因为有这样的使用场景。

再看引用价值 ❓

如果使用传值传参,它会再调用拷贝构造,再额外开一块空间。

如果使用传值返回,它不会返回 *this,而是返回一个临时对象 (它也会再调用拷贝构造)。

这时使用引用的价值就有体现了。

注意出了作用域 *this 对象不在了,只能用传值返回;但是这里的对象都是在 main 函数里定义的,所以能用引用返回。

❓ 同样如果我们不写赋值重载,编译器也会默认生成 ❔

类似于拷贝构造函数

1、内置类型成员会完成值拷贝。

2、自定义类型成员会去调用它的赋值重载。

class A
{
public:
  A& operator=(const A& a)
  {
    cout << "A& operator=(const A& a)" << endl;
    return *this;
  }
};
class Date 
{
public:
  Date(int year = 0, int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day; 
  }
  void Print()
  {
    cout << _year << "/" << _month << "/" << _day << endl;
  }
private:
  int _year;
  int _month;
  int _day;
  A _a;
};
int main()
{
  Date d1(2021, 10, 11);
  d1.Print();
  Date d2(2020, 11, 11);
  Date d3(2021, 11, 11);
  d1 = d2;
  d1.Print();
  return 0;
}


💨小结:

对于 6 个默认成员函数,原则:编译器默认生成的能完成我们要的功能就可以不写,不能完成就需要我们自己写。

六、日期类的实现

更深入的学习常见的运算符重载,当然还有一些需要后面的知识铺垫,比如流提取、流插入运算符。

Date.h

#pragma once
#include<iostream>
#include<assert.h>
#include<stdbool.h>
using namespace std;
class Date
{
public:
  //获取某年某月的天数
  int GetMonthDay(int year, int month)
  {
    assert(month > 0 && month < 13);
    //默认平年
    int monthDays[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 monthDays[month];
  }
  Date(int year = 0, int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day;
    //判断日期是否合法
    if (_year < 0 || _month <= 0 || _month >= 13 || _day <= 0 || _day > GetMonthDay(_year, _month))
    {
      cout << _year << "/" << _month << "/" << _day << "->";
      cout << "非法日期" << endl;
    }
  }
  void Print() const
  {
    cout << _year << "/" << _month << "/" << _day << endl;
  }
  bool operator>(const Date& d) 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;
  }
  bool operator==(const Date& d) const
  {
    return _year == d._year && _month == d._month && _day == d._day;
  }
  bool operator>=(const Date& d) const
  {
    return *this > d || *this == d;//复用operator>、operator==
  }
  bool operator<(const Date& d) const
  {
    return !(*this >= d);//复用operator>=,再取反
  }
  bool operator<=(const Date& d) const
  {
    return !(*this > d);//复用operator>,再取反
  }
  bool operator!=(const Date& d) const
  {
    return !(*this == d);//复用operator==,再取反 
  }
  Date operator+(int day) const;  
  Date operator-(int day) const;
  Date& operator+=(int day);
  Date& operator-=(int day);
  Date& operator++();
  Date operator++(int);
  Date& operator--();
  Date operator--(int);
  int operator-(const Date& d) const;
private:
  int _year;
  int _month;
  int _day;
};

Date.cpp

#include"Date.h"
Date Date::operator+(int day) const
{
  //Date temp(*this);
  //temp._day += day;
  //while (temp._day > GetMonthDay(temp._year, temp._month))
  //{
  //  temp._day -= GetMonthDay(temp._year,temp._month);
  //  ++temp._month;
  //  if (temp._month == 13)
  //  {
  //    ++temp._year;
  //    temp._month = 1;
  //  }
  //}
  //return temp;
  //相同逻辑太多,直接复用operator+= (这个比较好)
  Date temp = *this;
  temp += day;
  return temp;
}
Date& Date::operator+=(int day)
{
  if (day < 0)//d1 + -100;
  {
    return *this -= -day;//+= -100到-= 100
  }
  _day += day;
  //日期不合法,需要进位
  while (_day > GetMonthDay(_year, _month))
  {
    _day -= GetMonthDay(_year, _month);
    ++_month;
    if (_month == 13)
    {
      ++_year;
      _month = 1;
    }
  }
  return *this;
  //相同逻辑太多,直接复用operator+
  //*this = *this + day;
  //return *this;
}
Date Date::operator-(int day) const
{
  Date temp = (*this);
  temp -= day;
  return temp;
}
Date& Date::operator-=(int day)
{
  if (day < 0)
  {
    return *this += -day;
  }
  _day -= day;
  while (_day < 1)
  {
    --_month;
    if (_month == 0)
    {
      --_year;
      _month = 12;
    }
    _day += GetMonthDay(_year, _month);
  }
  return *this;
}
Date& Date::operator++()
{
  *this += 1;//复用operator+=
  return *this;//返回++后的值
}
Date Date::operator++(int)
{
  Date temp = *this;
  *this += 1;//复用operator+=
  return temp;//返回++前的值
}
Date& Date::operator--()
{
  *this -= 1;//复用operator-=
  return *this;//返回--后的值
}
Date Date::operator--(int)
{
  Date temp(*this);
  *this -= 1;//复用operator-=
  return temp;//返回--前的值
}
int Date::operator-(const Date& d) const
{
  //比较大小
  Date max = *this, min = d;
  int flag = 1;
  if (*this < d)
  {
    max = d;
    min = *this;
    flag = -1;
  }
  int n = 0;
  while (min != max)
  {
    ++min;//利用operator++()
    ++n;
  }
  return n * flag;
}

Test.cpp

#include"Date.h"
void TestDate1()
{
  Date d1(2021, 10, 11);//ok
  Date d2(2021, 2, 29);//err
  Date d3(2020, 2, 29);//ok
  Date d4(2020, 13, 29);//err
}
void TestDate2()
{
  Date d1(2021, 10, 11);
  Date ret;
  ret = d1 + 100;
  ret.Print();
  ret = d1 += 100;
  ret.Print();
  d1.Print();
  ret = d1 + -100;
  ret.Print();
}
void TestDate3()
{
  Date d1(2021, 10, 11);
  Date ret;
  ret = d1 - 11;
  ret.Print();
  ret = d1 -= 11;
  ret.Print();
  d1.Print();
  ret = d1 - -11;
  ret.Print();
}
void TestDate4()
{
  Date d1(2021, 10, 11);
  Date ret;
  ret = ++d1;
  ret.Print();
  d1.Print();
  ret = d1++;
  ret.Print();
  d1.Print();
}
void TestDate5()
{
  Date d1(2021, 10, 11);
  Date ret;
  ret = --d1;
  ret.Print();
  d1.Print();
  ret = d1--;
  ret.Print();
  d1.Print();
}
void TestDate6()
{
  Date d1(2023, 10, 11);
  Date d2(2022, 10, 11);
  cout << d1 - d2 << endl;
  cout << d2 - d1 << endl;
}
int main()
{
  //TestDate1();//日期合法
  //TestDate2();//+、+=、+ -(负)
  //TestDate3();//-、-=、- -(负)
  //TestDate4();//++x、x++
  //TestDate5();//--x、x--
  TestDate6();//d1 - d2
  return 0;
}

📝说明

日期的不合法,构造函数有无责任 ❓

有的,构造函数不仅仅是完成初始化工作,还要判断数据是否合法。

除了上面说的 5 个运算符不能重载之外,其余的运算符有重载的意义 ❓

一个类到底要重载哪些运算符,是看你需要哪些运算符,并且要考量重载的运算符有无价值。

运算符重载 | 函数重载 ❓

虽然它们都用了重载这个词,但是它们之间没有关联。

operator+ | operator+= ❓

对于大量重复的代码我们可以利用复用来提高代码质量 (它俩必须得实现一个)。

operator+复用operator+=不需要声明 ❓

operator+ 是在 operator+= 之前,所以需要声明,但是我们的 Date.cpp 文件中的第一行,已经把 Date.h 的声明都展开了,所以这里的先后位置可随意。

Date d3 = d1 + -100 & Date d3 = d1 - -100 ❓

显然我们并未考虑到这种情况,所以这里我们还可以把复用用到极致。

运算符前置++和后置++怎么进行重载(同前置- -和后置- -) ❓

它们的共同点都是需要 ++ 的,但是它们的返回值不同:前置++ 的返回值是 ++ 以后的;后置++ 的返回值是 ++ 之前的。

这两个运算符都是单操作数,也就是只能有一个 this 指针,按正常的运算符重载规则是无法区分前置与后置的,所以编译器为了能区分这种情况,这里就做了一个特殊的处理 —— 给后置++ 增加一个 int 参数 (这个参数仅仅是为了区分,你给多少它都不会用),这样它们就能构成函数重载。

日期 - 日期 ❓

常规思路是日减、月减、年减,补它们中间的位,但是实际上实现较难。

思路一:把某月某日映射到 365 天

思路二 (最优):先比较两日期的大小,然后让小的++,当小的等于大的时,那么加了多少次就是它们相差的天数

比较运算符重载 ❓

常规思路是写好一个比较运算符重载之后就复制代码改符号。

利用复用的思路是实现 operator> 和 operator== 就行了。

七、const成员函数

💦 const修饰类的成员函数

class Date
{
public:
  Date(int year = 0, int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day;
  }
  void Print1()//void Print(Date* this)
  {
    cout << _year << "/" << _month << "/" << _day << endl;
  }
  void Print2() const//void Print(const Date* this)
  {
    cout << _year << "/" << _month << "/" << _day << endl;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main()
{
  Date d1(2021, 10, 13);
  //Date d2(2021, 10, 14);//ok
  //const Date d2(2021, 10, 14);//err
  d1.Print1();
  d2.Print1();//d2.Print(&d2);
  const Date d3(2021, 10, 14);//ok
  d3.Print2();//d3.Print(&d3);
  return 0;
}

📝 说明

为什么const对象不能调用Print ❓

我们之前说了在调用类成员函数时有一个隐式的参数 this 指针

💨总结

1、成员函数加 const,变成 const 成员函数是有好处的,这样 const 对象可以调用,非 const 对象也可以调用。

2、不是说所有的成员函数都要加 const ,具体要看成员函数的功能,如果成员函数是修改型 (operrato+=、Push),那就不能加;如果是只读型 (Print、operator+),那就最好加。

3、const 对象不可以调用非 const 成员函数 (权限放大);非 const 对象可以调用 const 成员函数 (权限缩小)。

4、const 成员函数内不可以调用其它非 const 成员函数;非 const 成员函数内可以调用其它 const 成员函数。

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

class Date
{
public:
  Date(int year = 0, int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day;
  }
  //普通对象取地址 
  /*Date* operator&()
  {
    return this;
  }*/
  //const对象取地址 
  /*const Date* operator&() const
  {
    return this;
  }*/
  Date* operator&()
  {
    return nullptr;
  }
  const Date* operator&() const
  {
    return nullptr; 
  }
private:
  int _year;
  int _month;
  int _day;
};
int main()
{
  Date d1(2021, 10, 13);
  const Date d2(2021, 10, 14);
  cout << &d1 << endl;
  cout << &d2 << endl;
  return 0;
}

📝 说明

一般不需要写,编译器生成的就够用。

如果非要写,比如不想让别人获取对象的地址,就可以自己实现,返回 nullptr。

九、补充

class widget
{
public:
  widget()       
  {}
  widget(const widget& w)
  {
    cout << "widget(const widget& w)" << endl;
  }
  widget& operator=(const widget& w)
  {
    cout << "widget& operator=(const widget& w)" << endl;
    return *this;
  }
  ~widget()
  {}
};
void f1(widget w)
{}
widget f2()
{
  widget W;
  return W;
}
int main()
{
    widget w1;
  //传值传参和传值返回都会生成一个拷贝对象
  f1(w1);
  f2();
  //本来应该是两次拷贝构造,但是这种情况下编译器会优化
  widget ret = f2();
  //不会优化
  /*widget ret;
  ret = f2();*/
  return 0;
}

📝说明

❓ 传返回值拷贝构造优化问题 ❔

匿名对象 ❓

我们定义对象除了常规的方式还可以定义匿名对象,匿名对象没有名字,它的生命周期只在这一行 。

widget w;
widget(); 

比如当只想调用 Print 时就会使用匿名对象。

widget w;
w.Print();
widget().Print();

❓ 传参数也是类似的 ❔

class widget
{
public:
  widget()       
  {
    cout << "widget" << endl;
  }
  widget(const widget& w)
  {
    cout << "widget(const widget& w)" << endl;
  }
  ~widget()
  {
    cout << "~widget()" << endl;  
  }
};
void f1(widget w)
{}
int main()
{
  //先构造,再去拷贝构造,这是正常的写法,编译器没有任何的优化
  /*widget w;
  f1(w);*/
  //传参匿名对象
  f1(widget());
  return 0;
}

📝说明

传匿名对象优化问题 ❓

本来应该是构造和拷贝构造,但是编译器做了优化。

💨总结

VS2017 中在传值传参和传值传返回值的过程中,只要是在一个表达式调用的连续步骤中:拷贝构造、拷贝构造,两次会被优化成一次; 构造、拷贝构造,会被编译器优化合并成构造。

❓ 以下代码共调用多少次拷贝构造函数 ❔

Widget f(Widget u)
{
  Widget v(u);
  Widget w = v;
  return w;
}
main()
{
  Widget x;
  Widget y = f(f(x));
}

📝 说明

所以总共拷贝构造了 7 次。


相关文章
|
1天前
|
编译器 C++
C++程序中的对象赋值和复制
C++程序中的对象赋值和复制
7 1
|
1天前
|
编译器 C++
C++程序中的派生类构造函数
C++程序中的派生类构造函数
5 1
|
1天前
|
编译器 C++
C++程序中的派生类析构函数
C++程序中的派生类析构函数
9 2
|
1天前
|
C++
C++程序中的析构函数
C++程序中的析构函数
8 2
|
6天前
|
C++ Linux
|
6天前
|
编译器 C++
【C++从练气到飞升】03---构造函数和析构函数
【C++从练气到飞升】03---构造函数和析构函数
|
6天前
|
设计模式 安全 算法
【C++入门到精通】特殊类的设计 | 单例模式 [ C++入门 ]
【C++入门到精通】特殊类的设计 | 单例模式 [ C++入门 ]
19 0
|
4天前
|
测试技术 C++
C++|运算符重载(3)|日期类的计算
C++|运算符重载(3)|日期类的计算
|
5天前
|
C语言 C++ 容器
C++ string类
C++ string类
9 0