C++11【左右值引用精析】
目录
一、左值引用
左值是指能够表示数据的表达式,包括变量名、指针和解引用指针等 ,它具有以下特性:
- 可以获取其内存地址
- 既能出现在赋值运算符左侧,也能出现在右侧(而右值仅能出现在右侧)
- 被const修饰的左值虽然不可赋值,但仍可获取其地址
左值引用本质是对左值的别名,使用&符号进行声明
代码演示:
#include <iostream>
using namespace std;
int main()
{
//左值:
int a = 10;
int* ptr = new int(10);
const char* f = "wxihuanxiachi";//字符串具有常性,所以要加const
//左值可以取出地址:
cout << &a << endl;
cout << &ptr << endl;//取出的是栈上存储ptr指针的空间的地址
cout << &(*ptr) << endl;//取出的是堆上存放数据10那块空间的地址
cout << (void*)f << endl;//由于cout遇到字符串会直接打印字符串
//所以这里我们强转一下打印ta的地址
cout << (void*)"wxihuanxiachi" << endl;//字符串"wxihuanxiachi"的本质是表示为常量区
cout << (void*)&((f[1])) << endl;//取出字符串"wxihuanxiachi"中的二个字符'x'的地址
//以下是对左值进行的左值引用
int& r = a;//左值a可以出现在赋值符号的右边
int*& rp = ptr;
const char*& fx = f;
return 0;
}
二、右值引用
右值也是一个数据的表达式,如:常量(10),表达式返回值(x+y),函数返回值(func())(函数返回值是右值的前提是返回值不能是传&返回,返回值应该是传值返回)
右值具有两个重要特征:
- 不能出现在赋值符号左侧
- 无法获取其内存地址
右值引用(使用&&表示)实质上是对右值的别名引用,允许我们对右值进行直接操作(右值引用是左值,小编会在后面讲到)
代码演示:
int func(double x, double y)//函数是传值返回
{
return x + y;
}
int main()
{
double x = 1.1, y = 2.2;
//以下的10,x + y,func(x, y)都是右值
10;
x + y;//x+y的临时变量是右值
func(x,y)
//以下是右值引用对右值的引用
int&& r7 = 10;//可见右值10可以出现在赋值符号的右边
double&& b = x + y;
double&& c = func(x, y);
//编写的错误的赋值操作(右值不能被修改)
10 = 1;//可以看出右值10不能出现在赋值符号的左边
x + y = 1;//表答式不能修改
func(x, y) = 1;//函数返回值不能修改
//以下三个错误的取地址操作
cout << &10 << endl;//常量不能取地址
cout << &(x + y) << endl;//x + y表达式的返回值是一个临时对象,临时对象不能直接取出地址(所以可以看到临时变量也是右值)
cout << &func(x, y) << endl;//func(x + y)由于是传值返回
//所以会使用拷贝一个临时对象进行返回,临时对象不能直接取出地址
return 0;
}
三、右值引用的场景
3.1右值的作用
右值引用的核心价值是进一步减少拷贝弥补左值引用没有解决的场景,如传值返回
先看场景一,自定义类型中拷贝的类必须要传值返回的场景
int main()
{
string ret3("XXXXXXXXXXXXX");
string copy = ret3;
return 0;
}
- ret3开了空间存字符串 ,此处copy 又开了空间拷贝ret3 ,ret3的空间再释放
加上右值
int main()
{
string ret3("XXXXXXXXXXXXX");
string copy = ret3;
move(ret3);//ret3会变成右值吗
string copy2 = ret3;//不会,move并不会改变ret3的属性,而是返回了一个ret3的浅拷贝的右值(指向ret3同一个空间)
//什么时候会 ,什么时候都不会
string copy3 = move(ret3);//ret3直接变成空了,copy3接手ret3的空间
//那move的底层是怎么实现的
//move会返回一个右值,这个右值是和ret3同空间的(这不相当与浅拷贝嘛)
return 0;
}
- move(ret3);ret3不会变成右值, move并不会改变ret3的属性,而是返回了一个ret3的浅拷贝的右值(指向ret3同一个空间)
- string copy3 = move(ret3);copy3接手ret3的空间 ,ret3再被置为空指针
- move会返回一个右值,这个右值是和ret3同空间的(相当于浅拷贝)
- 此时copy3通过移动拷贝节省了空间
3.2右值引用的作用
还有一个场景能减少拷贝
int main()
{
list<string> lt;
string s1("XXXXXXXXX");
lt.push_back(s1);
string s2("XXXXXXXXX");
lt.push_back(move(s2));
lt.push_back("xxxxxxxxxx");//这样写也是一个移动拷贝,"xxx..."产生一个临时对象(不能取地址):临时对象是右值->发生移动拷贝
//那在移动构造没出时,"xxx.."是右值为什么还能调用左值的构造?,因为构造函数的参数加了const : Node(const string& s)
//这样右值和左值都能用
return 0;
}
- s1开空间存字符串,新节点开空间拷贝s1
- s2开空间存字符串 ,lt.push_back() 把s2识别为右值,可以直接把空间拿过来,(也相当于延长了s2的空间的生存时间)
- lt.push_back("xxxxxxxxxx"); 这样写也是一个移动拷贝,"xxx..."产生一个临时对象(不能取地址):临时对象是右值->发生移动拷贝 ,那在移动构造没出时,"xxx.."是右值为什么还能调用左值的构造? 因为构造函数的参数加了const : Node(const string& s) ,这样右值和左值都能用



