右值引用
文章目录
- 右值引用
- 左值
- 左值引用
- 右值
- 右值引用
- 右值与左值的区别
- 性能优化
- &&特性
- 模板中的 `T&&`:完美转发与转发引用
- 自动类型推导中的 `auto&&`:推导为转发引用
- `const T&&` 和 `T&&` 的区别
- 未定引用与右值引用的区别
左值
左值是指可以出现在赋值语句左边的对象,它代表一个具有持久存储位置的值。换句话说,左值指向内存中某个具体的地址,可以在这个地址上进行修改。
- 特点:
- 左值有名称和内存地址
- 可以在赋值语句中作为左边的对象。
- 左值通常代表一个对象、变量或指针。
- 示例:
int x = 10; // x 是一个左值
x = 20; // x 可以出现在赋值语句的左边
## 在上面的代码中,x 是一个左值,它有一个地址并且可以被修改。
左值引用
左值引用 (T&) 用于绑定到一个左值,允许你通过引用修改其值。
int a = 10;
int& b = a; // b 是 a 的左值引用
b = 20; // 修改了 a 的值,a 变为 20
右值
右值是指临时的、不持久的值,它通常代表一个没有名字的值,或者是一个表达式的结果。右值通常是一个可以被销毁、不可修改的临时对象。
- 特点:
- 右值通常是临时对象、常量或运算结果。
- 不能出现在赋值语句的左边。
- 右值没有明确的内存位置,通常是短生命周期的临时对象。
- 示例:
int x = 10 + 20; // 10 + 20 是一个右值
int y = x + 30; // x + 30 是一个右值
右值引用
C++11 引入了右值引用 (T&&),允许你“窃取”右值的资源,右值引用就是对一个右值进行引用的类型。因为右值是匿名的,所以我们只能通过引用的方式找到它。无论声明左值引用还是右值引用都必须立即进行初始化,因为引用类型本身并不拥有所绑定对象的内存,只是该对象的一个别名。通过右值引用的声明,该右值又“重获新生”,其生命周期与右值引用类型变量的生命周期一样,只要该变量还活着,该右值临时量将会一直存活下去。
int&& r = 10 + 20; // 10 + 20 是右值,r 是右值引用
右值与左值的区别
- 左值:表示内存中的持久对象,可以取地址,可以修改其值。
- 右值:表示临时对象,通常是表达式结果,不持久化,不能取地址,生命周期较短。
性能优化
在C++中在进行对象赋值操作的时候,很多情况下会发生对象之间的深拷贝,如果堆内存很大,这个拷贝的代价也就非常大,在某些情况下,如果想要避免对象的深拷贝,就可以使用右值引用进行性能的优化。
假设我们有一个自定义的 String
类,它管理一个动态分配的字符数组(字符串)。如果我们直接复制一个 String
对象,会导致字符串内容被复制,这可能会导致性能问题,尤其是当字符串非常大的时候。通过实现移动构造函数和移动赋值运算符,我们可以通过右值引用来避免不必要的复制,直接转移资源。
#include <iostream>
#include <cstring>
#include <utility> // std::moveclass String {
private:
char* data;public:
// 构造函数
String(const char* str = "") {data = new char[strlen(str) + 1];strcpy(data, str);std::cout << "Constructing: " << data << std::endl;
}// 拷贝构造函数(拷贝语义)
String(const String& other) {data = new char[strlen(other.data) + 1];strcpy(data, other.data);std::cout << "Copy constructing: " << data << std::endl;
}// 移动构造函数(移动语义)
String(String&& other) noexcept : data(other.data) {other.data = nullptr; // 让 `other` 变为空std::cout << "Move constructing\n";
}// 拷贝赋值运算符
String& operator=(const String& other) {if (this != &other) { // 防止自赋值delete[] data; // 释放原来的资源data = new char[strlen(other.data) + 1];strcpy(data, other.data);}std::cout << "Copy assigning: " << data << std::endl;return *this;
}// 移动赋值运算符
String& operator=(String&& other) noexcept {if (this != &other) {delete[] data; // 释放原来的资源data = other.data; // 移动资源other.data = nullptr; // 让 `other` 变为空}std::cout << "Move assigning\n";return *this;
}// 析构函数
~String() {if (data != nullptr) {std::cout << "Destroying: " << data << std::endl;delete[] data;}
}// 打印字符串
void print() const {std::cout << data << std::endl;
}
};int main() {String s1("Hello, World!");String s2 = std::move(s1); // 使用移动构造s2.print(); // 打印 "Hello, World!",此时 s1 已经为空s1.print(); // 什么也不打印,因为 s1 的资源已经被移动String s3 = std::move(s2); // 使用移动赋值s3.print(); // 打印 "Hello, World!",此时 s2 已经为空s2.print(); // 什么也不打印,因为 s2 的资源已经被移动
}
移动构造函数:String(String&& other) 使用 std::move 将 other 的资源(即 data)转移给新创建的对象,并将 other.data 设置为 nullptr,表示资源已经被转移,避免析构时错误释放资源。移动赋值运算符:String& operator=(String&& other) 通过右值引用,将 other 的资源移动到当前对象,并将 other.data 设置为 nullptr,避免了不必要的深度复制。在 main 函数中,std::move(s1) 和 std::move(s2) 将左值转换为右值,使得移动构造和移动赋值能够发生,而不是深度复制。 右值引用具有移动语义,移动语义可以将资源(堆、系统对象等)通过浅拷贝从一个对象转移到另一个对象这样就能减少不必要的临时对象的创建、拷贝以及销毁,可以大幅提高C++应用程序的性能。
&&特性
在 C++ 中 &&
并不总是表示右值引用,特别是在模板和自动类型推导中有一些特殊的用法。如果是模板参数需要指定为T&&,如果是自动类型推导需要指定为auto &&,在这两种场景下 &&被称作未定的引用类型。另外还有一点需要额外注意const T&&表示一个右值引用,不是未定引用类型。
模板中的 T&&
:完美转发与转发引用
当我们在模板函数中使用 T&&
时,它并不一定是一个右值引用。这里的 T&&
被称为 转发引用(Forwarding Reference),也有时候称为 万能引用(Universal Reference)。它的行为是根据传递给模板的类型推导而变化的:
- 如果传入一个 左值,
T&&
会变成一个 左值引用。 - 如果传入一个 右值,
T&&
会变成一个 右值引用。
这种机制使得我们可以在模板中根据传入的值类型(左值或右值)适当选择绑定左值或右值引用,达到完美转发的效果。
template <typename T>
void f(T&& arg) {// T&& 是转发引用,它根据传入的 arg 自动推导成左值引用或右值引用
}template<typename T>
void f(T&& param);
void f1(const T&& param);
f(10);
int x = 10;
f(x);
f1(x); // error, x是左值
f1(10); // ok, 10是右值
自动类型推导中的 auto&&
:推导为转发引用
在自动类型推导中,auto&&
会被推导成转发引用,类似模板中的 T&&
。它的行为与模板中的转发引用相同——会根据传递给 auto
的类型推导出是左值引用还是右值引用。
int x = 10;
auto&& a = x; // a 被推导为 int&(左值引用)
auto&& b = 5; // b 被推导为 int&&(右值引用)int main()
{int x = 520, y = 1314;auto&& v1 = x;auto&& v2 = 250;decltype(x)&& v3 = y; // error 等价于int&&是一个右值引用不是未定引用类型,y是一个左值,不能使用左值初始化一个右值引用类型。cout << "v1: " << v1 << ", v2: " << v2 << endl;return 0;
};
const T&&
和 T&&
的区别
const T&&
是一个右值引用,而不是未定引用类型,这是一个重要的细节。const T&&` 是指 常量右值引用,它绑定到右值,但不可修改引用的对象。它并不等同于未定引用类型(即转发引用)。
const int&& ci = 10; // ci 是一个常量右值引用
未定引用与右值引用的区别
- 右值引用:在类型为
T&&
的情况下,右值引用用于绑定右值,允许移动语义。 - 转发引用(未定引用):
T&&
在模板或auto&&
中,是一种“未定类型”的引用,其具体是左值引用还是右值引用依赖于传入的实际参数。