赋值运算符重载:从无到有与从有到有,六大默认函数收官
我的github:(https://github.com/xcx55/ubuntu-linux-project)
感谢各位大佬参观我的github!!
运算符重载里有一个地位特殊的:赋值运算符 =。它是六大默认成员函数之一,跟拷贝构造长得像却干的是两码事。这篇把它写完,六大默认成员函数就收官了。
一、从无到有 vs 从有到有
先看两段代码,区分清楚了这篇就赢了一半:
cpp
Date d1(2024, 4, 26);
Date d2 = d1; // 拷贝构造!d2 还不存在,用 d1 初始化它------从无到有
Date d3(2025, 4, 28);
d1 = d3; // 赋值重载!d1、d3 都已存在------从有到有
笔记里那句原话值得单独放大:
用一个对象去创建另一个对象就是拷贝构造,是从无到有!而赋值运算符重载是两个都已存在。
最容易骗人的是 Date d2 = d1;------长得像赋值,实际是拷贝构造 (d2 正在诞生,不存在"两个对象")。判断标准就一条:看左边的对象是不是已经存在。
d1 = d3 则是 d1.operator=(d3) 的一次调用,等于号是一种特殊的 operator 函数。而且它有个特权:其他 operator 函数都可以是全局函数,= 必须是成员函数------它是六大默认函数之一,规定写在类里面(不写编译器自动生成,也无法用全局函数代替)。
二、赋值重载的标准写法
cpp
Date& operator=(const Date& d)
{
if (this != &d) // 防自赋值
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this; // 返回自身,支持连续赋值
}
三个要点:
- 参数建议 const 引用------传值传参会触发拷贝构造,白亏一次;
- 有返回值,且建议返回当前类类型的引用 ------返回
*this是为了支持d1 = d2 = d3连续赋值(右边的先算完返回自身,继续给左边赋); - 防自赋值 ------
d1 = d1在有资源的类里不判一下会出事故(自己 free 自己再拷贝,数据没了)。
三、默认生成的赋值重载:跟拷贝构造一个毛病
不写的话编译器自动生成,行为跟默认拷贝构造类似:内置类型成员完成值拷贝(一个字节一个字节的浅拷贝),自定义类型成员调用它自己的赋值重载。
所以对 Date 这种纯 int 的类,不写,编译器生成的就够了------两个已存在的对象互相拷成员,浅拷贝没毛病。
但对 Stack 这种成员里有指针的类,赋值重载和拷贝构造是同一个坑的两面:有资源(动态开辟的空间)就必须自己写 ------先释放自己的旧空间,再 new 出新空间把对方的数据深拷贝进来,最后返回 *this。拷贝构造是"出生时深拷贝",赋值重载是"生存期中深拷贝",一个都不能省。
至此六大函数的行为规律凑齐了:
| 函数 | 不写会怎样 | 什么时候必须自己写 |
|---|---|---|
| 构造函数 | 内置类型成员不初始化 | 基本都要写(重载适配) |
| 析构函数 | 不清理资源 | 成员有动态资源 |
| 拷贝构造 | 浅拷贝 | 成员有动态资源(深拷贝) |
| 赋值重载 | 浅拷贝 | 成员有动态资源(深拷贝) |
| 取地址重载 | 够用 | 几乎不用写 |
| const取地址重载 | 够用 | 几乎不用写 |
思维导图总结就是三大块:初始化和清理 (构造+析构)、拷贝复制 (拷贝构造+赋值重载)、取地址重载(普通对象和 const 对象取地址,很少自己实现)。
四、前置++ / 后置++:一个 int 参数的约定
++ 有前置后置两种,重载函数名都叫 operator++,怎么区分?C++ 的约定:后置版本多一个 int 形参,跟前置构成重载:
cpp
Date& operator++() // 前置:先加再返回
Date operator++(int) // 后置:int 只是标记,先保存旧值再自增,返回旧值
那个 int 不用来传值,纯粹是个"后置"的标记位------语言设计的巧思,也解释了为什么后置 ++ 对自定义类型更贵(要拷贝一份旧值)。
五、收官前补一块底:编译器到底怎么找符号
六篇下来,"编译器找名字"的规则散落各处,借收官把 3-14 晚上那篇《域--链接--编译》的账算总:
- 域四级:局部域 → 全局域 → 命名空间域 → 类域 ;编译器遇到符号先在当前局部域找声明/定义,找不到先保留,去当前文件全局域找;
- 对函数,先有声明才会去大符号表找定义------没有声明,编译器根本不会去查表;声明可以在局部、全局、命名空间、类任何域里;
- 类域和命名空间域编译器不会主动进 ,除非
::或对象.触发:以类创建对象后,访问它的成员函数就去类域找声明,声明有了再去找定义; - 声明和定义都没有空间------只有链接完成后,才在代码段为定义分配空间。声明只是描述,定义才分配空间;
- 回收旧账:inline 函数在编译期完成替换,决定它只能在当前 .cpp 里找 (不进符号表);static 函数不进大表,别的文件永远找不到。
这套规则就是前面 namespace、inline、this、类域所有细节的总开关------查找规则只有一套,各种语法都在它上面跳舞。
六大默认成员函数到此收官:构造/析构管初始化和清理,拷贝构造/赋值重载管拷贝复制(从无到有 vs 从有到有),两个取地址重载几乎不用管。下一篇进"下":初始化列表、explicit、static 成员与友元------类和对象的最后一批语法。