【C++要笑着学】运算符重载 | 赋值重载 | 取地址重载 | const成员

简介: 朋友们好啊,今天终于更新了。我是柠檬叶子C,本章将开始讲解运算符重载。运算符重载的技能是学习实现 STL 内部底层的不可缺少的 "利器" !所以本篇非常重要,下一篇会手把手实现一个Date类,可以进一步地实战体会运算符重载。

2118ecc7a8a75671bdd6530ed1981dca_def962d46b464fc3ae6eb048980d21a0.png

写在前面


朋友们好啊,今天终于更新了。我是柠檬叶子C,本章将开始讲解运算符重载。运算符重载的技能是学习实现 STL 内部底层的不可缺少的 "利器" !所以本篇非常重要,下一篇会手把手实现一个Date类,可以进一步地实战体会运算符重载。


Ⅰ.  运算符重载


0x00 引入

什么是运算符重载呢?

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

简单来说:就是能让自定义类型和内置类型一样使用运算符。


0x01 运算符重载的概念

函数名:  关键字 operator+ 需要重载的运算符符号

比如:

operator+
operator>
operator==

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


返回值类型,看操作符运算后返回的值是什么。


参数,操作符有几个操作数,它就有几个参数。


📌 注意事项:


① 不能通过连接其他符号来创建新的操作符,


你只能对已有的运算符进行重载,你也不能对内置类型进行重载。


operator@  ❌

② 重载操作符必须有一个类类型或枚举类型的操作数。


③ 用于内置类型的操作符,其含义不能改变。比如内置的 整型 +,你不能改变其含义。


④ 作为类成员的重载函数时,其形参看起来比操作数数目少 1,


    成员函数的操作符有一个默认的形参 this,限定为第一个形参。


⑤ 不支持运算符重载的 5 个运算符:


.          (点运算符)
::         (域运算符)
.*         (点星运算符,)
?:         (条件运算符)
sizeof

虽然点运算符( . )不能重载,但是箭头运算符( -> )是支持重载的


解引用(*)是可以重载的,不能重载的是点星运算符( .* )

1de7dc710937c861a3fe41d7befd90a7_f24319b3b23c4b969e457bf8e0140cd6.png

⑥ 会自动转化


重载后的运算符可以直接用!岂不美哉?


d1 > d2; 会转换成  operator>(d1, d2);     可读性大大增强
cout << (d1 > d2) << endl;
cout << operator>(d1, d2) << endl;


💬 举一个运算符重载的例子: ==


#include <iostream>
using namespace std;
class Date {
public:
  Date(int year = 1970, int month = 1, int day = 1) {
  this->_year = year;
  this->_month = month;
  this->_day = day;
  }
// private:    
  int _year;
  int _month;
  int _day;
};
/* d1 == d2 */
bool operator==(const Date& d1, const Date& d2) {
  return d1._year == d2._year
  && d1._month == d2._month 
  && d1._day == d2._day;
}
int main(void) {
  Date d1(2022, 3, 8);
  Date d2(2022, 5, 1);
  cout << (d1 == d2) << endl;  
  return 0;
}


🚩 运行结果演示:

2601d206ccae51060d4564add5dec105_f0abefa17cd7479597c4d4083614283d.png


这里会发现运算符重载成全局的,我们不得不将成员变量是共有的,


我们得把 private 撤掉:

87e44aabe9bf9bdea5a869407cdfb108_178ee2ab346f4ec598bd65f548106fea.png


❓ 那么问题来了,封装性如何保证?


这里其实可以用 "友元" 来解决,如果现在不知道也没关系,我们后面会讲。


💬 或者干脆直接重载成成员函数就完事儿了:


#include <iostream>
using namespace std;
class Date {
public:
  Date(int year = 1970, int month = 1, int day = 1) {
  this->_year = year;
  this->_month = month;
  this->_day = day;
  }
  /* d1 == d2 
  bool operator==(Date* this, const Date& d2)   (打回原形)  
  */
  bool operator==(const Date& d2) {
  return (
      this->_year == d2._year
    && this->_month == d2._month
    && this->_day == d2._day
    );
  }
private:
  int _year;
  int _month;
  int _day;
};
int main(void) {
  Date d1(2022, 3, 8);
  Date d2(2022, 5, 1);
  cout << (d1 == d2) << endl;  
  return 0;
}


🔑 解读:


既然要当成员函数,就得明白这里的 this 指的是谁。


