👉左值引用和右值引用👈
左值引用和右值引用
传统的 C++ 语法中就有引用的语法,而 C++11 中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。
什么是左值?什么是左值引用?
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址 + 可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时 const 修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名
int main() { // 以下的p、b、c、*p都是左值 int* p = new int(0); int b = 1; const int c = 2; // 以下几个是对上面左值的左值引用 int*& rp = p; int& rb = b; const int& rc = c; int& pvalue = *p; return 0; }
什么是右值?什么是右值引用?
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名。
int main() { double x = 1.1, y = 2.2; // 以下几个都是常见的右值 10; x + y; fmin(x, y); // 以下几个都是对右值的右值引用 int&& rr1 = 10; double&& rr2 = x + y; double&& rr3 = fmin(x, y); // 下面的语句都会编译报错,左操作数必须为左值 //10 = 1; //x + y = 1; //fmin(x, y) = 1; return 0; }
左值可以引用右值吗?右值可以引用左值吗?
// x既能接受左值,又能接受右值 template <class T> void Func(const T& x) { //... } int main() { // 左值引用可以引用右值吗?const的左值引用可以 double x = 1.1, y = 2.2; //double& rr1 = x + y; // 编译报错 const double& rr2 = x + y; // 可以 // 右值引用可以引用左值吗?不可以,可以引用move以后的左值 int a = 10; //int&& rr3 = a; // 编译报错 int&& rr5 = move(a); return 0; }
左值引用与右值引用总结:左值引用只能引用左值,不能引用右值。但是 const 左值引用既可引用左值,也可引用右值。右值引用只能引用右值,不能引用左值,但是右值引用可以 move 以后的左值。
需要注意的是:右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址。也就是说,不能取字面量 10 的地址,但是 rr1 引用后,可以对 rr1 取地址,也可以修改 rr1。如果不想 rr1 被修改,可以用 const int&& rr1 去引用。注:rr1 和 rr2 都是左值。
int main() { double x = 1.1, y = 2.2; int&& rr1 = 10; const double&& rr2 = x + y; rr1 = 20; //rr2 = 5.5; // 报错 cout << &rr1 << endl; cout << &rr2 << endl; return 0; }
右值引用使用场景和意义
左值引用解决的问题:
- 做参数:a. 减少拷贝,提高效率。b. 做输出型参数
- 做返回值:a. 减少拷贝,提高效率。b. 引用返回,可以修改返回对象(比如:
operator[]
)
左值引用既可以引用左值和又可以引用右值,那为什么C++11 还要提出右值引用呢?其实左值引用无法解决一些场景的问题,所以就提出了右值引用。
C++11 的右值引用的一个重要功能就是要解决上面的问题,但右值引用并不是直接作为返回值起作用的。
注:只有在一个表达式里,编译器才能够优化,上图的场景无法优化。对象已经存在,只能用to_string
生成的临时对象调用赋值运算符重载赋值给ret
。
那么在string
类里添加移动构造和移动赋值就能够解决上面的问题了。
// 移动构造 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; }
添加移动构造和移动赋值后,就不会存在深拷贝了。在bit::string中增加移动构造和移动赋值,移动构造和移动赋值本质是将参数右值的资源转移到指定的对象中,那就不需要深拷贝了。
注:右值有两类,第一类是纯右值,即内置类型右值;第二类是将亡值,即自定义类型右值。右值将亡值的资源可以转移到指定的对象,而左值不能。移动构造和移动赋值是延长了资源的生命周期。
完整代码
namespace Joy { 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); // 传统写法 _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; } // 移动构造 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; } 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; } const char* c_str() const { return _str; } private: char* _str; size_t _size; size_t _capacity; // 不包含最后做标识的\0 }; string to_string(int value) { bool flag = true; if (value < 0) { flag = false; value = 0 - value; } string str; while (value > 0) { int x = value % 10; value /= 10; str += ('0' + x); } if (flag == false) { str += '-'; } reverse(str.begin(), str.end()); return str; } }
STL 中的容器都是增加了移动构造和移动赋值的。
STL 容器的插入接口函数也增加了右值引用版本。
完美转发
模板中的 && 万能引用
void Fun(int& x) { cout << "左值引用" << endl; } void Fun(const int& x) { cout << "const 左值引用" << endl; } void Fun(int&& x) { cout << "右值引用" << endl; } void Fun(const int&& x) { cout << "const 右值引用" << endl; } // 万能引用:既能引用左值,也能引用右值 // 引用折叠 template<typename T> void PerfectForward(T&& t) { Fun(t); } int main() { PerfectForward(10); // 右值 int a; PerfectForward(a); // 左值 PerfectForward(std::move(a)); // 右值 const int b = 8; PerfectForward(b); // const 左值 PerfectForward(std::move(b)); // const 右值 return 0; }
可以看到,上面的引用通通被折叠成左值引用。其实这可以用上面的一个知识点来解释,当右值引用右值时,那么这个引用的属性也是左值,其有自己的地址。
std::forward 完美转发在传参的过程中保留对象原生类型属性。
void Fun(int& x) { cout << "左值引用" << endl; } void Fun(const int& x) { cout << "const 左值引用" << endl; } void Fun(int&& x) { cout << "右值引用" << endl; } void Fun(const int&& x) { cout << "const 右值引用" << endl; } // 万能引用:既能引用左值,也能引用右值 template<typename T> void PerfectForward(T&& t) { Fun(std::forward<T>(t)); } int main() { PerfectForward(10); // 右值 int a; PerfectForward(a); // 左值 PerfectForward(std::move(a)); // 右值 const int b = 8; PerfectForward(b); // const 左值 PerfectForward(std::move(b)); // const 右值 return 0; }
注:如果想要一直保持对象的元素类型,就要一直完美转发。注:只有模板参数采用万能引用,确定的类型没有万能引用。