左值右值和左值引用右值引用

Colina大约 4 分钟c++现代c++c++

在 C++ 中,左值、右值以及引用是我们经常遇到的概念,深入理解它们对于编写高效且清晰的代码至关重要。在本篇博客中,我们将探讨这些概念的含义、区别以及在实际编程中的应用。

左值和右值

左值和右值是 C++ 中的两种基本表达式类型,它们对应于赋值操作中的左侧和右侧。简而言之,左值是可以取地址的表达式,而右值是临时的、不能取地址的表达式。

  • 左值(Lvalues):具有持久性和身份的表达式,可以取地址的表达式,有较长的生命周期。例如,变量、数组元素、对象成员等都是左值。

  • 右值(Rvalues):临时的、一次性的值,不能取地址的表达式,生命周期很短。例如,常量、临时变量、表达式的计算结果等都是右值。

对于自增运算符x++++xx++会生成一份x的临时复制,然后对x自增,返回复制的内容,所以是一个右值;而++x是直接对x递增后返回其自身,所以是左值,并且像++++x是可以正常使用的。

可以用取地址来检测左值右值

int *p = &x++;  // 错误
int *q = &++x;  // 正确

对于函数get,返回一个全局变量xx明显是左值,但经过函数返回后变成了一个右值,所以像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()不会有任何问题。延长临时对象的生命周期并不是右值引用的最终目标,真正的目的是减少对象复制,提升程序性能。