容器的插入接口,如果插入的对象是右值,可以利用移动构造转移资源给数据结构中的对象,减少空间的消耗
四、完美转发
4.1万能引用
template<typename T>
void PerfectForward(T&& t)
{
Fun(t); //T&& 这个是万能引用,无论是左值还是右值都可以传给他,而不是单单只接受右值,当传左值给ta时,ta 会发生引用折叠,它既可以接受左值,又可以接受右值 ,如果实参是左值的话,那么它就是左值引用 ,发生引用折叠 (T&& -> T& ),如果实参是右值,那么他就是右值引用,(但是无论是传右值还是左值,这个t都是左值,小编在后文中会讲到)
}
在C++中,
T&&被称为万能引用(Universal Reference)或转发引用(Forwarding Reference),它确实具有非常灵活的特性。让我们详细分析它的工作原理:
基本特性:
- 当用于模板参数推导时,
T&&可以绑定到任何类型的值- 它可以接受左值(lvalue)、右值(rvalue)、const限定的值
引用折叠规则:
- 当传递左值时,发生引用折叠:
T&& &→T&- 当传递右值时:
T&& &&→T&&- 这就是为什么它能同时处理左值和右值
代码演示:
template<typename T>
void func(T&& t) { // 万能引用
// 函数体
}
int main() {
int x = 10;
const int cx = 20;
func(x); // 左值,T 推导为int&
func(cx); // const左值,T推导为const int&
func(30); // 右值,T推导为int
}
- 无论传入的是左值还是右值,在函数内部参数
tt本身都是一个左值 - 这是因为所有具名的值都是左值
- 如果需要保持原始值的类别,需要使用
std::forward(完美转发)
这种机制使得C++能够实现高效且灵活的通用编程,特别是在需要同时处理左值和右值的模板代码中非常有用
4.2右值识别成左值
我们来看一段代码:
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
template<typename T>
void PerfectForward(T&& t) // 这样不是右值的引用,而是表示T&& 既可以接受左值又可以接受右值
{ //如果实参是左值,则ta就是左值引用 (引用折叠:int&& -> int& )
Fun(t); //无论是传右值还是左值,t 都是左值
//如果我就想让t保持属性呢
//t是左值引用,t是左值
//t是右值引用,t是右值
//Fun(forward<T>(t)); //完美转发 ,是什么类型的引用 ,就是什么类型的值
}
int main()
{
PerfectForward(10);//右值
int a;
PerfectForward(a);//左值
PerfectForward(move(a));//右值
const int b = 8;
PerfectForward(b);//左值
PerfectForward(move(b));//右值
int& r = a;//r是左值
int&& rr = move(a);//rr是左值 ,右值引用变量的属性会被编译器识别成左值,否则在移构造的场景下,无法完成资源的转移
//可以认为编译器在这里做了特殊处理,把rr强行识别成了左值 ,要不然右值是不允许修改的
return 0;
}
-
10是右值 ,int a;是左值 ,move(a)是右值 ,const int b = 8;是左值 ,move(b)是右值
看一下运行结果:

