%% # 纲要 > 主干纲要、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