左值右值和左值引用右值引用
在 C++ 中,左值、右值以及引用是我们经常遇到的概念,深入理解它们对于编写高效且清晰的代码至关重要。在本篇博客中,我们将探讨这些概念的含义、区别以及在实际编程中的应用。
左值和右值
左值和右值是 C++ 中的两种基本表达式类型,它们对应于赋值操作中的左侧和右侧。简而言之,左值是可以取地址的表达式,而右值是临时的、不能取地址的表达式。
左值(Lvalues)
:具有持久性和身份的表达式,可以取地址的表达式,有较长的生命周期。例如,变量、数组元素、对象成员等都是左值。右值(Rvalues)
:临时的、一次性的值,不能取地址的表达式,生命周期很短。例如,常量、临时变量、表达式的计算结果等都是右值。
对于自增运算符x++
和++x
,x++
会生成一份x
的临时复制,然后对x
自增,返回复制的内容,所以是一个右值
;而++x
是直接对x
递增后返回其自身,所以是左值
,并且像++++x
是可以正常使用的。
可以用取地址来检测左值右值
int *p = &x++; // 错误
int *q = &++x; // 正确
对于函数get
,返回一个全局变量x
,x
明显是左值,但经过函数返回后变成了一个右值,所以像int *p = &get()
会编译失败,原因与x++
类似,返回的是x
的复制;set
虽然传的是右值,但是进入函数被赋值给了形参val变成了左值,对val取地址不会有问题。
int x;
int get() {
return x;
}
void set(int val) {
int *p = &val;
x = val;
}
- 注意:字面值通常是右值,除了字符串字面值
auto p = &"hello world"; // 右值
左值引用和右值引用
在 C++11 中引入了右值引用的概念,它们与左值引用一起提供了更灵活的内存管理和性能优化机制。
左值引用(Lvalue References):通过
&
符号声明的引用,用于绑定到左值。左值引用允许修改所引用对象的值。右值引用(Rvalue References):通过
&&
符号声明的引用,用于绑定到右值。右值引用通常用于移动语义和完美转发,提高了内存利用效率和性能。
左值引用在一定程度上脱离了危险的指针。非常量左值引用只能引用左值,而常量左值引用既可以引用左值也可以引用右值。
int &x1 = 1; // 错误
const int &x2 = 2; // 正确
虽然const int &x2 = 2;
与const int x2 = 2;
在结果上是一样的,但因为前者是引用,所以语句结束后11的生命周期会被延长,而后者语句结束后11会被销毁。这在函数形参列表中有巨大作用,最典型的例子就是复制构造函数和复制赋值运算符函数,通常这两个函数的形参都是一个常量左值引用。
class X {
public:
X() {}
X(const X &x) {}
X& operator = (const X &x) { return *this; }
};
常量左值引用可以绑定右值是非常好的特性,但也存在一个缺点——常量性。一但使用常量左值引用就表示我们无法在函数里修改该对象(强制类型转换除外),所以需要右值引用来完成这项工作。
int i = 0;
int &j = i; // 左值引用
int &&k = 2; // 右值引用
如果右值引用k
试图引用变量i
,则会报错。右值引用的特性之一是延长右值的生命周期
#include <iostream>
using namespace std;
class X {
public:
X() { cout << "X ctor" << endl; }
X(const X &x) { cout << "X copy ctor" << endl; }
~X() {cout << "X dtor" << endl; }
void show() { cout << "show X" << endl; }
};
X make_x() {
X x1;
return x1;
}
int main() {
X &&x2 = make_x();
x2.show();
}
如果X &&x2 = make_x();
这句话是X x2 = make_x();
,在没有进行优化的情况下会发生3次构造:make_x()
中x1
会默认构造一次,然后return x1
会复制构造产生临时对象,接着X x2 = make_x()
会用复制构造将临时对象复制到x2
,最后临时对象被销毁。
使用右值引用后的结果为(使用GCC需要加上命令行参数-fno-elide-constructors
来关闭函数返回值优化(RVO),具体方法是在CMakeLists.txt中添加set(CMAKE_CXX_FLAGS "-fno-elide-constructors")
)
X ctor
X copy ctor
X dtor
show X
X dtor
从结果看只发生了两次构造:第一次是make_x()
中x1
的默认构造,第二次是return x1
的复制构造。由于x2
是右值引用,引用对象是make_x()
返回的临时对象,所以该临时对象的生命周期得以延长,在语句结束后继续调用show()
不会有任何问题。延长临时对象的生命周期并不是右值引用的最终目标,真正的目的是减少对象复制,提升程序性能。