🔥 🔥 🔥 🔥 🔥  火速猛戳订阅 👉  ​​《C++要笑着学》​​   👈 趣味教学博客 🔥 🔥 🔥 🔥 🔥

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

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

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

   [ 本篇博客热榜最高排名:4

写在前面:

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

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


Ⅰ.  运算符重载

0x00 引入

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

 什么是运算符重载呢?

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

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

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

0x01 运算符重载的概念

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

比如:
operator+
operator>
operator==

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

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

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

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

📌 注意事项:

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

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

operator@  ❌

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

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

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

this,限定为第一个形参。

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

.          (点运算符)

:: (域运算符)

.* (点星运算符,)

?: (条件运算符)

sizeof

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

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

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

⑥ 会自动转化

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

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

cout << (d1 > d2) << endl;
cout << operator>(d1, d2) << endl;

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

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

#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;
}

🚩 运行结果演示:

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

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

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

我们得把 private 撤掉:

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

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

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

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

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

#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 概念

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

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

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

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

📌 要分清!

int main(void) 
{
/* 一个已经存在的对象初始化一个马上创建实例化的对象 */
Date d3(d1); // 拷贝构造

/* 两个已经存在的对象,之间进行赋值拷贝 */
Date d1(2022, 3, 8);
Date d2(2022, 5, 1);
d1 = d2; // 让 d1 和 d2 一样

return 0;
}

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

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

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

① 参数类型

② 返回值年

③ 检查是否给自己复制

④ 返回 *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 语句来判断就是为了防止极端情况下,

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

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

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

💬 我们来验证一下:

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

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

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

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

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

#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;
}

🚩 运行结果如下:

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

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

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

🔍 我们来调试看一下:

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

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

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

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

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

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

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

所以一共三次:

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

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

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

/* 赋值运算符重载: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
}

🚩 运行结果如下:

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

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

  成功减少了拷贝!

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

#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;
}

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

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

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

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

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

💬 比如下面这种情况:

#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;
}

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

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

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

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

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

Ⅲ.  const 成员

0x00  引入

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

 我们定义一个日期类,对它调用 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;
}

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

❓ 如果我这个对象是 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;
}

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

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

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

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

我们可以使用 const

0x01  const 修饰类的成员函数

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

  将 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 指针传递和接收的过程,我用黑色代码块表示。

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

0x02  使用建议

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

 成员函数加 const 是很好的!

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

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

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

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

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

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

0x00 取地址操作符重载

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

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

#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;
}

🚩 运行结果如下:

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

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;
}

🚩 运行结果: 

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

0x02 建议

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

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

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

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

#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;
}

🚩 运行结果:

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

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

#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;
}

🚩 运行结果: 

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


参考资料:

Microsoft. MSDN(Microsoft Developer Network)[EB/OL]. []. .

百度百科[EB/OL]. []. https://baike.baidu.com/.

比特科技. C++[EB/OL]. 2021[2021.8.31]. .

📌 笔者:王亦优

📃 更新: 2022.3.18

❌ 勘误:​​一位13岁的编程爱好者​​ : operator=那里参数忘了加引用。【已修正】

​​可回收艺术家​​:某处d2调用打印函数,误打印了d1 —— d1.Print(); 【已修正】

📜 声明: 由于作者水平有限,本文有错误和不准确之处在所难免,本人也很想知道这些错误,恳望读者批评指正!

本章完。