什么是左值,什么是右值?
左值就是程序能获得其地址的表示数据的表达式,包括变量,const常量,解除引用的指针。
相反,右值就是不能应用地址运算符&的表示数据的表达式,包括字面常量,x+y,非引用的返回值。
什么是左值引用,什么是右值引用?
【C++ 11 右值引用和移动语义的实现】我们常说的C++的引用,大部分时候指的就是左值引用,符号是&,
比如 int a=10;
int &b=a;
其中,b就是a的引用,可以理解为别名。
右值引用符号是&&,
比如 int &&a = 10;
其中,a就是10的右值引用。&10是非法的,但是&a却是合法的。
移动语义和右值引用的关系
移动语义对降低C++构造和析构的开销有重要的意义,减少了传值、返回值过程中的资源拷贝。
C++移动语义的实现,正是基于右值引用。
传值和返回值的代码开销在哪里?
请看下面这段代码;
#include
using std::cin;
using std::cout;
using std::endl;
struct Person {
Person(const char* p) {
cout << "constructor" << endl;
}
Person(const Person& p) {
cout << "copy constructor" << endl;
}
const Person& operator=(const Person& p) {
cout << "operator=" << endl;
return *this;
}
~Person() {
cout << "destructor" << endl;
}
};
Person getAlice() {
Person p("alice");
// 对象创建。调用构造函数,一次 new 操作
return p;
// 返回值创建。调用拷贝构造函数,一次 new 操作
// p 析构。一次 delete 操作
}int main() {
cout << "______________________" << endl;
Person a = getAlice();
// 对象创建。调用拷贝构造函数,一次 new 操作
// 返回值析构,一次 delete 操作
// 当前步骤合共 3次构造,2次析构
cout << "______________________" << endl;
a = getAlice();
// 对象创建。调用拷贝构造函数,一次 new 操作
// 返回值析构,一次 delete 操作
// 当前步骤合共 3次构造,2次析构
cout << "______________________" << endl;
return 0;
// a 析构。一次 delete 操作
}
在不考虑NVRO(返回值优化)的情况下,上面这段代码的预期过程如注释,总共6次构造,5次析构。
当然了,编译器会进行NVRO(返回值优化),减少构造和析构次数。
不同编译器的NVRO结果是不一样的:
在Visual Studio 2015上面编译运行结果是:
文章图片
Person a = getAlice(),这一步,getAlice里面p的析构和返回值的构造被优化掉了,相当于a直接用了getAlice()的对象;
a=getAlice(),这一步,没有NVRO优化。
g++(8.2.0)优化程度比VS高。
文章图片
Person a = getAlice(),这一步,getAlice() 里面p的析构,返回值的构造和析构,a的拷贝构造都被优化掉了;
a=getAlice(),这一步,NVRO优化程度比赋初值操作的低,
getAlice() 里面p的析构和返回值的构造被优化掉了,相当于a直接用了getAlice里面的对象;
上面的代码还能优化吗?
可以。
通过移动语义,可以把拷贝构造函数改写成移动构造函数;或者就是另外写一个移动构造函数,实现重载。参考[4]
使用std::move相当于显式使用移动语义。std::move()实际上是static_cast
用右值引用实现移动语义,从而优化拷贝构造函数
参见以下代码和注释
// 基于左值引用的拷贝构造函数
//(参数p设置const属性,不允许直接取用参数p的指针成员,这是为了拷贝构造函数既能接受左值参数,也能接受右值参数)
//(不设置const属性也行,但是就不能用右值(getAlice的返回值)进行拷贝构造得到新的对象了。)
const Person& operator=(const Person& p) {
cout << "operator=" << endl;
delete[] name;
int len = strlen(p.name) + 1;
name = new char[len];
memcpy(name, p.name, len);
//左值引用的拷贝构造,会有一次申请内存和数据拷贝
return *this;
}
// 基于右值引用的拷贝构造函数
//(不需要const了,那么就可以直接取用参数p的指针成员,且可以在取用后将p的指针成员置为nullptr,这样该块内存就不会被析构了)
const Person& operator=(Person&& p) {
cout << "operator=" << endl;
delete[] name;
name = p.name;
//直接取用
p.name = nullptr;
//置空使得系统无法将该块内存析构掉
//相对比左值引用,右值引用的拷贝构造可以实现更加高效:少了一次内存申请和拷贝。
return *this;
}
【参考】
[1]《C++ Primer Plus》,18.1.9,右值引用一节
[2] https://harttle.land/2015/10/11/cpp11-rvalue.html 这篇文章讲的比较易于理解
[3] 如何评价 C++11 的右值引用(Rvalue reference)特性? - Tinro的回答 - 知乎 https://www.zhihu.com/question/22111546/answer/30801982
[4] https://www.cnblogs.com/dongdongweiwu/p/4743661.html