%%
# 纲要
> 主干纲要、Hint/线索/路标
# Q&A
#### 已明确
#### 待明确
> 当下仍存有的疑惑
**❓<font color="#c0504d"> 有什么问题?</font>**
%%
# 类的成员函数类别
类的成员函数的按**功能类型**可划分为:
- **构造函数**(constructor)
- **析构函数**(destructor)
- **运算符重载函数**(operator overloading)
- **转换函数**(converting function)
- 其他成员函数
### 拷贝控制操作
其中,一个类的**拷贝、移动、赋值、销毁**的具体操作,统称 "**拷贝控制操作**"(copy control),通过对应的五种特殊成员函数来进行控制:
- **析构函数**(destructor)
- **拷贝构造函数**(copy constructor)
- **拷贝赋值运算符**(copy-assignment operator)
- **移动构造函数**(move constructor)
- **移动赋值运算符**(move-assignment operator)
> [!NOTE] 五种 "**拷贝控制操作**" 之间彼此关联,通常符合 "三/五法则":
>
> **Rule-of-Three**:
>
> - 如果一个类需要一个自定义的==**析构函数**==,则几乎肯定其同时需要一个自定义的==**拷贝构造函数**==、==**拷贝赋值运算符**==
> - (需要手动释放资源,意味着拷贝时需要**自定义实现深拷贝**)
>
> - 如果一个类需要自定义的==**拷贝构造函数**==,则几乎肯定其同时需要一个==**拷贝赋值运算符**==,反之亦然。但是,并不一定需要析构函数。
>
> **Rule-of-Five**:
>
> - 如果一个类**自定义了任何一个拷贝操作**,则**应当自定义所有五个操作**(析构函数、拷贝构造函数、拷贝赋值、移动构造函数、移动赋值)
>
> [!NOTE]
> 当定义一个类时,应当定义对象**拷贝**、**移动**、**赋值**或**销毁**时的处理细节:
>
> - 实现**拷贝构造函数**、重载**拷贝赋值运算符**,避免编译器自动定义时导致资源的浅拷贝;
> - 实现**移动构造函数**、**移动赋值运算符**,实现资源移动而不是复制。
> - 实现**析构函数**来释放动态分配的内存;
<br>
#### 编译器可自动提供的合成版本函数
C++术语下,"**特殊成员函数**" 是指编译器可能自动生成的函数:**默认构造函数、析构函数、拷贝操作、移动操作**。
> [!important] 编译器为类**自动生成特殊成员函数(称之为 "**==Synthesized==**",合成的)** 的规则如下:
>
> 参见 [^1] [^2]
>
> - **==默认构造函数==** <= **仅当 "未显式定义任何构造函数" 时才提供**;
> - **==析构函数==** <= **若没有显式定义就会提供**;
> - **==拷贝构造函数==**、**==拷贝赋值运算符==** <= 仅当**未显式定义移动操作**时才提供(浅拷贝);
> - **==移动构造函数==、==移动赋值运算符==** <= 仅当**未显式定义任何拷贝控制成员**(包括继承来的基类部分的),且**类的每个非 static 的数据成员都可以移动**(移动构造或移动赋值)时,编译器才会提供;
>
> **编译器自动合成的拷贝构造操作都是 `public` 且 `inline` 的**。
>
> 注意:
>
> - 上述各项中,当**条件不满足**时,**编译器会==将相应函数声明为 `= delete`==**。
> - 仅当上述某一函数在程序中有被调用时,编译器才会考虑为其自动生成。
> - 声明 `= default` 即**显式定义**。
> - 例如**声明虚析构函数 `virtual ~MyClass() = default` 后**,**编译器将不会自动生成合成的移动操作**。
> [!caution] 若类中存在有 "不可默认构造、拷贝、复制、移动或销毁" 的数据成员,则 "编译器为该类生成的相应成员函数" 将被定义为 `= delete`
>
> 参见 [^3] [^4] ,例如:
>
> 存在下列情况时,编译器自动生成的 "**默认构造函数**" 定义为删除的:
>
> - 当类中具有 "**引用**" 成员,且没有类内初始化项时;
> - 当类中具有 `const` 成员,且**没有类内初始化项**,且**其类型无默认构造函数**时;
> - 当类中**某个成员的"析构函数" 是删除的( `= delete`) 或不可访问 (`private`)** 时;
>
> 存在下列情况时,编译器自动生成 "**拷贝操作**" 定义为删除的(**移动操作类同**):
>
> - 当类中具有 "**引用**" 或 **"const" 数据成员**时;
> - 当类中**某个成员(包括基类成员)的拷贝操作是删除的( `= delete`) 或不可访问 (`private`)** 时;
> - 当类中**某个成员的"析构函数" 是删除的( `= delete`) 或不可访问 (`private`)** 时;
>
> 由此,当存在会触发拷贝构造或赋值的语句时,将导致编译错误。
对于一个空类,编译器为其自动生成的函数如下所示[^5]:
```cpp
// 声明了一个空类
class Empty {};
// 等价于如下定义, 编译器自动提供6个特殊成员函数
class Empty {
public:
Empty() {} // 默认构造函数
~Empty() {} // 析构函数
Empty(const Empty&) {} // 拷贝构造函数
Empty(Empty&&) noexcept {} // 移动构造函数
Empty& operator=(const Empty&) {} // 重载拷贝赋值运算符
Empty& operator=(Empty&&) noexcept {} // 重载移动赋值运算符
};
// 只有当某个函数有被调用时, 才会被编译器创建出来. 例如存在下述语句:
Empty e1; // default构造函数
Empty e2(e1); // copy构造函数
e2 = e1; // copy assignment操作符
```
> [!NOTE] 为**避免**编译器自动生成拷贝控制成员,可采取三种方式 [^6] [^3]
>
> - (1)**使用 `= delete` 关键字声明**;
> - (2)将成员函数**声明为 `private` 并且不定义**;
> - 外部函数不可访问,编译时将报告 "不可访问" 错误。
> - 友元函数或成员函数可访问,但 "链接" 时将报告 "未定义" 错误(试图访问未定义成员,导致的是 "链接错误")
> - (3)声明**一个 `Uncopyable` 基类**,**其中将需拒绝的成员函数声明为 `private`**,**再由目标类==私有继承==**。
>
> 方式二示例:
>
> ```cpp
> class Uncopyable {
> protected:
> Uncopyable() {};
> ~Uncopyable() {};
> private:
> Uncopyable(const Uncopyable&); // 阻止拷贝;
> Uncopyable& operator=(const Uncopyable&);
> };
>
> // 目标类继承自 Uncopyable, 由于基类拷贝操作私有, 故编译器不能为该子类自动生成拷贝操作.
> class MyClass : private Uncopyable {
> ...
> };
> ```
<br><br>
# 类的构造函数
类的构造函数包括以下种类:
| | 说明 |
| ---------- | ------------------------------ |
| **默认构造函数** | 没有任何参数,或所有参数都有默认值的构造函数。 |
| **拷贝构造函数** | 以同类型的 **==(常量)对象==**作为参数的构造函数。 |
| **移动构造函数** | 以同类型对象的 **==右值引用==**作为参数的构造函数。 |
| **委托构造函数** | 委托另一个构造函数完成初始化。 |
| **继承构造函数** | 子类使用基类的构造函数。 |
| **常规构造函数** | 上述类型以外,带有一个或多个参数的构造函数。 |
根据构造函数是否被 `explict` 修饰,可划分为:
- **显式构造函数**:由 `explice` 修饰的构造函数,禁止隐式类型转换情况下调用
- **转换构造函数**:所有 `non-explict` 的构造函数均属于转换构造函数。
> [!NOTE] 构造函数不能被声明为 `const`,但可以是 `constexpr` 的
<br>
## 默认构造函数
默认构造函数**不接受任何参数,或者所有参数都有默认值**,用于在 "未提供任何参数" 、或者 "未显式调用构造函数" 时创建一个实例对象。
声明 "默认构造函数" 有两种方式:
1. 定义一个**没有参数**的构造函数;
2. 为已有构造函数的所有参数**提供默认值**(等价于**同时定义了默认构造函数**)
> [!NOTE] 仅当 "**==类中没有声明任何构造函数==**" 时,**编译器**才会自动提供一个 **==默认构造函数==**(空函数),称为 "**合成的默认构造函数**(synthesized default constructor)"。
>
> 如果类存在有一个自定义的其他构造函数,则编译器**不再自动提供默认构造函数** 。
```cpp
// 方式一: 定义一个没有参数的构造函数
class Stock {
string company;
long shares;
public:
Stock(); // 默认构造函数, 无任何参数;
};
Stock::Stock() : company("no name"), shares(0) {}
// 方式二: 为已有构造函数的所有参数提供默认值
class Stock {
string company;
long shares;
public:
// 所有参数均带有默认值, 该构造函数将作为默认构造函数.
Stock(const string &co = "no name", long shares = 0);
};
```
<br>
## 拷贝构造函数
拷贝(复制)构造函数 :**首项参数**是==**自身类类型的常量引用**==,==**无其它参数**== 或 ==**其他参数都有默认值**== 的构造函数。
拷贝构造函数**根据一个已存在的"同类型"对象来创建另一个新对象**。
使用场景如下:
1. 用于**初始化**过程中,而不是赋值过程。
2. 每当生成"对象副本", 即函数参数 "**按值传递**" 对象、或 "**按值返回**" 对象时, 都会调用复制构造函数。
```cpp
class MyClass {
public:
MyClass(const MyClass& other) { // 拷贝构造函数
// 拷贝构造函数的实现
// 通常涉及复制other的每个数据成员. 注意进行深拷贝.
}
};
```
> [!NOTE]
> - 拷贝构造函数**通常不应该是 `explict` 的**,以便在拷贝初始化等多种场合能够被隐式使用。
> - `non-explicit` 的拷贝构造函数**既能在"直接初始化"中被调用,也能在拷贝初始化中被使用**。
>
<br>
### 合成的拷贝构造函数
当用户未定义拷贝构造函数时,编译器会自动提供一个**合成的拷贝构造函数**(Synthesized Copy strstructor)。
合成拷贝构造函数**只会对"非静态数据成员"进行==浅拷贝==**:
- 对基本类型,直接拷贝,拷贝"**成员的值**";
- 对类类型,调用其 "**拷贝构造函数**" 进行拷贝;
- 对数组,将会**逐元素拷贝**,遵循按上述规则
> [!caution] "浅拷贝" 意味着对于**指针**,只是单纯地==**复制了地址**==,指向同一内存空间。
>
> 如果一个对象包含一个指针成员,则合成拷贝构造函数将**导致该对象指针所指的地址被复制给另一个对象,两个对象的指针成员将指向同一片内存空间**。
<br>
### "拷贝构造函数" 的调用示例
使用已有对象来创建新对象,这一 "**初始化**" 过程总是会调用 "**拷贝构造函数**"。
```cpp
// 注: garment是Stock类型对象
//------------------------调用 "复制构造函数"
Stock gove(garment); // 直接初始化, 调用复制构造函数
Stock gove = garment; // 拷贝初始化, 调用复制构造函数
Stock gove = Stock(garment); // 直接初始化&拷贝初始化, 调用复制构造函数
Stock *pg = new Stock(garment); // 直接初始化, 然后将地址存给指针*pg.
//------------------------使用赋值运算符
Stock PG(garment);
gove = PG; // 赋值;调用重载赋值运算符函数
```
### 拷贝省略
上述示例的四个初始化语句中,**第三条**的执行过程可能有两种:
1. 调用拷贝构造函数直接创建 gove;
2. 调用拷贝构造函数**生成一个临时对象**,再调用拷贝构造函数用临时对象初始化 gove;
通常编译器会做优化(URVO 优化),实际过程是上面的(1)。
同理,对于 "**函数返回类型为类类型**" 时,理论上 return 语句会隐式调用类的**拷贝构造函数**,对函数体内生成的局部临时对象进行拷贝并返回。
但是,编译器会执行 **RVO 优化**,即函数体内生成的临时对象**将直接构建在接受函数返回值的目标内存地址上**,从而省略拷贝构造的过程。
> [!info] C++17 起,URVO 优化已纳入 "标准" 规定,称之为 "==拷贝省略=="。参见 [[02-开发笔记/01-cpp/编译器相关/cpp-编译器优化#拷贝省略 Copy Elision|cpp-编译器优化#拷贝省略]]
<br><br>
## 移动构造函数
移动构造函数:**以同类型的另一个对象的 "==右值引用==" 作为参数的构造函数**。
移动构造函数接受一个 **==同类型对象的右值引用==** (`&&`) 作为参数,通过**转移资源(而非复制)实现对象创建**,将资源**从源对象转移到新创建的对象**。
![[02-开发笔记/01-cpp/类型相关/cpp-引用相关#^3qfpzd]]
> [!caution]
> 移动构造函数应当确保创建新对象后,源对象不再拥有原始资源,处于有效但"未定义"的状态,**确保源对象在被销毁时不会释放或删除已被转移的资源**。
>
移动构造函数的注意事项:
- **添加 `noexcept` 说明符**:向编译器显式说明该构造函数不抛出异常。
- **==资源转移==**:将源对象的资源(如**指向动态分配内存的指针**)直接赋值给目标对象。
- **==资源置空==**:在移动资源后,**必须将源对象的资源指针值为 `nullptr` ,从而确保源对象被析构/销毁时,不会影响或尝试释放已被转移走的资源;**
示例:
```cpp
class String{
private:
char *str;
int len;
public:
// 移动构造函数
String(String&& other) noexcept
: str(other.str), len(other.len) {
// 将原对象的资源指针置为 nullptr,长度置为 0
// 确保源对象不再指向已转移的资源
other.str = nullptr;
other.len = 0;
}
};
```
![[02-开发笔记/01-cpp/类型相关/cpp-引用相关#^yh06d4]]
<br><br>
## 委托构造函数
**委托构造函数**(delegating constructor),自 C++11 引入。
"委托构造函数" **在其 "成员初始化列表" 中使用其所属类的==另一个构造函数==来完成初始化过程**,其**初始化列表只有==唯一一项**==,即类名本身(调用该类的其它构造函数)。
委托构造函数的目的是在多个构造函数之间共享相同的初始化代码,**==减少代码重复、集中初始化逻辑==**。
> [!caution]
> - 委托构造函数的**初始化列表中只能有==调用其它构造函数==的这一项**,不能再用于初始化成员变量。
> >
> - 委托构造函数在**执行完被委托的构造函数之前**不执行任何操作。
> 因此,**所有初始化逻辑都应放在==被委托的构造函数==中**。
> >
> - 两个构造函数**不能互相委托**,会导致编译错误。
使用示例:
```cpp
class MyClass {
public:
// 三个委托构造函数, 全部委托另一个构造函数来完成初始化过程.
MyClass() : MyClass("", 0, 0) {} // 该默认构造函数委托另一个函数
MyClass(std::string s) : MyClass(s, 0, 0) {} // 该函数委托另一个函数
MyClass(std::istream &is) : MyClass() { read(is, *this); } // 该函数委托默认构造函数
// 被委托的构造函数
MyClass(std::string s, unsigned cnt, double price) :
bookNo(s), units_sold(cnt), revenue(cnt*price) {}
//...
private:
std::string bookNo;
unsigned units_sold;
double revenue;
};
```
<br><br>
## 转换构造函数 (converting constructor)
**所有 "==non-explicit==" 的构造函数都称为 "==转换构造函数=="** [^7],包括 `non-explict` 的默认构造函数、拷贝构造函数、移动构造函数等。
转换构造函数提供了一种 "**隐式转换**":从其 "**==构造函数参数的类型==**" 到其 "**==所属类类型==**" 的转换。
> [!info] **转换构造函数在 "==直接/拷贝初始化==" 和"==显式/隐式类型转换==" 中均可被调用**,作为用户定义转换序列的一部分
>
> `explicit` 构造函数则只能被用于 "**直接初始化**" (包括静态强制转换等显式转换);
> [!example] [转换构造函数示例](https://en.cppreference.com/w/cpp/language/converting_constructor)
> ```cpp
> struct A
> {
> A() { } // converting constructor (since C++11)
> A(int) { } // converting constructor
> A(int, int) { } // converting constructor (since C++11)
> };
> ```
<br><br><br>
## 显式构造函数(explicit constructor)
由 `explict` 关键字所修饰的构造函数称为 "**==显式构造函数==**"。
**显式构造函数** 只能被"**直接调用**",不能用在**需要隐式类型转换**的场景来构造对象
- **只能被用于 "==直接初始化==",不能被用于 "==拷贝初始化**==" ;<br>(包括可用于直接列表初始化,而不可用于拷贝列表初始化)
- 只能被用于==**显式转换**==(如静态强制转换),不能被用于==**隐式转换**==(**函数传参**、**函数返回值**) 等需要进行的情况。
##### 标准库容器中常用的显式构造函数
C++标准库的**vector、list 等容器中:**
- **接受一个"==容量参数=="的单参数构造函数是 `explitct` 的**。
- 接受一个"**==容量参数==**"以及"**==元素初始值==**"双参数的构造函数也是 `explict` 的。
```c++ title:stl_exam.cpp
vector<int> vec(10); // 直接初始化, 具有10项元素的vector, 元素全被值初始化为0.
// vector<int> vec = 10; // error, `explicit`的构造函数, 禁止在拷贝初始化中隐式调用
vector<int> vec(10, 5); // 直接初始化, 10项元素, 全部初始化为5.
vector<int> vec = {10, 5}; // 拷贝列表初始化,2项元素,分别为10和5. 调用的是另一个构造函数
```
对比 `non-explicit` 的构造函数:
```cpp
struct MyClass {
MyClass(int) {
cout << "Invoke constructor MyClass(int)" << '\n';
}
MyClass(int, int) {
cout << "Invoke constructor MyClass(int, int)" << '\n';
}
};
MyClass obj1(10); // 直接初始化, 调用`MyClass(int)`
MyClass obj2 = 10; // 拷贝初始化, 调用`MyClass(int)`
MyClass obj3(10, 5); // 直接初始化, 调用`MyClass(int, int)`
MyClass obj4 = {10, 5}; // 拷贝初始化, 调用`MyClass(int, int)`
```
<br><br>
## 构造函数的使用
以"string"类为例:
```cpp
//------------------------------ C++ string类的使用示例------------------------------
string s0; // 默认初始化, 调用默认构造函数, 得到空串
string s1{}; // 值初始化, 调用默认构造函数
string s1 = {}; // 拷贝列表初始化=>值初始化, 调用默认构造函数
string s2(s1); // 直接初始化, 调用复制构造函数
string s2{s1}; // 直接初始化, 调用复制构造函数
string s2 = s1; // 拷贝初始化, 隐式调用复制构造函数
string s2 = {s1}; // 拷贝列表初始化, 隐式调用复制构造函数
string s2 = string(s1); // 直接初始化&拷贝初始化, 显式调用复制构造函数; 通常不推荐该语句;
string s3("value"); // 直接初始化, 调用"单参数"构造函数
string s3{"value"}; // 直接列表初始化, 调用"单参数"构造函数
string s3 = "value"; // 拷贝初始化, 隐式调用"单参数"构造函数
string s3 = {"value"}; // 拷贝列表初始化, 隐式调用"单参数"构造函数
string s3 = string("value"); // 直接初始化&拷贝初始化, 显式调用"单参数"构造函数
string s4(n, 'c'); // 直接初始化, 调用多参数构造函数.
```
> [!warning] "最令人困惑的解析"(Most vexing parse)
>
> C++中的"最令人困惑的解析"(Most vexing parse)是指**当一个声明语句==可以被解释为一个函数声明==时,编译器一定会将其解释成==函数声明==,然而编写这句代码的实际意图可能是定义一个对象**。
>
>
> ```cpp title:most_vexing_parse.cpp
> struct AnotherClass;
> struct MyClass {
> MyClass() = default;
> MyClass(const AnotherClass& obj) { cout << "invoke the constructor\n"; }
> };
>
> struct AnotherClass {
> AnotherClass() { cout << "invoke AnotherClass's the constructor\n"; }
> };
>
> // 二义性示例:
> // 下面代码的实际意图是"以 AnotherClass 类对象为构造函数参数, 创建一个 MyClass 实例对象".
> // 然而, 编译器会将其解释为一句"函数声明":
> // 声明了一个名为`obj`的函数, 返回类型为`MyClass`, 其接收一个参数,
> // 该参数是一个"指向函数的指针"——函数返回类型为 AnotherClass, 函数无参数.
> // 相当于解释为一个函数指针类型: `AnotherClass(void)`.
> MyClass obj(AnotherClass());
> // 同理, 下面语句实际意图为"调用 MyClass", 也被解释为一句函数声明:
> // 声明了一个名为`obj2`的函数, 返回类型为 MyClass, 无参数.
> MyClass obj2();
>
> // 解决方式如下:
> // (1) 使用`直接列表初始化`的语法
> MyClass obj11{AnotherClass()};
> MyClass obj21{};
> // (2) 使用额外的括号, 避免编译器将其解析为函数声明
> MyClass obj12((AnotherClass()));
> ```
> ^56y0yj
<br><br><br>
# 类的析构函数
析构函数的作用:**释放对象使用的资源**。
当类的析构函数被调用时,首先**执行函数体**,然后编译器自动 "**按初始化顺序的==逆序==**" 销毁其**数据成员**,**对于类类型成员则会递归地自动调用其析构函数**。
析构函数**自动调用**时机:
- 若对象是自动变量(局部变量),则**离开定义该对象的作用域**时,将自动调用其析构函数。
- 对于临时对象,当**创建其的完整表达式结束**时,被销毁。
- 若对象是静态变量(外部、静态、静态外部或来自名称空间),则在**程序结束时**将自动调用其析构函数。
- 若对象是类的数据成员,则**当其所属对象被销毁时**,该成员也被销毁。
- 若对象是用 new 创建的,**显式进行 `delete` 时** 将自动调用其析构函数。
> [!NOTE] 析构函数默认为 `noexcept` 的,不需要再显式使用该关键字
> [!caution] 析构 "函数体自身" 并不直接 "销毁成员",成员在析构函数体之后的隐含阶段被自动销毁。
>
> 析构函数体是作为成员销毁步骤之外的另一部分进行,例如显式调用 `delete` 释放动态内存。
> [!warning]
> 对 `new` 创建的对象,如果**直接调用其析构函数**而不是使用 `delete`,会导致:
>
> - **==内存泄漏==**:尽管对象的析构函数被调用并执行,但**由 `new` 分配的内存并没有被释放**。
> - **==二次析构==**:如果在调用析构函数之后又再调用 `delete`,那么对象的析构函数将再次被调用,导致二次析构。这是危险的,因为对象的资源可能已经被释放,再次尝试释放可能导致未定义的行为。
```cpp
class MyClass {
public:
~MyClass() {
std::cout << "Destructor called!" << std::endl;
}
};
int main() {
MyClass* obj = new MyClass;
obj->~MyClass(); // 第一次显式调用对象的析构函数, 但是堆上的内存空间并未被释放.
delete obj; // delete会再次调用对象的析构函数, 可能导致未定义行为; 此时释放堆内存,
return 0;
}
```
<br><br>
# 类的重载运算符
重载运算符**本质上是函数**,函数名为 "**`operator` 关键字 + 运算符符号**"。
例如**重载赋值运算符**即是名为 `operator=` 的函数(**"赋值" 重载能被声明为 `const` 成员函数**,因为赋值必然是一个修改对象本身的操作)
> [!NOTE] 以 "**类的成员函数**" 进行运算符重载时
>
> - 对于单目运算符,**运算对象**绑定为**隐式 `this` 指针**;
> - 对于双目运算符,**左侧运算符对象**绑定为**隐式 `this` 指针**,**==右侧运算对象==** 作为**显式参数**传递。
> [!caution] 赋值 `=`、下标 `[]`、函数调用 `()` 、指针访问 `->`、成员指针访问 `->*` 运算符只能作为 "**==成员函数==" 被重载** [^8]
>
> **其余运算符(例如 `+`, `-`, `*`, `/`, `==`, `!=` 等)既可作为 "成员函数" 被重载,也可作为普通函数或友元函数进行重载**
>
> ```cpp
> // 示例: 双目运算符 `+` 可作为成员函数, 或者普通函数(友元)进行重载.
> class Integer {
> public:
> Integer(): val(0) {}
> Integer(int v): val(v) {}
> // (1) `+ 作为成员函数重载时
> const Integer operator+(const Integer& rhs) {
> Integer tmp;
> tmp.val = this->val + rhs.val;
> return tmp;
> }
>
> // (2) `+` 作为友元函数重载时
> friend const Integer operator+(const Integer&, const Integer&);
>
> private:
> int val;
> };
>
> // (2) `+` 作为友元函数重载时
> const Integer operator+(const Integer& lhs, const Integer& rhs) {
> Integer res;
> res.val = lhs.val + rhs.val;
> return res;
> }
> ```
>
> [!NOTE] 后置 `++` / `--` 运算符接受一个额外的(不被使用)的 ==`int` 类型参数==,以区分前置版本
>
> ```cpp
> MyClass& MyClass::operator++(); // 前置++版本
> const MyClass MyClass::operator++(int); // 后置++版本
> ```
>
> [!example] 使用示例
>
> 对于需要**返回 "==临时量==" 的重载运算符**而言(例如算术`+` 与后置 `++`),**返回类型应当==为 `const` 对象==**,**==防止外部对临时量进行修改==**!
>
> ```cpp
> class Integer {
> public:
> Integer(): val(0) {}
> Integer(int v): val(v) {}
>
> Integer& operator=(const Integer& rhs) {
> if (this == &rhs) {
> return *this;
> }
> this->val = rhs.val;
> return *this;
> }
>
> // 前置++, 返回引用.
> Integer& operator++() {
> ++val;
> return *this;
> }
>
> // 后置++. 返回const对象. 需要接收一个int型参数作为标识符(不使用), 区分于前置重载.
> const Integer operator++(int) {
> Integer tmp = *this;
> ++val;
> return tmp;
> }
>
> const Integer operator+() const {
> return Integer(this->val);
> }
>
> const Integer operator+(const Integer& rhs) const {
> Integer tmp;
> tmp.val = this->val + rhs.val;
> return tmp;
> }
>
> bool operator==(const Integer& rhs) const {
> return this->val == rhs.val;
> }
>
> friend ostream& operator<<(ostream&, const Integer&);
> friend istream& operator>>(istream&, Integer&);
>
> private:
> int val;
> };
>
>
> ostream& operator<<(ostream& os, const Integer& rhs) {
> os << rhs.val;
> return os;
> }
>
> istream& operator>>(istream& is, Integer& rhs) {
> is >> rhs.val;
> return is;
> }
>
> int main() {
> Integer v1 = 1, v2 = 2;
> Integer v3 = v1 + v2;
> cout << ++v3 << endl;
> cout << v3++ << endl;
> cout << v3 << endl;
> return 0;
> }
> ```
>
<br>
## 拷贝赋值运算符
重载拷贝赋值运算符 `=`,用于实现复制另一个对象到当前对象(`this`对象)。注意点:
- **自赋值检查**:即对象尝试赋值给自身。
- **释放原资源**:在从 `other` 对象移动资源之前,确保释放 `this` 对象的旧资源
- **分配新资源&拷贝数据成员**:分配新资源并进行数据成员赋值
- 返回`this`对象:为支持链式赋值,通常需要返回"当前对象的引用"。
```cpp title:copy_assignment_operator.cpp
class MyClass {
public:
MyClass& operator=(const MyClass& other) {
if (this == &other) return *this; // 检查自赋值
// 释放当前对象的资源
// 拷贝other对象的数据到当前对象
return *this;
}
private:
int *data; // 假设有一个指针成员
};
```
<br><br>
## 移动赋值运算符
重载移动赋值运算符的注意点:
- **`noexcept`**:如之前所述,建议在移动赋值运算符后添加 `noexcept` 说明符
- **自赋值检查**
- **释放原资源**:在从 `other` 对象移动资源之前,确保释放当前对象的资源
- **资源置空**:
- 在移动资源后,确保将 `other` 对象的资源指针置为 `nullptr`,这样当 `other` 对象被销毁或再次被赋值时,它不会尝试释放已经被移走的资源。
- **返回`this`对象**:为支持链式赋值,通常需要返回"当前对象的引用"。
```cpp title:move_assignment_operator.cpp
class String {
private:
char *str;
int len;
public:
// 其他构造函数、析构函数、成员函数...
// 移动赋值运算符
String& operator=(String&& other) noexcept {
// 检查自赋值
if (this == &other) {
return *this;
}
// 释放当前对象的资源
delete[] str;
// 从 other 对象移动资源
str = other.str;
len = other.len;
// 将 other 对象的资源指针置为 nullptr,长度置为 0
other.str = nullptr;
other.len = 0;
return *this;
}
};
```
<br><br><br>
# 类的转换函数
> "**转换函数**"(Type conversion function,类型转换函数)
转换函数是用于将 "**==当前类类型==**" 转换为 "**==其它类型==**"的特殊成员函数[^9]。
转换函数的声明语法:
![[_attachment/02-开发笔记/01-cpp/类与对象/cpp-类的成员函数.assets/IMG-cpp-类的成员函数-3132A3B5DBE97DA88B8C71F4B1C0D83A.png|750]]
转换函数的定义示例:
```cpp
MyClass::operator type_name() const { // 通常带有"const"限定,也可不带
// 将当前类类型转换成其它类型. 返回其它类型的变量
return data;
}
```
- 转换函数 **==不声明返回类型==**,但要**返回所需类型**的值。
- 转换函数 **==不能带有任何参数==**。
- **可转为任何可作为 "函数返回类型" 的类型**,**例如指针或引用(包括函数指针)**,但**不能转化为 "数组或函数类型"**
> [!NOTE]
> - `non-explict` 的转换函数可用于**任何隐式、显式转换的场景**(包括直接初始化/拷贝初始化)
> - `explicit` 的转换函数只能用于 "**直接初始化**"与 "**显式类型转换**",以及 **表达式==被用作 "条件"== 时**:
> - **==到 `bool` 类型的 `explicit` 转换==**,任何用作 "条件" 的场景中,将会被隐式执行(因此通常均定义为 `explicit` ):
> - `if`,`while`,`do` 语句中的 "条件部分" ;
> - `for` 语句中的 "条件表达式" ;
> - 条件运算符 `?:` 中的 "条件表达式";
> - 作为逻辑运算符 `||` ,`&&`,`!` 的操作数时;
>
> ```cpp
> // 示例: 流对象作为"条件"时, 将隐式调用IO类型定义的`operator bool`.
> while (cin >> value) {
> ...
> }
> ```
> [!important]
> `non-explicit` 转换函数在 **"隐式类型转换" 的第二阶段("零个或一个转换构造函数" 或 "零个或一个用户定义转换函数")** 可被调用。
>
> 如果同时存在可用的**转换函数**和**转换构造函数**,则:
>
> - 在==**拷贝初始化**==和==**引用初始化**==中,转换函数与转换构造函数**均会被考虑**,基于重载解析(overload resolution)进行选取;
> - 在==**直接初始化**==中,只会考虑 "**==转换构造函数==**"。
>
> <br>
>
> > [!quote] Quote:[User-defined conversion function - cppreference.com](https://en.cppreference.com/w/cpp/language/cast_operator#Explanation)
> > User-defined conversion function is invoked in **the second stage of the implicit conversion**, which consists of **zero or one converting constructor** or **zero or one user-defined conversion function**.
> >
> > If **both conversion functions and converting constructors** can be used to **perform some user-defined conversion**,
> > - **the ==conversion functions and constructors== are both considered by overload resolution** in **==copy-initialization==** and **==reference-initialization==** contexts,
> > - but only the **==constructors==** are considered in **==direct-initialization==** contexts.
<br>
### 避免定义具有二义性的类型转换
避免产生 "**多重级别相同的转换路径**",编译器将报告 "**二义性错误**" [^10]。
包括两种情况:
- (1)避免在两个类之间定义 "**==相同的类型转换==**"
- 例如,A 类定义了**以 B 类对象为参数的转换构造函数**,同时 B 类定义了一个**转换目标是 A 类的类型转换运算符**,二者提供了相同的类型转换,将导致二义性。
- (2)避免为一个类定义多个 **=="转换源或转换目标" 均为算术类型==** 的转换。
- 例如,A 类具有到 `operator int` 和 `operator double` 两种算术类型的转换;或者,A 类具有以 `int` 或 `double` 为形参的两个构造函数。
示例一:
```cpp
struct B;
struct A {
A() = default;
A(const B&); // B=>A;
};
struct B {
operator A() cont; // B=>A;
};
A f(const A&);
B b;
A a = f(b); // 二义性错误: 应该调用`f(B::operator A())`还是`f(A::A(const B&))`?
```
示例二:
```cpp
struct A {
A(int = 0); // 存在int或double到A的转换
A(double);
operator int() const; // A 具有到int或double的转换
operator double() const;
};
void f2(long double);
A a;
// 二义性错误: 调用`f2(A::operator int())`还是`f2(A::operator double())`?
// 无论哪个版本都不能精确匹配long double,都还要执行到long double的标准转换;
f2(a);
// 二义性错误: 调用`A::A(int))` 还是 `A::A(double)`?
// 两个版本均不能精确匹配long类型, 因此都需要在long到其一类型的标准转换.
// 两种转换序列无优劣之分, 编译器无法区分
long lg;
A a2(lg);
```
%%
---
### 待梳理
对于**函数形参、函数返回类型、被实例化类的构造函数**所期望的 "**预期类型 A**" ,如果实际传入一个**其他类型 B 的对象**,则编译器将检查是否存在**从 B 到 A 的转换规则**,如果存在则触发 "**==类型转换==**"——将传入的**其他类型 B** 转为**预期类型 A** 。
这一类型转换的具体过程可能有两种:
- 如果 **B 类型** 存在一个到 **A 类型** 的 "**==转换函数==**",则调用该转换函数。
- 如果 **A 类型** 存在一个接受**单个 B 类型对象**的 "**==转换构造函数==**",则调用该构造函数。
当上述两种转换都"可用"时,通常编译器会**优先调用 B 类型的 "==转换函数==**",因为**转换函数直接提供了一个明确具体的转换路径**。
但是在某些情况下,如果编译器**无法确定使用哪种转换**则会报告**二义性错误**。
在实际编程中,为避免二义性和提高代码清晰度,建议显式指定使用哪种转换。
注意:
==返回类型预期 A,实际语句返回 B,==
==B 存在能转 A 的函数,A 存在接收 B 的构造函数,那么问题来了,到底会调用哪一个?==
对比示例:
```cpp
B b;
// 显式使用A的、以B类型对象为参数的构造函数
A a1(b);
// 显式使用B的转换函数, 将B转换为A类型对象后再通过"拷贝初始化"来创建a2.
A a2 = static_cast<A>(b);
```
- **其他类型转 "预期类类型"**
- 函数形参/返回类型为预期类类型,而**传入实参/实际返回值**是一个**可用于构造该类类型的其他类型**,则触发隐式类型转换,根据传入实参调用预期类类型的 "**==转换构造函数==**",创建**临时对象**。
- 拷贝初始化中,**被实例化类的构造函数**期望一个**预期类类型**,而等号右侧是一个**可用于构造该类类型的其他类型**,则触发隐式类型转换,调用其他类型转到该预期类类型的 '==**转换函数**==",构造出预期类类型的临时对象,再将其作为参数来**调用被实例化类的构造函数**。
> [!note]
> C++11 中引入的**显式类型转换运算符** `static_cast<type_name>` ,表明将当前类类型转换为其它类型,此时即会**调用该类的转换函数**。
三种情况,从**预期类 A** 与**其他类 B、C、D**
| | A 有接收该类对象"引用"的构造函数 | 该类有到 A 的转换函数 | A 有接收该类对象的构造函数 |
| ---- | ---- | ---- | ---- |
| **B** | ✔️ | ❌ | ❌ |
| **C** | ❌ | ✔️ | ❌ |
| **D** | ✔️ | ✔️ | ❌ |
| E | ❌ | ✔️ | ✔️ |
```cpp
class B;
struct A {
A() = default;
A(const B&) { // conversion constructor
cout << "Invoke A's conversion constructor `A(const B&)`" << '\n';
}
};
struct B {
B() = default;
operator A() { // conversion operator
cout << "Invoke A's conversion operator" << '\n';
return A();
}
}
struct C
int main() {
B b = A(); // 调用A的转换构造函数.
return 0;
}
```
类 B 中的 `operator A()` 在重载解析(overload resolution)下等价于 `A(B&)`,而 `operator A() const` 等价于 `A(const B&)`。
因此,
首先明确,**具有相同参数列表的重载函数**之间,无 `const` 修饰的休闲级更高。
1: 当转换序列中只有 **A 的以 B 为参的构造函数**、或者只有 **B 的 B=>A 转换函数**时,这个唯一存在的函数即会被调用。
2: 当转换序列中同时存在构造函数和转换函数时,则编译器根据优先级和调用形式决定
`non-const` 函数的优先级高于 `const` 函数。对于转换函数而言,在重载解析(overload resolution)下,类 B 中的 `operator A()` 等价于 `A(B&)`,而 `operator A() const` 等价于 `A(const B&)`,转换函数会以包含隐式对象参数的形式同 A 的构造函数进行优先级比较。
即 **( "A 的 `A(B&)` " ≈ "B 的 `operator A()`" ) > ( "A 的 `A(const B&)`" ≈ "B 的 `operator A() const` ")**
因此,在 `A obj_a = B();` 这一语句中
%%
<br><br>
# Buffer
## 闪念
> sudden idea
## 候选资料
> Read it later
# ♾️参考资料
# Footnotes
[^1]: 《Effective Modern C++》(Item 17)
[^2]: 《C++ Primer》P475
[^3]: 《C++ Primer》P450
[^4]: 《C++ Primer》P476
[^5]: 《Effective C++》Item5
[^6]: 《Effective C++》Item6
[^7]: [Converting constructor - cppreference.com](https://en.cppreference.com/w/cpp/language/converting_constructor)
[^8]: 《C++ Primer》P493
[^9]: 《C++ Primer》P514
[^10]: 《C++ Primer》P517-520