目录
1、准备条件
下面的测试案例需用用到移动构造和移动赋值的测试代码,我们给出上篇博文封装过移动构造和移动赋值的简化版string类放到下面,以方便后续的测试代码中进行调用观察现象:
namespace cpp { class string { public: // 构造函数 string(const char* str = "") :_size(strlen(str)) , _capacity(_size) { //cout << "string(char* str)" << endl; _str = new char[_capacity + 1]; strcpy(_str, str); } // 交换两个对象的数据 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& operator=(const string& s) { cout << "string& operator=(string s) -- 深拷贝" << endl; string tmp(s); swap(tmp); return *this; } // 移动构造 string(string&& s) :_str(nullptr) , _size(0) , _capacity(0) { cout << "string(string&& s) -- 移动构造" << endl; swap(s); } // 移动赋值 string& operator=(string&& s) { cout << "string& operator=(string&& s) -- 移动赋值" << endl; swap(s); return *this; } // 析构函数 ~string() { delete[] _str; _str = nullptr; } private: char* _str; size_t _size; size_t _capacity; // 不包含最后做标识的\0 }; }
2、默认成员函数
原来C++类中(C++11之前),有6个默认成员函数:
- 构造函数
- 析构函数
- 拷贝构造函数
- 拷贝赋值重载
- 取地址重载
- const 取地址重载
最重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。C++11 新增了两个:移动构造函数和移动赋值运算符重载。针对移动构造和移动赋值,编译器也会默认生成,不过生成的条件极其苛刻,下面展开来讨论:
①、移动构造:
- 如果你没有自己实现移动构造函数,且均没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
②、移动赋值:
- 如果你没有自己实现移动赋值重载函数,且均没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
③、总结:
- 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
④、示例:
- 对于如下的Person类,我们不需要写析构、拷贝构造、赋值重载,因为Person的成员变量_name是自定义类型,会自动去调用string类的拷贝构造、析构、赋值重载完成深拷贝。而内置类型_age完成值拷贝即可。
// 以下代码在vs2013中不能体现,在vs2019及以上的编译器下才能演示体现上面的特性。 class Person { public: Person(const char* name = "", int age = 0) :_name(name) , _age(age) {} /*Person(const Person& p) :_name(p._name) , _age(p._age) {}*/ /*Person& operator=(const Person& p) { if (this != &p) { _name = p._name; _age = p._age; } return *this; }*/ /*~Person() {}*/ private: cpp::string _name; int _age; }; int main() { Person s1; Person s2 = s1;//拷贝构造 Person s3 = std::move(s1);//移动构造 Person s4; s4 = std::move(s2);//移动赋值 return 0; }
因为我们都没写拷贝构造、析构、赋值,所以编译器会默认生成移动构造和移动赋值运算符重载。针对main函数的测试用例,很明显,s2 = s1是拷贝构造,下面的两个分别调用移动构造和移动赋值:
但凡我把Person类中的任何一个拷贝构造或析构或赋值放出来,结果都是去调用string类的拷贝构造函数去完成深拷贝:
3、类成员变量初始化
默认生成的构造函数,对于自定义类型会自动调用它的构造函数进行初始化,对于内置类型并不会进行处理,于是C++11允许在类定义时给成员变量初始缺省值,默认生成构造函数会使用这些缺省值初始化,这个我们在类和对象默认就讲了,这里再简要提下。
class Person { public: //…… private: //C++11允许非静态成员变量在声明时进行初始化赋值 cpp::string _name = "王五"; int _age = 19; static int _num;//静态成员变量不能给缺省值 };
4、强制生成默认函数的关键字default
C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定生成移动构造或移动赋值。
- 示例:如下我们实现了拷贝构造,所以编译器就不会生成移动构造和移动赋值了,会去调用自定义类型string类的拷贝构造函数完成深拷贝:
class Person { public: //构造函数 Person(const char* name = "", int age = 0) :_name(name) , _age(age) {} //拷贝构造 Person(const Person& p) :_name(p._name) , _age(p._age) {} private: cpp::string _name; int _age; }; int main() { Person s1; Person s2 = s1; Person s3 = std::move(s1); Person s4; s4 = std::move(s2); return 0; }
为了让编译器生成移动构造和移动赋值,我们可以使用default关键字显示指定生成移动构造或移动赋值:
class Person { public: //构造函数 Person(const char* name = "", int age = 0) :_name(name) , _age(age) {} //拷贝构造 Person(const Person& p) :_name(p._name) , _age(p._age) {} //强制生成移动构造 Person(Person&& pp) = default; //强制生成移动赋值 Person& operator=(Person&& pp) = default; private: cpp::string _name; int _age; };
5、禁止生成默认函数的关键字delete
在C++中,如果想禁止生成默认成员函数,我们有如下两种方式:
- 在C++98中,是该函数设置成private,并且只是声明不定义,这样只要其他人想要调用就会报错。
- 在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
示例:我不想让一个类被拷贝,那么在拷贝构造声明的后面加上=delete即可:
6、继承和多态中的final与override关键字
- 这个我们在继承和多态章节已经进行了详细讲解,这里再强调下:
1、final:修饰虚函数,表示该虚函数不能再被重写,修饰类表示不能被继承。
- 这里我父类的虚函数Drive不想被其它人重写,在其后面加上final即可,此时子类就无法对Drive进行重写了,如下:
final修饰一个类,让其不能被继承,如下:
2、override:
- override的作用是检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。