
C++98的C++语法中就有引⽤的语法,⽽C++11中新增了的右值引⽤语法特性,C++11之后我们之前学习的引⽤就叫做左值引⽤。⽆论左值引⽤还是右值引⽤,都是给对象取别名。 在C++11之前,C++中只有左值引用(&)。左值引用主要用于绑定到已命名的变量,用于函数参数传递、返回值等场景,但无法直接绑定到临时对象。临时对象通常是匿名的,它们在表达式求值过程中产生,并在表达式结束后销毁。例如,std::string(“temporary”)就是一个临时对象。 然而,临时对象在很多场景下都存在资源浪费的问题。以std::vector为例,当我们对一个std::vector对象进行拷贝赋值时,即使源对象是一个临时对象,目标对象也会先分配一块新的内存,然后将源对象中的元素逐个拷贝过去,最后销毁临时对象。这个过程不仅涉及额外的内存分配和拷贝操作,还可能导致不必要的性能开销。 为了解决这一问题,C++11引入了右值引用(&&)。右值引用可以绑定到临时对象,从而允许我们直接操作临时对象的资源,避免不必要的资源浪费。
• 左值是⼀个表⽰数据的表达式(如变量名或解引⽤的指针),⼀般是有持久状态,存储在内存中,我们可以获取它的地址,左值可以出现赋值符号的左边,也可以出现在赋值符号右边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。
• 右值也是⼀个表⽰数据的表达式,要么是字⾯值常量、要么是表达式求值过程中创建的临时对象等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。
• 值得⼀提的是,左值的英⽂简写为lvalue,右值的英⽂简写为rvalue。传统认为它们分别是left value、right value 的缩写。现代C++中,lvalue 被解释为loacto rvalue的缩写,可意为存储在内存中、有明确存储地址可以取地址的对象,⽽ rvalue 被解释为 read value,指的是那些可以提供数据值,但是不可以寻址,例如:临时变量,字⾯量常量,存储于寄存器中的变量等,也就是说左值和右值的核⼼区别就是能否取地址。
• 左值引⽤不能直接引⽤右值,但是const左值引⽤可以引⽤右值
• 右值引⽤不能直接引⽤左值,但是右值引⽤可以引⽤move(左值)
• 需要注意的是变量表达式都是左值属性,也就意味着⼀个右值被右值引⽤绑定后,右值引⽤变量变量表达式的属性是左值在C++中,函数参数的匹配规则会根据参数类型(左值引用、右值引用、普通参数等)和传递的实参类型(左值或右值)有所不同。
对于普通参数(非引用类型),无论是左值还是右值,都会通过拷贝构造函数或拷贝赋值运算符进行传递。这意味着传递的实参会被复制到函数的形参中。
例如:
void func(int x) {
// x 是普通参数,实参会被复制到这里
}
int main() {
int a = 10;
func(a); // a 是左值,会被复制到 x
func(20); // 20 是右值,也会被复制到 x
}左值引用参数只能绑定到左值。如果尝试将右值绑定到左值引用参数,会导致编译错误。
例如:
void func(int& x) {
// x 是左值引用参数
}
int main() {
int a = 10;
func(a); // 正确,a 是左值
func(20); // 错误,20 是右值
}右值引用参数只能绑定到右值。如果尝试将左值绑定到右值引用参数,会导致编译错误。
例如:
void func(int&& x) {
// x 是右值引用参数
}
int main() {
int a = 10;
func(a); // 错误,a 是左值
func(20); // 正确,20 是右值
}C++11引入了万能引用的概念,它允许一个引用既可以绑定到左值,也可以绑定到右值。通用引用通过模板参数和&&实现。
例如:
template <typename T>
void func(T&& x) {
// x 是通用引用
}
int main() {
int a = 10;
func(a); // 正确,a 是左值,x 会退化为左值引用
func(20); // 正确,20 是右值,x 会保持为右值引用
}在通用引用中,T&&的行为取决于模板参数T的具体类型。如果T是一个具体的类型(如int),T&&就是右值引用;如果T是一个模板参数,T&&就是通用引用。
当函数重载时,参数匹配的优先级规则如下:
std::forward,则会根据模板参数的实际类型进行完美转发。例如:
void func(int& x) {
std::cout << "lvalue reference" << std::endl;
}
void func(int&& x) {
std::cout << "rvalue reference" << std::endl;
}
int main() {
int a = 10;
func(a); // 输出 "lvalue reference"
func(20); // 输出 "rvalue reference"
}下面的代码能够总的说明参数匹配这一规则:
#include<iostream>
using namespace std;
void f(int& x)
{
std::cout << "左值引⽤重载 f(" << x << ")\n";
}
void f(const int& x)
{
std::cout << "到 const 的左值引⽤重载 f(" << x << ")\n";
}
void f(int&& x)
{
std::cout << "右值引⽤重载 f(" << x << ")\n";
}
int main()
{
int i = 1;
const int ci = 2;
f(i); // 调⽤ f(int&)
f(ci); // 调⽤ f(const int&)
f(3); // 调⽤ f(int&&),如果没有 f(int&&) 重载则会调⽤ f(const int&)
f(std::move(i)); // 调⽤ f(int&&)
// 右值引⽤变量在⽤于表达式时是左值
int&& x = 1;
f(x); // 调⽤ f(int& x)
f(std::move(x)); // 调⽤ f(int&& x)
return 0;
}左值引⽤主要使⽤场景是在函数中左值引⽤传参和左值引⽤传返回值时减少拷⻉,同时还可以修改实参和修改返回对象的价值。左值引⽤已经解决⼤多数场景的拷⻉效率问题,但是有些场景不能使⽤传左值引⽤返回,C++98中的解决⽅案只能是被迫使⽤输出型参数解决。那么C++11以后这⾥可以使⽤右值引⽤做返回值解决吗?显然是不可能的,因为这⾥的本质是返回对象是⼀个局部对象,函数结束这个对象就析构销毁了,右值引⽤返回也⽆法概念对象已经析构销毁的事实。
在下面代码中,若是没有优化的情况,vv在这里返回时要重新申请空间,进行构造,所花费的代价实际是特别的大的。在之前我们要解决这个问题,就会多一个参数,传过去一个vv。
C++98这里的传值返回拷贝代价就太大了,C++11之后效率就很高,不用担心效率
vector<vector<int>> generate(int numRows) {
vector<vector<int>> vv(numRows);
for (int i = 0; i < numRows; ++i)
{
vv[i].resize(i + 1, 1);
}
for (int i = 2; i < numRows; ++i)
{
for (int j = 1; j < i; ++j)
{
vv[i][j] = vv[i - 1][j] + vv[i - 1][j - 1];
}
}
return vv;
}
C++11之前,得通过输出型参数改善效率
void generate(int numRows, vector<vector<int>>& vv) {
//vector<vector<int>> vv(numRows);
vv.resize(numRows);
for (int i = 0; i < numRows; ++i)
{
vv[i].resize(i + 1, 1);
}
for (int i = 2; i < numRows; ++i)
{
for (int j = 1; j < i; ++j)
{
vv[i][j] = vv[i - 1][j] + vv[i - 1][j - 1];
}
}
}这个问题,在c++11之后是不需要担心的,编译器会自行优化。
参数类型:
const),绑定到左值。&&),绑定到右值。拷贝构造函数是一种特殊的构造函数,它用于通过已存在的对象初始化一个新对象。拷贝构造函数的参数是一个同类型的对象的引用,通常是常量引用(const),以避免修改源对象。
移动构造函数是C++11引入的一种新的构造函数,它用于通过一个右值(通常是临时对象)初始化一个新对象。移动构造函数的参数是一个同类型的对象的右值引用(&&)。通过将临时对象交换,来达到构造的效果