需要注意的是,左操作数是 this 指向的调用函数的对象。


此外,因为前几章才讲 this 指针,为了演示清楚,所以我这里把 this 写上(虽然可以不写)。


Ⅱ.  赋值重载(默认成员函数)


0x00 概念

 赋值重载主要是把一个对象赋值给另一个对象。


如果你不写,编译器会默认生成。


0x02  operator=(默认成员函数)

📌 要分清!


int main(void) 
{
    /* 一个已经存在的对象初始化一个马上创建实例化的对象 */
    Date d3(d1);  // 拷贝构造
    /* 两个已经存在的对象,之间进行赋值拷贝 */
  Date d1(2022, 3, 8);
  Date d2(2022, 5, 1);
  d1 = d2;    // 让 d1 和 d2 一样
  return 0;
}

类的默认成员函数 —— 赋值重载


赋值运算符重载主要有以下四点:


① 参数类型


② 返回值年


③ 检查是否给自己复制


④ 返回 *this


💬 d1 = d2


#include <iostream>
using namespace std;
class Date {
public:
  /* 全缺省的构造函数 */
  Date(int year = 0, int month = 1, int day = 1) {
  this->_year = year;
  this->_month = month;
  this->_year = day;
  }
  /* 赋值运算符重载:d1 = d3 */
  Date& operator=(const Date& d) {
  if (this != &d) {   // 防止自己跟自己赋值(这里的&d是取地址)
    this->_year = d._year;
    this->_month = d._month;
    this->_day = d._day;
  }
  return *this;   // 返回左操作数d1
  }
private:
  int _year;
  int _month;
  int _day;
};
int main(void)
{
  Date d1(2022, 3, 10);
  Date d2(2022, 7, 1);
  d1 = d2;
  return 0;
}

🔑 解读:


一定要防止自己和自己赋值!我们这里加 if 语句来判断就是为了防止极端情况下,


自己给自己赋值,加上这条判断后就算遇到自己给自己赋值,就会不做处理,直接跳过。


 因为出了作用域 *this 还在,所以我们可以使用引用来减少拷贝。


💬 我们来验证一下:


① 我们先把引用返回去掉:


Date& operator=(const Date& d) {...}
Date operator=(const Date& d) {...}

② 这里为了方便观察,就不让拷贝构造函数自己生成了。


我们自己实现一个只会嗷嗷叫的拷贝构造函数,不让编译器自己生成。

fd087ccf9b931b6053a3ff300a63eec8_52edebc3810647359af6d4cb39e40d42.png

#include <iostream>
using namespace std;
class Date {
public:
  /* 全缺省的构造函数 */
  Date(int year = 0, int month = 1, int day = 1) {
  this->_year = year;
  this->_month = month;
  this->_year = day;
  }
  /* 测试用拷贝构造 */
  Date(const Date& d) {
  cout << "调用了一次拷贝构造" << endl;
  }
  /* 赋值运算符重载:d1 = d3 */
  Date operator=(const Date& d) {
  if (this != &d) {
    this->_year = d._year;
    this->_month = d._month;
    this->_day = d._day;
  }
  return *this;   // 返回左操作数d1
  }
private:
  int _year;
  int _month;
  int _day;
};
int main(void)
{
  Date d1(2022, 3, 10);
  Date d2(2022, 7, 1);
  Date d3(2020, 3, 5);
  Date d4(d3);   // 拷贝构造
  d1 = d2 = d3;  
  return 0;
}


🚩 运行结果如下:

f751df5a9889de64f49e68530c670da7_7af94e60e9e64e43a7fcab9ea13b7c53.png


我们发现,调用了三次拷贝构造函数。


🔍 我们来调试看一下:


① 第一句 "调用了一次拷贝构造" 是因为 Date d4(d3) ,我们自己调用的。

f0d89027f10a916870a2c5658781f060_4ea510bb156a45a9ade1675b02b3a1d7.png

② 第二句出自 d1 = d2 = d3 ,先是 d2 = d3。


因为传值返回不会直接返回对象,而是会生成一个拷贝的对象。

36431cceb2ee7ac9746f9ee9a6cef801_fca54e2e6f5647fd9a4490f694ffd8ca.png

③ 第三句是 d2 = d3 搞完后把返回值作为参数再去调用:

a508b033c65b3ea3258fdc4b46979921_e2c7f4f72a5e47dcb5d285cc7867a2ce.png

所以一共三次:

