类的新功能
默认成员函数
在C++98中 类的默认成员函数一般可以认为有六个
这六个默认成员函数分别是
- 构造函数
- 析构函数
- 拷贝构造函数
- 拷贝赋值函数
- 取地址运算符重载
- const取地址运算符重载
之所以称之为默认成员函数意思就是即使我们不写这六个成员函数 他们也会自己生成
在实际的写代码过程中前面四个默认成员函数是比较重要的 我们需要自己实现一遍 而后面两个让系统自己默认生成就好
在C++11中为了提高效率 提出了右值引用的概念 因此默认成员函数也增加了两个 变成了八个
增加了两个默认成员函数分别是
- 移动构造函数
- 移动赋值重载函数
但是他们的默认生成条件有些不同 他们两个的默认生成条件分别是
移动构造函数: 没有自己实现移动构造函数 并且没有自己实现析构函数 拷贝构造函数和拷贝赋值函数
移动赋值重载函数: 没有自己实现移动赋值重载函数 并且没有自己实现析构函数 拷贝构造函数和拷贝赋值函数
这里还有一点需要特别注意的是: 如果我们自己生成了移动构造和移动赋值重载函数 那么就算我们没有生成拷贝构造和拷贝赋值 系统也不会默认生成
也就是说这两对函数之间是有相互作用的 不能单独拆开来看
默认的移动构造和移动赋值会做什么呢
- 默认移动构造 对于内置类型 它会完成浅拷贝 对于自定义类型 如果它实现了移动构造就会调用移动构造 否则就调用拷贝构造
- 默认移动赋值 对于内置类型 它会完成浅拷贝 对于自定义类型 如果它实现了移动赋值就会调用移动赋值 否则就调用拷贝赋值
验证默认移动构造和移动赋值
首先我们先创造出一个简单的string类 直接复用上一篇博客的代码就好
namespace shy { class string { public: typedef char* iterator; // begin迭代器返回第一个元素 iterator begin() { return _str; } // end迭代器返回最后一个元素后一个元素的位置 iterator end() { return _str + _size; } // 构造函数 string(const char* str = "") { _size = strlen(str); // 初始化设置字符串大小 _capacity = _size; _str = new char[_capacity + 1]; // 这里要多开一个空间来存储/0 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) , _size(0) , _capacity(0) { cout << "string(const string& s)" << endl; string tmp(s._str); // 调用构造函数 swap(tmp); // 交换私有成员变量 } // 移动构造 string(string&& s) :_str(nullptr) , _size(0) , _capacity(0) { cout << "string(string&& s)" << endl; swap(s); } // 赋值运算符重载(现代写法) string& operator=(const string& s) { cout << "string& operator=(const string& s)" << endl; string tmp(s); //拷贝构造出一个临时变量 swap(tmp);// 交换这个两个对象 return *this; // 返回左值 } // 移动赋值 string& operator= (string && s) { cout << "string& operatpr=(string&& s)" << endl; swap(s); return *this; } // 析构函数 ~string() { delete[] _str; // 释放str的空间 _str = nullptr; _size = 0; _capacity = 0; } //[]运算符重载 char& operator[](size_t i) { assert(i < _size); return _str[i]; // 返回左值引用 } // 改变容量 大小不变 void reserve(size_t n) { if (n > _capacity) { char* tmp = new char[n + 1]; strncpy(tmp, _str, _size + 1); // 这里不使用strcpy的原因是字符串中哟i可能出现/0 delete[] _str; _str = tmp; _capacity = n; // 容量改变 } } // 尾插字符 void push_back(char ch) { // 首先判断容量是否足够 if (_size >= _capacity) { reserve(_capacity == 0 ? 4 : 2 * _capacity); } // 尾插到最后 最后加上\0 _str[_size] = ch; _str[_size + 1] = '\0'; _size++; } //+=运算符重载 string& operator+=(char ch) { push_back(ch); return *this; } // 返回c类型的字符串 const char* c_str() const //这里加const是修饰this指针 让它的权限变成只读 为了防止后面的只读对象调用这个函数 { return _str; } private: char* _str; size_t _size; size_t _capacity; }; }
之后我们再写出一个简单的person类 将我们写的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) {} //拷贝赋值函数 Person& operator=(const Person& p) { if (this != &p) { _name = p._name; _age = p._age; } return *this; } //析构函数 ~Person() {} private: shy::string _name; //姓名 int _age; //年龄 };
我们的person类中没有实现移动构造和移动赋值 但是实现了拷贝构造 析构和赋拷贝赋值
所以说移动构造和移动赋值并不会默认生成
那么我们下面就会有一段代码来证明上面的话
Person p1("zhangsan",18); Person p2 = ::move(p1);
我们这里使用了右值去构造p2 假设p2的移动构造存在的话 那么因为string是我们的自定义类型
所以说它就回去调用string的移动构造
那么我们来看看实际上允许的结果是什么
实际上它调用的是string类的拷贝构造
从这个试验就可以证明并没有默认生成移动构造函数
那么如果要生成移动构造和移动赋值我们就必须将原person代码中的 析构函数 拷贝构造 拷贝赋值全部注释掉
那么注释掉之后我们再来看看效果是什么样子的
接下来我们用另外一段代码试验一下移动赋值
Person p1("zhangsan",18); Person p2; p2 = ::move(p1);
我们可以发现 移动构造和移动赋值的生成条件符合我们之前的学习
这里还有一点需要注意的 有些比较古老的编译器可能不支持C++11 所以在这些编译器上无法进行我们上面的试验
类成员变量的初始化
C++98之前的类成员默认构造初始化规则特别奇怪 是这样子的
- 对于自定义类型来说会调用他们的构造函数进行初始化
- 对于内置类型不进行初始化
而在C++11中 对于内置类型打了个补丁
- 对于自定义类型来说会调用他们的构造函数进行初始化
- 对于内置类型我们可以使用缺省值来进行初始化
以我们的person类来说
shy::string _name; //姓名 int _age = 18; //年龄
如果我们使用默认的构造函数 那么他们的年龄就会被默认初始化为18