⚡【C++要笑着学】(8) 重载:运算符重载 | 赋值重载 (默认) | const 成员 | const 修饰成员函数 | 取地址重载 (默认) | const 取地址重载 (默认)

本文详细介绍了C++中运算符重载的概念、注意事项、赋值重载、const成员和取地址重载。通过实例演示了如何重载运算符、保证封装性和使用默认成员函数。掌握这些技巧有助于理解STL底层实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

阅量破千的火热 C++ 教程
👉 火速订阅
《C++要笑着学》 

 🔥 CSDN 累计订阅量破千的火爆 C/C++ 教程的 2023 重制版,C 语言入门到实践的精品级趣味教程。
了解更多: 👉 "不太正经" 的专栏介绍 试读第一章
订阅链接: 🔗《C语言趣味教程》 ← 猛戳订阅!

  本篇博客全站综合热榜最高排名:4

💭 写在前面:我是亦优叶子,本章将开始讲解运算符重载。运算符重载的技能是学习实现 STL 内部底层的不可缺少的 "利器" !所以本篇非常重要,下一篇会手把手实现一个 Date 类,可以进一步地实战体会运算符重载。

目录

Ⅰ.  运算符重载(Operator Overloading)

0x00 引入:什么是运算符重载?

0x01 运算符重载的概念

0x02 运算符重载的注意事项

0x03 探讨:如何保证封装性?

 0x04 不支持重载的运算符

Ⅱ.  赋值重载(Assignment Overload)

0x00 引入:也是默认成员函数?

0x01 默认成员函数:operator=

0x02 验证:使用引用可以有效减少拷贝

0x03 关于编译器默认生成

Ⅲ.  const 成员(const-member)

0x00  引入:继续讨论 "权限的放大"

0x01  const 修饰类的成员函数

0x02  建议:尽量使用 const 成员函数

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

0x00 引入:默认成员函数之取地址重载

0x01 const 取地址运算符重载

0x02 一般不需要自己实现

0x03 特例:不让别人取到你的地址


Ⅰ.  运算符重载(Operator Overloading)

0x00 引入:什么是运算符重载?

 什么是运算符重载呢?

C++ 为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,

也具有返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。

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

0x01 运算符重载的概念

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

比如:
operator+
operator>
operator==

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

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

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

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

0x02 运算符重载的注意事项

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

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

operator@  ❌

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

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

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

会自动转化

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

🚩 运行结果演示:

 这里会发现运算符重载成全局的,不得不将成员变量是共有的,得把 private 撤掉:

0x03 探讨:如何保证封装性?

❓ 那么问题来了,封装性如何保证?这里其实可以用 "友元" 来解决!

(如果现在不知道也没关系,我们后面会讲)

 或者干脆直接重载成成员函数就完事儿了,上一章我们学的 this 指针:

#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 写上(虽然可以不写)。

 0x04 不支持重载的运算符

 注意,有五个运算符不支持重载!慎之慎之!

.          (点运算符)

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

sizeof

值得一提的是:虽然点运算符 .  不能重载,但是箭头运算符 -> 是支持重载的

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

"网络上不知名的冲浪大神发明的,个人感觉挺不错。"

Ⅱ.  赋值重载(Assignment Overload)

0x00 引入:也是默认成员函数?

赋值重载 (Assignment overload) ,operator= 

用来 把一个对象赋值给另一个对象,赋值重载是默认成员函数之一。

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

0x01 默认成员函数: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 语句来判断就是为了防止极端情况下,

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

0x02 验证:使用引用可以有效减少拷贝

 刚才返回值为什么要用引用?

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

" 减少拷贝,极致优雅。"

💬 我们来验证一下:

我们先把引用返回去掉:

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

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

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

"经典之嗷嗷嗷……"

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

🚩 运行结果如下:

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

🔍 调试:我们来调试看一下

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

第二句则出自 d1 = d2 = d3 ,先是 d2 = d3,从右往左。

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

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

 所以一共三次,是不是茅塞顿开了?犹如拨开云雾见到了青天?

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

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

🚩 运行结果如下:

 成功减少了拷贝!

0x03 关于编译器默认生成

赋值运算符重载是默认成员函数,所以如果一个类没有显式定义赋值运算符重载,

编译器也会生成一个,完成对象按字节序的值拷贝。

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

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

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

当然像日期这样的类是没有必要的,有时候还是需要自己实现的,比如下面这种情况:

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

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

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

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

Ⅲ.  const 成员(const-member)

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

❓ 如果我这个对象是 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 成员函数。

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

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

 注意,我说的是全部!加了表示整个函数都不对任何成员变量动手!

void Print() const;   👈 它的位置也很特别,实际就是修饰this的
this 你也看不着,所以这里的 const 孤零零的,就比较地 "显眼"

这里我们可以在函数后面加 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 指针传递和接收的过程,我用黑色代码块表示。

0x02  建议:尽量使用 const 成员函数

 成员函数加 const 是很好的!

建议能加上 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;
}

🚩 运行结果如下:

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

🚩 运行结果: 

0x02 一般不需要自己实现

"取地址重载只是在极少数情况下需要自己实现"

  取地址重载和 const 取地址重载一般不需要自己实现。

它是默认成员函数,编译器会自己默认生成,让编译器自己去生成就够了,这里是真的够了!

💬 代码演示:让编译器自己生成就行

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

🚩 运行结果如下:

0x03 特例:不让别人取到你的地址

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

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

🚩 运行结果: 

当然,这里也是要需要根据业务开发需要去选择的。

大多数情况下,取地址重载是完全不需要我们自己实现的。

​​

📌 [ 笔者 ]   王亦优
📃 [ 更新 ]   2022.3.18 | 2023.10.3(重制)
❌ [ 勘误 ]   ​一位13岁的编程爱好者 : operator=那里参数忘了加引用。【已修正】
              可回收艺术家:某处d2调用打印函数,误打印了d1 —— d1.Print(); 【已修正】
​
📜 [ 声明 ]   由于作者水平有限,本文有错误和不准确之处在所难免,
              本人也很想知道这些错误,恳望读者批评指正!

📜 参考资料 

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

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

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

评论 48
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

王平渊

喜欢的话可以支持下我的付费专栏

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值