500bc6b2ca41b8d20cc2f8648b997877_b804c1ed9a2e473cb7bc119ecb11c012.png


💬 我们这里出了作用域,对象还在,就可以使用引用返回:

1be9e431d9e56ca4d5087298299dd4a7_d3802c7304444dbda98a88cc858e9549.png


/* 赋值运算符重载:d1 = d3 */
  Date& operator=(const Date& d) {
        if (this != &d) {
      this->_year = d._year;
      this->_month = d._month;
      this->_day = d._day;
        }
  return *this;   // 返回左操作数d1
  }

🚩 运行结果如下:

4ca73fa158f571cd2a16d071c7b332da_6a41365571854202b129535a2d0c73b3.png


 成功减少了拷贝!


📚 赋值运算符重载是默认成员函数,所以如果一个类没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝。


#include <iostream>
class Date {
public:
  Date(int year = 1970, int month = 1, int day = 1) {
  this->_year = year;
  this->_month = month;
  this->_day = day;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main(void) {
  Date d1;
  Date d2(2022, 5, 1);
  // 这里 d1 调用的编译器自动生成operator完成拷贝,d2和d1的值也是一样的。
  d1 = d2;
  return 0;
}

e370f6f9194f0041cf4f31685bb8725b_64640c2eb42d4dc693138f779cf41235.png


既然编译器会自己默认生成,已经可以完成字节序的值拷贝了,我们还需要自己实现吗?


(上一篇博客我们经常问这种问题,以后就不再多说了)


当然像日期这样的类是没有必要的,有时候还是需要自己实现的。


💬 比如下面这种情况:


#include <iostream>
using namespace std;
class String {
public:
  String(const char* str = "") {
  this->_str = (char*)malloc(strlen(str) + 1);
  strcpy(this->_str, str);
  }
  ~String() {
  cout << "~String() 吱吱吱" << endl;
  free(this->_str);
  }
private:
  char* _str;
};
int main(void)
{
  String s1("hello");
  String s2("world");
  s1 = s2;
  return 0;
}

addf540bfcdb411a04f1a9f30d86c72d_1ceec95c0629495cb2ffa018f2246472.png


📌 编译器默认生成复制重载,跟拷贝构造做的事情完全类似:


① 内置类型成员,会完成字节序值拷贝 —— 浅拷贝。


② 对于自定义类型成员变量,会调用它的 operator= 赋值。

6faa3401af70c2e79235a5486d1a96f2_bf17175f32e841a78fb54735e0c4de4f.png



Ⅲ.  const 成员


0x00  引入

我们定义一个日期类,对它调用 Print ,是可以调得动的。


#include <iostream>
class Date {
public:
  Date(int year = 1970, int month = 1, int day = 1) {
  _year = year;
  _month = month;
  _day = day;
  }
  void Print() {
  printf("%d-%d-%d\n", _year, _month, _day);
  }
private:
  int _year;
  int _month;
  int _day;
};
int main(void) {
  Date d1;
  d1.Print();
  return 0;
}

480f5f1c2bb7cb7e5b74fd819cb3aad6_d187ebfa91f14666a600dc175da75018.png


❓ 如果我这个对象是 const 的呢?

#include <iostream>
class Date {
public:
  Date(int year = 1970, int month = 1, int day = 1) {
  _year = year;
  _month = month;
  _day = day;
  }
  void Print() {
  printf("%d-%d-%d\n", _year, _month, _day);
  }
private:
  int _year;
  int _month;
  int _day;
};
int main(void) {
  Date d1;
  d1.Print();
  const Date d2;
  d2.Print();
  return 0;
}


这样编译就报错了,这块报错的原因是什么?


这里涉及的问题是 "权限的放大" ,这个知识点我们再前几章讲过。


我们可以使用 const 成员函数来解决这种情况,我们继续往下看。


0x01  const 修饰类的成员函数

 将 const 修饰的类成员函数,我们称之为 const 成员函数。


const 修饰类成员函数,实际修饰的是该成员函数隐含的 this 指针,


表明在该成员函数中不能对类的任何成员进行修改。


💬 这里我们可以在函数后面加 const,保持权限的统一:


void Print() const;
class Date {
public:
  Date(int year = 1970, int month = 1, int day = 1) {
  _year = year;
  _month = month;
  _day = day;
  }
  void Print() const {
  printf("%d-%d-%d\n", _year, _month, _day);
  }
private:
  int _year;
  int _month;
  int _day;
};
int main(void) {
  Date d1;
  d1.Print();
  const Date d2;
  d2.Print();
  return 0;
}

🔑 直接看图详解:


为了能够更好地展示出 this 指针传递和接收的过程,我用黑色代码块表示。

26f1fdd827fe4b611e2244a5f4f3703c_489e4a7a0a9b4c5fa7506a0f57ab23cf.png



0x02  使用建议

成员函数加 const 是很好的!


建议能加上 const 都加上,这样普通对象和 const 对象都可以调用了。


但是,如果要修改成员变量的成员函数是不能加的,比如日期类中 += ++ 等等实现。


它是要修改的,你加 const 还怎么修改成员变量??直接让它爬!


(因为加 const,指向的类容不可被修改)


Ⅳ.  取地址重载(默认成员函数)


0x00 取地址操作符重载

💬 取地址运算符重载也是一个默认成员函数。


#include <iostream>
using namespace std;
class Date {
public:
  Date(int year, int month, int day) {
  _year = year;
  _month = month;
  _day = day;
  }
  Date* operator&() {
  return this;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main(void)
{
  Date d1(2022, 2, 2);
  cout << &d1 << endl;   // 取出d1的地址
  return 0;
}


🚩 运行结果如下:

0a23ce2c6bfd437f4b11e2e555631bcf_216347aca20d4e448db16103fb55d8d0.png



0x01 const 取地址运算符重载

💬 const 取地址运算符重载:


#include <iostream>
using namespace std;
class Date {
public:
  Date(int year, int month, int day) {
  _year = year;
  _month = month;
  _day = day;
  }
  const Date* operator&() const {
  return this;   // this就是地址,返回this就是返回地址。
  }
private:
  int _year;
  int _month;
  int _day;
};
int main(void)
{
  const Date d2(2022, 1, 1);
  cout << &d2 << endl;   // 取出d2的地址
  return 0;
}

🚩 运行结果:

15b59abfb15cd51ef35d38327b67bd97_33d5bdc26bd944238142730f60166b1d.png


0x02 建议

这两个运算符一般不需要重载。


因为它是默认成员函数,编译器会自己默认生成。


💬 其实让编译器自己去生成,就够 了:

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
class Date {
public:
  Date(int year, int month, int day) {
  _year = year;
  _month = month;
  _day = day;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main(void)
{
  Date d1(2022, 2, 2);
  cout << &d1 << endl;   // 取出d1的地址
  const Date d2(2022, 1, 1);
  cout << &d2 << endl;   // 取出d2的地址
  return 0;
}


🚩 运行结果:

d3e1931316c251d6287c26fb80f3dbd8_8510d46ab3754760acdf02d3116ba931.png



💬 只有特殊情况才需要重载,比如你不想让别人取到你的地址:


#include <iostream>
using namespace std;
class Date {
public:
  Date(int year, int month, int day) {
  _year = year;
  _month = month;
  _day = day;
  }
  Date* operator&() {
  // return this;  我不想让你取我的地址
  return nullptr;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main(void)
{
  Date d1(2022, 2, 2);
  cout << &d1 << endl;
  return 0;
}

🚩 运行结果:

d486a9231bd20fc55f1f9c098221bc42_a62382a36b6748bb9b9ac5f8a6b1d5c0.png


相关文章
|
3天前
|
程序员 编译器 C++
C++中的运算符重载(Operator Overloading)
C++中的运算符重载(Operator Overloading)
12 1
|
3天前
|
存储 Serverless 数据安全/隐私保护
C++ 类的成员函数和数据成员的技术性探讨
C++ 类的成员函数和数据成员的技术性探讨
11 0
|
11天前
|
C++
C++中的const指针与const引用
C++中的const指针与const引用
26 2
|
3天前
|
编译器 C++
C++中的内联函数与const限定词的使用
C++中的内联函数与const限定词的使用
12 1
|
5天前
|
编译器 C语言 C++
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值(下)
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值
14 1
|
5天前
|
编译器 C语言 C++
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值(中)
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值
8 1
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值(中)
|
5天前
|
存储 安全 C语言
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值(上)
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值
10 2
|
7天前
|
程序员 编译器 C++
c++重载运算符和重载函数
c++重载运算符和重载函数
13 1
|
7天前
|
C++
C++运算符重载
C++运算符重载
14 0
|
11天前
|
C++
C++ 重载 数组对象输入输出流的实现!!!
C++ 重载 数组对象输入输出流的实现!!!