在vs2019的release和vs2022的debug和release,下⾯代码优化为⾮常恐怖,会直接将str对象的构造,str拷⻉构造临时对象,临时对象拷⻉构造ret对象,合三为⼀,变为直接构造。



右值引⽤可⽤于为临时对象延⻓⽣命周期,const 的左值引⽤也能延⻓临时对象⽣存期,但这些对象⽆法被修改。
int main()
{
std::string s1 = "Test";
// std::string&& r1 = s1; // 错误:不能绑定到左值
const std::string& r2 = s1 + s1; // OK:到 const 的左值引⽤延⻓⽣存期
// r2 += "Test"; // 错误:不能通过到 const 的引⽤修改
std::string&& r3 = s1 + s1; // OK:右值引⽤延⻓⽣存期
r3 += "Test"; // OK:能通过到⾮ const 的引⽤修改
std::cout << r3 << '\n';
return 0;
}当实参是⼀个左值时,容器内部继续调⽤拷⻉构造进⾏拷⻉,将对象拷⻉到容器空间中的对象
当实参是⼀个右值,容器内部则调⽤移动构造,右值对象的资源到容器空间的对象上
在C++11之前,值传递会创建参数的副本,这在处理大型对象时会导致不必要的拷贝操作。通过右值引用和移动语义,我们可以优化值传递,避免不必要的拷贝操作。 例如:
void process(std::vector<int> vec) {
// 使用 vec
}在C++11中,编译器会自动优化这个函数,避免不必要的拷贝操作。如果vec是一个临时对象,编译器会直接调用移动构造函数,而不是拷贝构造函数。
在C++11之前,引用传递不能处理临时对象,且不能修改源对象。通过右值引用和移动语义,我们可以优化引用传递,处理临时对象,并允许修改源对象。 例如:
void process(std::vector<int>&& vec) {
// 使用 vec
}在这个例子中,vec是一个右值引用,可以绑定到临时对象,允许我们直接操作临时对象的资源。 3. 通用引用 C++11引入了通用引用的概念,允许一个引用既可以绑定到左值,也可以绑定到右值。通用引用通过模板参数和&&实现。 例如:
template <typename T>
void process(T&& vec) {
// 使用 vec
}在这个例子中,T&&是一个通用引用,可以绑定到左值或右值。通过std::forward,我们可以实现完美转发,根据实际参数类型调用相应的函数。


