文章目录
一、值类别
C++11之后把表达式按值类别分为了3种,分别是左值(left value,lvalue)、将亡值(expiring value,xvalue)和纯右值(pure rvalue,pralue)。
其中把左值和将亡值统称为泛左值(generalized lvalue,glvalue),把纯右值和将亡值统称为右值(right value,rvalue)。具体关系如下图所示。
1、C++11之前常规左值与右值的辨析
a.是否能进行取地址操作
取地址操作就是表达式前加&,所以判断左值右值最简单的方法就是在编译器上进行测试,观察加上取地址符是否报错。
可以进行取地址操作的就是左值,而不能取地址的就是右值。当然,不能取地址不代表这个表达式就没有地址,只是系统不允许进行取地址操作。
b.列举一些常规表达式
除了字符串以外,其他字面量都是右值。所以右值可以包括:临时变量、匿名变量、字面值常量(字符串除外)等
左值表示一个占据内存中可识别位置的一个对象。等号左边的只能是左值,右边的可以是左值也可以是右值。
2、将亡值
将亡值就是即将被编译器销毁的值
//str是将亡值,过了{}作用域,会被编译器销毁
//在{}作用域内,str可以取地址,是左值;过了{}作用域,str不能取地址,是右值。
std::string getString(){
std::string str("hello");
return std::move(str);
}
3、纯右值
字面值、算数表达式、lambda表达式、栈上的匿名对象、后置自增和自减表达式、函数返回值都是纯右值。
//1、字面值
true; 7; nullptr;
//2、算数表达式
a>b; a+b; a&&b; 1+3;
//3、lambda表达式
[=](int a)->int{return ++a;}
//4、栈上的匿名对象
Object();
//5、后置自增和自减表达式
i--;i++;//先将i存到临时变量,然后对i进行加1,最后返回的是临时变量,很快就销毁了,所以是右值
二、引用类型
左值引用是只能被左值初始化的变量名;右值引用是只能被右值初始化的变量名,即表达式等号右边的值需要是右值,可以使用std::move函数强制把左值转换为右值。。无论左值引用还是右值引用都必须且只能在声明时进行初始化。Type &表示左值引用,Type &&表示右值引用。
int value = 7;
int && rvalue = 7; //正确
int & lvalue = value;//正确
int &c = 10; // 错误,10无法取地址,无法进行引用
const int &d = 10; // 正确,因为是常引用,引用常量数字,这个常量数字会存储在内存中,可以取地址
int a = 4;
int &&b = a; // 错误, a是左值
int &&c = std::move(a); // 正确
1、左值引用
左值引用的出现让C++编码在一定程度上摆脱了危险的指针。
2、右值引用( C++11 标准引入)
右值引用主要是为了解决两个问题:
1 实现 move 语义(移动构造函数,移动赋值函数 )
2 支持函数参数的完美转发
通俗来讲,右值引用其本质就是减少内存开销、优化内存使用的一种方法,右值引用就是将那些产生的临时的变量或对象偷过来作为长生命周期的对象存在,避免了不必要的多次的在内存中创建销毁
三、类型转换
过去常用的类型转化有dynamic_cast,static_cast,reinterpret_cast,const_cast等等,引入左右值引用后,就多了两个类型转换的方式,移动语义std::move和完美转发std::forward。
1、移动语义std::move
深拷贝vs浅拷贝
深拷贝就是再拷贝对象时,如果被拷贝对象内部还有指针引用指向其它资源,自己需要重新开辟一块新内存存储资源,而不是简单的赋值。
C++11引入了移动构造函数,默认移动构造函数会执行浅拷贝操作,与拷贝构造函数不同的是,在拷贝结束后,移动构造函数会把源对象内的指针置空
class A {
public:
A(int size) : size_(size) {
data_ = new int[size];
}
A(){}
//拷贝构造函数,深拷贝
A(const A& a) {
size_ = a.size_;
data_ = new int[size_];
// need memcpy
cout << "copy " << endl;
}
//移动构造函数
A(A&& a) {
this->data_ = a.data_;
a.data_ = nullptr;
cout << "move " << endl;
}
~A() {
if (data_ != nullptr) {
delete[] data_;
}
}
int *data_;
int size_;
};
int main() {
A a(10);
// 调用的是拷贝构造函数
A b = a;
// 调用移动构造函数
A c = std::move(a);
return 0;
}
由此可见。如果不使用std::move(),会有很大的拷贝代价,使用移动语义可以避免很多无用的拷贝,提高程序性能,C++所有的STL都实现了移动语义,方便我们使用。
std::vector<string> vecs;
std::vector<string> vecm = std::move(vecs);
2、完美转发std::forward
函数模板可以将自己的参数“完美”地转发给内部调用的其它函数。所谓完美,即不仅能准确地转发参数的值,还能保证被转发参数的左、右值属性不变。
完美转发指可以写一个接受任意实参的函数模板,并转发到其它函数,目标函数会收到与转发函数完全相同的实参,转发函数实参是左值那目标函数实参也是左值,转发函数实参是右值那目标函数实参也是右值。
void PrintV(int &t) {
cout << "lvalue" << endl;
}
void PrintV(int &&t) {
cout << "rvalue" << endl;
}
template<typename T>
void Test(T &&t) {
PrintV(t);
PrintV(std::forward<T>(t));
PrintV(std::move(t));
}
int main() {
Test(1); // lvalue rvalue rvalue
int a = 1;
Test(a); // lvalue lvalue rvalue
Test(std::forward<int>(a)); // lvalue rvalue rvalue
Test(std::forward<int&>(a)); // lvalue lvalue rvalue
Test(std::forward<int&&>(a)); // lvalue rvalue rvalue
return 0;
}
参考
https://zhuanlan.zhihu.com/p/526034602?utm_id=0
https://zhuanlan.zhihu.com/p/137662465