为什么是这样 ,为什么不是移动拷贝
看这段代码:
int& r = a;//r是左值
int&& rr = move(a);//rr是左值 ,右值引用变量的属性会被编译器识别成左值,否则在移构造的场景下,无法完成资源的转移
rr++; //可以认为编译器在这里做了特殊处理,把rr强行识别成了左值 ,要不然右值是不允许修改的
如果rr是右值,那么它是不能改变的 ,但rr此时能++,说明rr是左值 ,为什么呢 ,因为右值引用变量的属性会被编译器识别成左值,否则在移动构造的场景下,无法完成资源的转移,可以认为编译器在这里做了特殊处理,把rr强行识别成了左值 ,要不然右值是不允许修改的
回到上面的代码,这里的T&& t ,在实参是右值时,t是T&& 类型的,而t是左值,为什么
看下面这段代码:
//这是移动拷贝的实现:
string中重载operator=(string&& s)
string& operator=(string&& s)
{
swap(s);
return *this;
}
swap(s):这里是void swap(string& s) ,因为这里要修改,右值是不允许修改的,所以参数是左值,而operator=(string&& s) ,s是string&& 类型的,如果s是右值,
void swap(string& s) 那ta怎么在swap中修改,所以编译器在这里做了特殊处理,把rr强行识别成了左值
{ }
- swap(s):这里的形参是string& s ,因为这里要s是要修改的,右值是不允许修改的,所以参数是左值,而operator=(string&& s) ,s是string&& 类型的,如果s是右值,void swap(string& s) 那ta怎么在swap中修改,所以可以理解为编译器在这里做了特殊处理,把rr强行识别成了左值
另一个场景中: 
这里val是右值引用,依旧被识别成了左值,所以这里_val依旧是被左值初始化的,所以在这里我们要把val是右值引用,保持右值的特性。
所以在上上面的代码中,t 要被识别成右值 )
4.3右值识别成右值
这里就要用到完美转发 ,什么是完美转发?
- 完美转发 ,是什么类型的引用 ,就是什么类型的值 :Fun(forward<T>(t))
- t是左值引用,t是左值
- t是右值引用,t是右值
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
template<typename T>
void PerfectForward(T&& t) // 这样不是右值的引用,而是表示T&& 既可以接受左值又可以接受右值
{ //如果实参是左值,则ta就是左值引用 (引用折叠:int&& -> int& )
//无论是传右值还是左值,t 都是左值
//如果我就想让t保持属性呢
//t是左值引用,t是左值
//t是右值引用,t是右值
Fun(forward<T>(t)); //完美转发 ,是什么类型的引用 ,就是什么类型的值
}
int main()
{
PerfectForward(10);//右值
int a;
PerfectForward(a);//左值
PerfectForward(move(a));//右值
const int b = 8;
PerfectForward(b);//左值
PerfectForward(move(b));//右值
int& r = a;//r是左值
int&& rr = move(a);//rr是左值 ,右值引用变量的属性会被编译器识别成左值,否则在移构造的场景下,无法完成资源的转移
//可以认为编译器在这里做了特殊处理,把rr强行识别成了左值 ,要不然右值是不允许修改的
return 0;
}

五、源代码
//场景一,自定义类型中拷贝的类必须传值返回的场景
int main()
{
string ret3("XXXXXXXXXXXXX");
string copy = ret3;
move(ret3);//ret3会变成右值吗
string copy2 = ret3;//不会,move并不会改变ret3的属性,而是返回了一个ret3的浅拷贝的右值(指向ret3同一个空间)
//什么时候会
string copy3 = move(ret3);//ret3直接变成空了,copy3接手ret3的空间
//那move的底层是怎么实现的
//move会返回一个右值,这个右值是和ret3同空间的(这不相当与浅拷贝嘛)
return 0;
}
//还有一个场景能减少拷贝
//容器的插入接口,如果插入的对象是右值,可以利用移动构造转移资源给数据结构中的对象,减少空间的消耗
int main()
{
list<string> lt;
string s1("XXXXXXXXX");
lt.push_back(s1);
string s2("XXXXXXXXX");
lt.push_back(move(s2));
lt.push_back("xxxxxxxxxx");//这样写也是一个移动拷贝,"xxx..."产生一个临时对象(不能取地址):临时对象是右值->发生移动拷贝
//那在移动构造没出时,"xxx.."是右值为什么还能调用左值的构造?,因为构造函数的参数加了const : Node(const string& s)
//这样右值和左值都能用
return 0;
}
//完美转发
//万能引用
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
template<typename T>
void PerfectForward(T&& t) // 这样不是右值的引用,而是表示T&& 既可以接受左值又可以接受右值
{ //如果实参是左值,则ta就是左值引用 (引用折叠:int&& -> int& )
Fun(t); //无论是传右值还是左值,t 都是左值
//如果我就想让t保持属性呢
//t是左值引用,t是左值
//t是右值引用,t是右值
//Fun(forward<T>(t)); //完美转发 ,是什么类型的引用 ,就是什么类型的值
}
int main()
{
PerfectForward(10);//右值
int a;
PerfectForward(a);//左值
PerfectForward(move(a));//右值
const int b = 8;
PerfectForward(b);//左值
PerfectForward(move(b));//右值
int& r = a;//r是左值
int&& rr = move(a);//rr是左值 ,右值引用变量的属性会被编译器识别成左值,否则在移构造的场景下,无法完成资源的转移
//可以认为编译器在这里做了特殊处理,把rr强行识别成了左值 ,要不然右值是不允许修改的
return 0;
}
//这是移动拷贝的实现:
//string中重载operator=(string&& s)
/*string& operator=(string&& s)
{
swap(s);
return *this;
}
swap(s):这里是string& s ,因为这里要修改,右值是不允许修改的,所以参数是左值,而operator=(string&& s) ,s是string&& 类型的,如果s是右值,
void swap(string& s) 那ta怎么在swap中修改,所以编译器在这里做了特殊处理,把rr强行识别成了左值
{ }*/
//那有没有需要把右值引用识别成右值的场景呢
更多推荐


所有评论(0)