从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值(上):https://developer.aliyun.com/article/1522384
3.3 左值引用与右值引用比较
思考:左值引用可以引用右值吗?
要知道,右值引用是C++11才出来的,右值传参给函数还是右值,那我们以前写的函数都用不了右值传参了?
template<class T> void Func(const T& x) {}
这里去掉const肯定是不能传参的,为了给右值传参(当然还有其它原因),所以const的左值引用可以引用右值。总结:普通的左值引用不可以引用右值,const的左值引用可以引用右值。
思考:右值引用可以引用左值吗?
右值引用不可以引用普通的左值,可以引用move以后的左值:(move这个语法先记住)左值经过move以后就变成了右值,如:
int main() { // 左值引用可以引用右值吗? const的左值引用可以 double x = 1.1, y = 2.2; //double& r1 = x + y; const double& r1 = x + y; // 右值引用可以引用左值吗?可以引用move以后的左值 int b = 7; //int&& rr5 = b; int&& rr5 = move(b); return 0; }
成功编译:
VS的提示已经很智能了:
3.4 右值引用的使用场景
namespace rtx { class string { public: string(const char* str = "") :_size(strlen(str)) , _capacity(_size) { _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(s); _str = new char[s._capacity + 1]; strcpy(_str, s._str); _size = s._size; _capacity = s._capacity; } string& operator=(const string& s) // 拷贝赋值 { cout << "string& operator=(string s) -- 拷贝赋值(深拷贝)" << endl; string tmp(s); swap(tmp); return *this; } protected: char* _str; size_t _size; size_t _capacity; }; }
先自己实现一个string,只有拷贝构造函数,赋值运算符重载函数,析构函数,以及一个普通的构造函数。无论是拷贝构造还是赋值运算符重载,都会进行深拷贝,采用现代写法来实现:
namespace rtx { class string { public: string(const char* str = "") :_size(strlen(str)) , _capacity(_size) { _str = new char[_capacity + 1]; strcpy(_str, str); } const char* c_str() const { return _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& operator=(const string& s) // 拷贝赋值 { cout << "string& operator=(string s) -- 拷贝赋值(深拷贝)" << endl; string tmp(s); swap(tmp); return *this; } ~string() { delete[] _str; _str = nullptr; } protected: char* _str; size_t _size; size_t _capacity; }; }
左值引用的场景:
使用普通传值调用,存在一次深拷贝:
void Func(rtx::string s) {} int main() { rtx::string s("hello world"); Func(s); return 0; }
使用传拷贝引用时,不存在深拷贝,Func函数直接使用main函数中的s1对象:
void Func(rtx::string& s) {} int main() { rtx::string s("hello world"); Func(s); return 0; }
函数返回参数和上面一样,传引用返回有时确实能提高效率。
3.4.1 左值引用的功能和短板
左值引用的功能:
做参数。
1. 减少拷贝,提高效率。
2. 做输出型参数。
做返回值。
1. 减少拷贝,提高效率。
2. 引用返回,可以修改返回对象(比如: operator[ ])。
但是左值引用做返回值只解决了70%的问题,在类似 to_string 函数中:
- 传值返回时,存在一次深拷贝。
- rtx::string to_string(int value)
要知道深拷贝的代价是比较大的,深拷贝次数减少可以很大程度上提高代码的效率。
- 传左值引用返回时,不存在深拷贝。(可以吗?)
- rtx::string& to_string(int value)
但是敢传引用返回吗?我们把int value 转换成string,此时的 string 是一个形参。出了函数就销毁了。外面拿到的就是被销毁了的栈帧。
所以左值引用存在的短板:
前面我们在调用 to_string 函数的时候,我们把int value 转换成string,此时的 string 是一个形参。所以只能传值返回,此时mian函数中拿到 to_string 中的 string 对象要进行两次深拷贝。
第一次深拷贝,to_string函数返回时,会将string对象放在一个临时变量中,此时发生的深拷贝。函数返回时,如果是内置类型等几个字节的变量,会将函数中的临时变量放在寄存器中返回,如果是自定义类型所占空间比较大,就会放在临时变量中压栈到上一级栈帧中。
第二次深拷贝,main函数中,ret接收函数返回了的string对象时会再发生一次深拷贝。
但是编译器会进行优化,将两次深拷贝优化成一次。虽然只有一次,但有些情况代价还是很大的。
C++98是如何解决上面的问题?那就是输出型参数:rtx::string to_string(int value)变成rtx::void to_string(int value,string& s)。但是这样不太符合使用习惯。
有没有办法让它符合使用习惯,并且一次深拷贝都没有?那就要用到下面的C++11新增的移动构造和移动赋值了
3.4.2 移动构造
此时用右值引用就可以解决这个问题。
右值引用的价值之一:补齐临时对象不能传引用返回这个短板。
前面的深拷贝是拷贝构造产生的:string(const string& s) // 拷贝构造(形参是左值引用)
演示在string类中增加一个移动构造函数:
前面提到过:内置类型的右值被称为纯右值。
自定义类型的右值被称为将亡值。(这里的传右值就是将亡值)
基于拷贝构造:无论是左值还是右值都老老实实地开空间:
string(const string& s) // 拷贝构造 :_str(nullptr) , _size(0) , _capacity(0) { cout << "string(const string& s) -- 拷贝构造(深拷贝)" << endl; string tmp(s._str); swap(tmp); }
左值因为还要使用,肯定要开空间的,这里的右值是将亡值,没用了,所以也不用开空间了,因为不用开空间了,所以深拷贝也没了,而是资源转移(直接swap):
string(string&& s) // 移动构造 :_str(nullptr) , _size(0) , _capacity(0) { cout << "string(string&& s) -- 移动构造(资源转移)" << endl; swap(s); }
- 移动构造的形参是右值引用。
从to_string中返回的string对象是一个临时变量,具有常性,也就是我们所说的右值。
- 用右值来构造string对象时,会自定匹配移动构造函数。(以前没有移动构造时,右值传参会走拷贝构造,因为const 的左值引用可以接收右值,但是这不是最优方案,现在写了移动构造,右值传参就会走移动构造)
3.4.3 移动赋值
拷贝赋值移动赋值和拷贝构造移动构造类似:
string& operator=(const string& s) // 拷贝赋值 { cout << "string& operator=(string s) -- 拷贝赋值(深拷贝)" << endl; string tmp(s); swap(tmp); return *this; } string& operator=(string&& s) // 移动赋值 { cout << "string& operator=(string s) -- 移动赋值(资源移动)" << endl; swap(s); return *this; }
总结:右值引用和左值引用减少拷贝的原理不太一样。
- 左值引用是别名,直接在原本的对象上起作用。
- 右值引用是间接起作用,通过右值引用识别到右值,然后在移动构造和移动赋值中进行资源转移。
使用移动构造和移动赋值时,被转移资源的对象必须是个将亡值(像to_string的使用一样),因为会被销毁。C++11的STL标准库中也提供了移动构造和移动赋值函数。
3.4.4 插入右值时减少深拷贝
C++11在STL库容器中的所有插入接口都提供了右值版本,push_back,insert等。
在我们写的string恢复这两个接口:
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'; }
然后分别像库里的 list 插入左值和右值
1.int main() { list<rtx::string> lt; rtx::string s1("hello"); // 左值 lt.push_back(s1); // 插入左值 cout << "----------------------------------" << endl; lt.push_back(rtx::string("world")); // 插入右值 //lt.push_back("world"); return 0; }
如果没有移动构造那么下面的也是深拷贝了。
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值(下):https://developer.aliyun.com/article/1522395