引用折叠的规则确保了在模板函数中,引用类型能够正确地解析。引用折叠的规则如下:
T& & 折叠为 T&T& && 折叠为 T&T&& & 折叠为 T&T&& && 折叠为 T&&在模板函数中,通用引用的折叠规则如下:
T 是一个具体的类型(如 int),则 T&& 是右值引用。T 是一个引用类型(如 int& 或 int&&),则 T&& 会根据引用折叠规则进行折叠。例如:
template <typename T>
void func(T&& x) {
// x 是通用引用
}通过以下代码可以很好的理解引用折叠:
// 由于引⽤折叠限定,f1实例化以后总是⼀个左值引⽤
template<class T>
void f1(T& x)
{}
// 由于引⽤折叠限定,f2实例化后可以是左值引⽤,也可以是右值引⽤
template<class T>
void f2(T&& x)
{}
int main()
{
typedef int& lref;
typedef int&& rref;
int n = 0;
lref& r1 = n; // r1 的类型是 int&
lref&& r2 = n; // r2 的类型是 int&
rref& r3 = n; // r3 的类型是 int&
rref&& r4 = 1; // r4 的类型是 int&&
// 没有折叠->实例化为void f1(int& x)
f1<int>(n);
f1<int>(0); // 报错
// 折叠->实例化为void f1(int& x)
f1<int&>(n);
f1<int&>(0); // 报错
// 折叠->实例化为void f1(int& x)
f1<int&&>(n);
f1<int&&>(0); // 报错
// 折叠->实例化为void f1(const int& x)
f1<const int&>(n);
f1<const int&>(0);
// 折叠->实例化为void f1(const int& x)
f1<const int&&>(n);
f1<const int&&>(0);
// 没有折叠->实例化为void f2(int&& x)
f2<int>(n); // 报错
f2<int>(0);
// 折叠->实例化为void f2(int& x)
f2<int&>(n);
f2<int&>(0); // 报错
// 折叠->实例化为void f2(int&& x)
f2<int&&>(n); // 报错
f2<int&&>(0);
return 0;
}
template<class T>
void Function(T&& t)
{
int a = 0;
T x = a;
//x++;
cout << &a << endl;
cout << &x << endl << endl;
}
int main()
{
// 10是右值,推导出T为int,模板实例化为void Function(int&& t)
Function(10); // 右值
int a;
// a是左值,推导出T为int&,引⽤折叠,模板实例化为void Function(int& t)
Function(a); // 左值
// std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t)
Function(std::move(a)); // 右值
const int b = 8;
// a是左值,推导出T为const int&,引⽤折叠,模板实例化为void Function(const int&
t)
// 所以Function内部会编译报错,x不能++
Function(b); // const 左值
// std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&&
t)
// 所以Function内部会编译报错,x不能++
Function(std::move(b)); // const 右值
return 0;
}完美转发是指在函数调用过程中,将参数的类型和值类别(左值或右值)完整地传递给另一个函数。这意味着,如果传递给函数的是一个左值,那么被调用的函数也会接收到一个左值;如果传递的是一个右值,那么被调用的函数也会接收到一个右值。