左值引用和右值引用

左值和右值的区分

左值是取地址,代表持续存活之物

右值不可以取地址,代表即将消亡之物

示例见下:num 是左值,3 是右值。

1
int num = 3;

左值引用和右值引用

无论左值引用还是右值引用,本质都是引用,即都是给对象取别名。

传统的 C++ 语法中就存在引用语法,而 C++11 标准中新增了右值引用的语法特性,因此为了区分两者,将 C++11 标准出现之前的引用称为左值引用。

左值引用就是对左值的引用,给左值取别名。主要作用是避免对象拷贝。

1
2
int x = 10;
int &ref = x; // 左值引用,绑定到变量x

右值引用就是对右值的引用,给右值取别名。主要作用是延长对象的生命周期,一般是延长到作用域之外。广泛用于实现移动语义完美转发

1
int &&rref = 10; // 右值引用,绑定到临时对象10

 

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>

class MyClass {
public:
MyClass() { std::cout << "Constructor\n"; }
MyClass(const MyClass &) { std::cout << "Copy Constructor\n"; }
MyClass(MyClass &&) { std::cout << "Move Constructor\n"; }
};

MyClass createObject() {
return MyClass(); // 返回一个右值(临时对象)
}

int main() {
MyClass obj1; // 调用默认构造函数
MyClass obj2 = obj1; // 调用拷贝构造函数(左值引用)

MyClass obj3 = createObject(); // 调用移动构造函数(右值引用)
return 0;
}

转换

此前我们说左值引用绑定左值,右值引用绑定右值。

那么,左值引用可以绑定右值吗?右值引用可以绑定左值吗?

右值到左值引用的绑定

右值可以通过const 左值引用进行绑定,虽然右值通常没有持久的地址,但 C++ 允许我们使用 const 左值引用来绑定一个右值。这种绑定可以使临时对象的生命周期延长至引用的作用域结束。

1
const int& ref = 10; // 右值 10 被绑定到 const 左值引用 ref 上

这里,右值 10 可以通过 const int& 绑定到 ref,从而延长了 10 的生命周期。在这种情况下,ref 只能被读取,不能被修改。

左值到右值引用的转换

左值可以通过 std::move 显式地转换成右值引用,这使得一个本来是左值的对象可以被移动语义所使用。例如在移动构造函数和移动赋值运算符中,使用 std::move 将左值转为右值引用以实现高效的资源转移。

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <utility>

void process(int&& x) {
std::cout << "Right value: " << x << std::endl;
}

int main() {
int a = 10;
process(std::move(a)); // std::move 将 a 转换为右值引用
return 0;
}

这里 std::move(a) 将左值 a 转为右值引用,因此可以传递给 process 函数。需要注意,使用 std::move 后,a 的状态可能变成“未定义”,通常在此之后不应再直接使用 a,除非它被重新赋值。

为什么出现右值引用和移动语义?

因为有个问题,左值引用解决不了,见下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
string operator+(const string& s, char ch)
{
string ret(s);
ret.push_back(ch);
return ret;
}

// 拿现在这个函数来举例:ret是函数内的局部对象,出了函数作用域后会被析构,即被销毁了
// 若此时再返回它的别名(左值引用),也就是再拿这个对象来用,就会出问题

/*
①:不能返回局部变量的引用。局部变量会在函数返回后被销毁,此时对 局部变量的引用就会成为“无所指”的引用,程序会进入未知状态。
②:不能返回函数内部通过 new 分配的内存的引用。虽然不存在局部变量的被动销毁问题,但如果被返回的函数的引用只是作为一个临时变量出现,而没有将其赋值给一个实际的变量, 那么就可能造成这个引用所指向的空间(有 new 分配)无法释放的情况(由于没有具体的变量名,故无法用 delete 手动释放该内存),从而造成内存泄漏。
*/