(一)默认成员函数
原来C++类中,有6个默认成员函数:
- 1. 构造函数
- 2. 析构函数
- 3. 拷贝构造函数
- 4. 拷贝赋值重载
- 5. 取地址重载
- 6. const 取地址重载
最后重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。
💨 C++11 新增了两个:移动构造函数和移动赋值运算符重载
1、 移动构造函数
移动构造函数是C++中的特殊成员函数之一,用于以移动语义的方式构造新对象。它是在C++11中引入的,旨在提高对象的性能和效率。
移动构造函数通常用于在不进行资源拷贝的情况下将临时对象或者右值引用的对象的内容转移到新创建的对象中。通过移动构造函数,可以避免不必要的拷贝操作,提高代码的性能。
2、代码辅助理解
接下来我们通过代码来尝试着学习相关的知识:
- 首先,我先给出手动实现的string类,代码如下:
namespace zp { class string { public: typedef char* iterator; iterator begin() { return _str; } iterator end() { return _str + _size; } string(const char* str = "") :_size(strlen(str)) , _capacity(_size) { cout << "string(char* str)" << endl; _str = new char[_capacity + 1]; strcpy(_str, str); } // s1.swap(s2) void swap(string& s) { ::swap(_str, s._str); ::swap(_size, s._size); ::swap(_capacity, s._capacity); } // 拷贝构造 string(const string& s) :_str(nullptr) { cout << "string(const string& s) -- 深拷贝" << endl; string tmp(s._str); swap(tmp); } // 移动构造 string(string&& s) :_str(nullptr) { cout << "string(string&& s) -- 移动拷贝" << endl; swap(s); } // 赋值重载 string& operator=(const string& s) { cout << "string& operator=(string s) -- 深拷贝" << endl; string tmp(s); swap(tmp); return *this; } // s1 = 将亡值 string& operator=(string&& s) { cout << "string& operator=(string&& s) -- 移动赋值" << endl; swap(s); return *this; } ~string() { //cout << "~string()" << endl; delete[] _str; _str = nullptr; } char& operator[](size_t pos) { assert(pos < _size); return _str[pos]; } void reserve(size_t n) { if (n > _capacity) { char* tmp = new char[n + 1]; strcpy(tmp, _str); delete[] _str; _str = tmp; _capacity = n; } } void push_back(char ch) { if (_size >= _capacity) { size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2; reserve(newcapacity); } _str[_size] = ch; ++_size; _str[_size] = '\0'; } //string operator+=(char ch) string& operator+=(char ch) { push_back(ch); return *this; } string operator+(char ch) { string tmp(*this); tmp += ch; return tmp; } const char* c_str() const { return _str; } private: char* _str; size_t _size; size_t _capacity; // 不包含最后做标识的\0 }; //const zp::string& to_string(int value) zp::string to_string(int value) { bool flag = true; if (value < 0) { flag = false; value = 0 - value; } zp::string str; while (value > 0) { int x = value % 10; value /= 10; str += ('0' + x); } if (flag == false) { str += '-'; } std::reverse(str.begin(), str.end()); return str; } }
接下来,有这样的一段代码,我们对它进行运行分析:
接下来,我们给出相关示例,把代码运行起来看最终我们的样例是调用的什么:
【解释说明】
- 如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。
此时,我们把其中的析构函数进行相关的实现,在看最终的打印结果是什么:
【解释说明】
- 使用 move 操作可以显式地将 s1转换为右值引用,并尝试将 s1 通过移动语义移动到 s3 中;
- 此时,我们可以发现,当我们手动的实现了一个析构函数之后,编译器就会对识别进行深拷贝操作;
【小结】
针对移动构造函数有一些需要注意的点如下:
- 如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造;
- 默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造
这里解释一下为什么生成默认移动构造条件这么苛刻的问题:
- 编译器之所以把这个条件设计这么苛刻,因为它认为说你是你自己要实现的拷构构造和拷贝赋值还是析构,按理来说你这个类呢,就是一个深拷贝的类,那你是一个深拷贝的类呢,那这个时候移动赋值,它不知道咋处理比较好;
- 就比如说这有个指针,那这个指针我要把你的资源转移,就是你可以认为它自己把控不住,它不知道该咋编译做资源移动。其次这个指针指向的资源就一定要移动吗?我们认为这是不一定的,要看我们实际当中的需求是什么,它自己把控不住,这个时候他就不再给你自动生成了;
- 那什么时候他觉得他可以把控住呢?就是像刚才这样的,你没有实现析构,你不是深拷贝的类,它就可以把控住。
3、移动赋值运算符重载
移动赋值运算符重载的原理跟上述移动构造函数一样的。大家只需记住一个,另一个类似的就可以记住!!!
(二)default关键字
在C++11之前,如果在类的定义中没有显式声明默认构造函数、复制构造函数、移动构造函数、复制赋值运算符或移动赋值运算符时,编译器会自动生成这些函数的默认版本。然而,在C++11及以后的标准中,如果我们显式地定义了一个带有参数的构造函数、复制构造函数、移动构造函数、复制赋值运算符或移动赋值运算符,编译器将不再自动生成默认版本。
为了强制生成默认版本的函数,我们可以使用关键字 default。在类的定义内部,用 = default形式指定函数。这将告诉编译器生成该函数的默认版本。
- 代码展示:
(三)delete关键字
当我们在类中手动定义了自定义构造函数、复制构造函数、移动构造函数、赋值运算符或移动赋值运算符时,编译器就不会再自动生成默认版本的这些函数。然而,在某些情况下,我们可能希望保留编译器自动生成的默认版本。
使用 delete 关键字可以告诉编译器生成该函数的默认版本,即恢复被手动定义函数覆盖的默认行为。
- 代码展示:
(四)委托构造函数
委托构造函数是C++11引入的特性之一,它允许一个构造函数调用同一类的其他构造函数来完成对象的初始化。通过委托构造函数,可以减少代码的冗余、提高可维护性,并且确保初始化逻辑的一致性。
以下是委托构造函数的简要说明和示例:
1、优势
- 委托构造函数的语法:
委托构造函数使用特殊的语法来调用同一类的其他构造函数。它在成员初始化列表中使用冒号(:)后面的成员初始化器列表来调用其他构造函数。
class Test { public: Test(int x, int y) : // 构造函数1,委托给构造函数2 Test(x, y, 0) { // 委托构造函数 } Test(int x, int y, int z) : // 构造函数2,实际完成对象初始化的构造函数 { // 具体的初始化逻辑 } };
【解释说明】
- 这样,当我们使用委托构造函数创建对象时,只需调用适合的构造函数,并由该构造函数负责完成所有初始化工作;
- 这样做可以避免在多个构造函数中复制相同的初始化代码,提高了代码的可维护性。
- 委托构造函数的特点:
- 委托构造函数的声明和定义位于同一类中,并且在其他构造函数的前面。
- 委托构造函数不能有初始值列表,因为它的作用是将初始化任务委托给其他构造函数。
- 委托构造函数可以有自己的成员初始化列表,用于初始化委托所使用的其他构造函数中未初始化的成员变量。
2、缺点
委托构造函数在使用时存在一种潜在的问题,称为"委托环"(delegation cycle)。委托环是指构造函数之间形成了循环的委托调用关系,导致无限递归或编译错误的情况。
下面是一个示例,展示了如何在不小心的情况下创建委托环的代码:
class Test { public: Test(int x) { // 执行一些初始化操作 } Test() : Test(0) { // 委托构造函数,调用了另一个构造函数 // 其他逻辑 } };
【解释说明】
- 在上面的代码中,Test 类有一个带有参数的构造函数和一个不带参数的构造函数,而不带参数的构造函数使用委托构造函数调用了带有参数的构造函数。这看起来没有问题,但实际上却形成了委托环;
- 当创建一个不带参数的 Test 对象时,会调用不带参数的构造函数。然后,由于委托构造函数的存在,它又会调用带有参数的构造函数。然后,带有参数的构造函数又会调用不带参数的构造函数。这样就形成了循环,导致无限递归。
委托构造函数自身不应该包含其他的初始化语句,否则会导致重复初始化的问题
class Test { public: Test(int value) : Test(value, 0.0) { // 错误!委托构造函数体中存在其他初始化语句 tmp_ = 42; } Test(int value, double rate) : value_(value) , rate_(rate) { // 构造函数体... } private: int value_; double rate_; int tmp_; };
【解释说明】
- 在上述示例中,委托构造函数的体内存在对 tmp_ 的初始化,这将导致该变量在构造过程中被初始化两次,可能会产生不可预料的结果。
- 正确的做法是,在委托构造函数内部只进行调用,不要插入其他的初始化语句。
总结
以上便是关于本期 c++11新的类功能全部知识。感谢大家的观看与支持!!!