C++:右值引用
右值与左值
在讲解右值引用之前,我们就需要先辨析一下左值
与右值
的区别。
左值
左值是一个表示数据的表达式,我们可以获取它的地址并且对其赋值,左值可以出现在赋值操作符=
的左边,但是右值不能。
int i = 0;
int* p = &i;
double d = 3.14;
变量i
,p
,d
都是左值,一方面来说,它们出现在了=
的左边,另一方面来说,我们可以对其取地址,并修改它的值。
const int ci = 0;
int const* cp = &i;
const double cd = 3.14;
变量ci
,cp
,cd
都是左值,它们出现在了=
的左边,我们可以对其取地址。但是由于具有const
属性,我们不能修改它。
因此, 左值
最显著的特征是可以取地址,但是不一定可以被修改。
右值
右值也是一个表达数据的表达式,比如字面常量
,表达式返回值
,函数返回值
等等,右值可以出现在赋值操作符=
的右边,但是不能出现在=
的左边,右值不能取地址。
double func()
{
return 3.14;
}
int x = 10;
int y = 20;
int z = x + y;
double d = func();
以上代码中,10
,20
,x + y
,func()
都是右值,它们出现在=
的右边。10
,20
对应了字面常量
;x + y
对应了表达式返回值
;func()
对应函数返回值
。这些都是右值,它们最显著的特点就是无法取地址。
右值引用语法
先回顾一下左值引用的语法:
int i = 0;
int* p = nullptr;
int& ri = i;
int*& rp = p;
左值引用的语法是:type&
;右值引用的语法是:type&&
。
接下来我们尝试对刚刚的值进行右值引用:
int&& ri = 0;
int*&& rp = nullptr;
double&& rd = 3.14;
- 左值引用不能直接引用右值
- const左值引用 可以引用右值
用左值引用直接引用右值:
int& i = 5; // 非法
以const
引用的形式,那么就可以引用右值:
const int& i = 5; // 合法
- 右值引用不能直接引用左值
- 右值引用可以引用
move
后的左值
右值引用不能直接引用左值:
int i = 5;
int&& rri = i; // 非法
函数move
,其可以把一个左值强制转化为一个右值。
int i = 5;
int&& rri = move(i); // 合法
move(i)
之后,i
依然是左值,但是move(i)
这个表达式返回了一个右值的i
。
右值引用底层
既然存在右值引用这个语法,那么我们来看看右值引用到底干了些啥。
右值引用的工作主要有两种情况,一种是右值引用了常量,另外一种是右值引用了move
后的左值。
右值引用了常量:
当右值引用了常量,引用会把常量区中的数据拷贝一份到栈区,然后该引用指向栈区中拷贝后的数据
看到一段代码:
int&& r = 5;
r = 10;
以上过程中,我们先用r右值引用了常量5,然后通过右值引用把5改为了10。
如果这个过程中,右值常量5存储在常量区,r右值引用后如果r指向常量区的5,会发生什么?此时我们的r = 10操作,就相当于把常量区的5修改为了10,从此以后整个程序中只要去常量区拷贝5都会变成拷贝10,这可就完蛋了。因此我们的右值引用常量,绝对不能直接引用常量区的数据!!
因此,右值引用常量时的真实操作是把常量区的数据拷贝到栈区中,然后这个引用指向这一块栈区内存。
- 当右值引用了常量,引用会把常量区中的数据拷贝一份到栈区,然后该引用指向栈区中拷贝后的数据,该数据可以修改
- 当
const
左值引用了常量,引用会把常量区中的数据拷贝一份到栈区,然后该引用指向栈区中拷贝后的数据,但是该数据是常量,不能修改
右值引用了move后的左值:
- 当右值引用了
move
后的左值,右值引用直接指向该左值
看到以下代码:
int i = 5;
int&& rri = move(i);
rri = 10;
cout << i << endl;
cout << rri << endl;
程序输出结果为:
10
10
我们可以通过修改右值引用来修改左值,或者说以通俗点的说法,此时右值引用就是这个左值的别名。确实是这样的,当右值引用了move
后的左值,其实和直接左值引用这个左值没有任何区别。
- 左值引用解决了传参时存在的拷贝问题
string add_string(string& s1, string& s2)
{
string s = s1 + s2;
return s;
}
int main()
{
string str;
string hello = "Hello";
string world = "world";
str = add_string(hello, world);
return 0;
}
以上代码中,add_string
函数需要接收两个string
类型的参数,此时我们使用传引用传参,就可以避免两个string
的拷贝消耗。
2.左值引用解决了一部分返回值的拷贝问题
string& say_hello()
{
static string s = "hello world";
return s;
}
int main()
{
string str1;
string str2;
str1 = say_hello();
return 0;
}
以上代码中,函数say_hello生成了一个string,并把它返回给外部,如果我们直接返回,那么str1接收参数时,就会先拷贝构造出一个临时变量,然后临时变量再拷贝构造str1。这个过程发生了两次拷贝构造。但是返回值s指向的string是全局的,其出了函数依然存在,因此我们传引用返回,可以不用拷贝构造一个临时变量,直接拿返回值s去拷贝构造,节省了一次拷贝构造。
也就是说,左值引用通过传引用传参
和传引用返回
节省了拷贝。
右值引用,其实更多的是一种标记。
先来看看什么情况下会产生可以被右值引用的左值:
- 当一个左值被move后,可以被右值引用
- C++会把即将离开作用域的非引用类型的返回值当成右值,这种类型的右值也称为
将亡值
右值的意思就是:这个变量的资源可以被迁移走
移动语义
为了讲解移动语义,先写一个简单的mystring
类:
class mystring
{
public:
//构造函数
mystring(const char* str = "")
{
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
//析构函数
~mystring()
{
delete[] _str;
}
// 赋值重载
mystring& operator=(const mystring& s)
{
cout << "赋值重载" << endl;
return *this;
}
// 拷贝构造
mystring(const mystring& s)
{
cout << "拷贝构造" << endl;
}
private:
char* _str = nullptr;
};
mystring get_string()
{
mystring str("hello");
return str;
}
int main()
{
mystring s2 = get_string();
return 0;
}
s2
通过函数get_string
来获得字符串,并构造自己。这个过程中,由于str
是局部变量,会发生拷贝构造临时变量,临时变量再拷贝构造s2
的过程。
但是由于str
是一个将亡值
,具有右值属性,我们可以写一个函数直接把它的资源转移走:
class mystring
{
public:
// 移动构造
mystring(mystring&& s)
{
cout << "移动构造" << endl;
std::swap(_str, s._str);
}
};
函数主体部分,通过一个swap
函数把参数s
的_str
指针成员与自己的_str
成员进行交换。由于指针指向字符串数组,此时相当于把s
的字符串数组交换给自己,这样就完成了对右值引用的数据转移。
除了移动构造,我们还有原先的拷贝构造:
class mystring
{
public:
// 移动构造
mystring(mystring&& s)
{
cout << "移动构造" << endl;
std::swap(_str, s._str);
}
// 拷贝构造
mystring(const mystring& s)
{
cout << "拷贝构造" << endl;
}
};
虽然说我们的左值引用,也可以达到这样的移动构造,但是有一个问题,并不是所有的对象,资源都是可以被转移走的。移动构造之所以这么叫,就是因为移走了别人的资源。这部分资源之所以会被移走,就是因为它有右值属性。而它之所以有右值属性,要么就是这个变量是个将亡值,资源不转移就浪费了;要么就是被程序员亲自move了,程序员许可把这个对象的资源转移走。
就是这样的一个逻辑闭环,右值引用以一个既安全,又高效的方式,完成了局部变量的资源拷贝问题。而这个过程,也叫做右值引用的移动语义。
移动
:改语法实现了通过移走别人的资源,实现高效的创建对象,避免大量拷贝语义
:在这个过程中,右值引用只提供语义层面的功能,即许可一个对象资源被转移的右值语义
因为右值引用的出现,C++11后,类的默认成员函数从6
个变成了8
个。新增两个成员函数:移动构造
,移动赋值重载
。
//移动赋值重载
mystring& operator=(mystring&& s)
{
std::swap(_str, s._str);
return *this;
}
// 移动构造
mystring(mystring&& s)
{
std::swap(_str, s._str);
}
它们的特点是:参数为右值引用,函数体内部通过交换别人的指针到自己手上,实现高效的资源转移。
引用折叠
看到以下代码:
template <class T>
void func(T&& t)
{
cout << "T&& 右值引用" << endl;
}
template <class T>
void func(const T& t)
{
cout << "const T& const左值引用" << endl;
}
int main()
{
int a = 5;
func(a);//左值
func(move(a));//右值
return 0;
}
程序输出结果如下:
T&& 右值引用
T&& 右值引用
C++在模板中推出了引用折叠
,也叫做万能引用
,规则如下:
T& &&
推演为T&
T&& &&
推演为T&&
如果你希望当参数为左值引用和右值引用的时候,函数的功能是一样的,你就可以只写一个函数:
template <class T>
void func(T&& t)
{
}
此时,参数T&&
就已经是一个引用折叠了。现在我们来调用这个函数:
int a = 5;
func(a);
func(move(a));
第一次传参,func(a);,模板参数T的类型为int&,但是参数类型为int& &&,此时根据折叠引用规则:int& &&等于int&
第二次传参,func(move(a));,模板参数T的类型为int&&,但是参数类型为int&& &&,此时根据折叠引用规则:int&& &&等于int&&
我们刚才的模板,如果作用于int
类型,就可以推演出四套函数重载:
void func(int&){};
void func(const int&){};
void func(int&&){};
void func(const int&){};
完美转发
看到以下代码:
void fuc1(int& rri)
{
cout << "func1 左值引用" << endl;
}
void fuc1(int&& rri)
{
cout << "func1 右值引用" << endl;
}
int main()
{
int i = 5;
int&& rri = move(i);
fuc1(rri);
return 0;
}
输出结果:
func1 左值引用
右值引用后,右值引用指向的对象是右值属性,但是引用本身是左值属性
再来看到一个案例:
void func2(int& x)
{
cout << "func2 左值引用" << endl;
}
void func2(int&& x)
{
cout << "func2 右值引用" << endl;
}
template <class T>
void fuc1(T&& t)
{
func2(t);
}
int main()
{
int i = 5;
fuc1(i);//左值
fuc1(move(i));//右值
return 0;
}
由于在func1
中,我们经过了折叠引用这一步,T&&
这个参数类型是不确定的。
如果
T&&
是右值的话,传参后t
会变成左值,那么我们可以对其进行move
操作
如果T&&
是左值的话,传参后t
还是左值,我们无需对其进行操作
这个地方就不能粗暴的进行move
了,不然会把原本就是左值的参数,给move
成右值。为了解决这个情况,C++提供了一个函数模板forward
,称为完美转发
,其可以识别到参数的左右值类型,从而将其转化为原来的值。
我们只需要在引用折叠中这样进行调用:
template <class T>
void fuc1(T&& t)
{
func2(forward<T>(t));
}
在forward
的模板参数中传入引用折叠的模板参数T
,那么forward<T>
就可以根据t
的类型自动返回其原始的左右值属性了。
原文地址:https://blog.csdn.net/m0_61088872/article/details/140376022
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!