C++中的右值引用

对右值引用的用法介绍

前言

C++98中提出了引用的概念,引用即别名,引用变量与其引用实体公共同一块内存空间,而引用的底层是通过指针来实现的,因此使用引用,可以提高程序的可读性。

右值引用的最大作用:我们要转移数据时需要进行拷贝,太消耗资源,代价很大,有时候这些对象是临时对象就是所说的"将亡值",这些临时对象马上就要销毁了,我们能不能不复制,直接把资源拿过来,下面介绍做法。


一、右值引用的概念

为了提高程序运行效率,C++11中引入了右值引用,右值引用也是别名,但其只能对右值引用。

这里区别一下左值和右值的不同;左值可以进行修改,左值通常是变量,比如

cpp 复制代码
int x=10;
x=11;

x就是一个左值,可以进行改变,

但是对于右值,通常是常量,比如10就是一个常量,右值也可以是表达式或者函数返回值(临时对象),

cpp 复制代码
右值引用

int&& a=10;

左值与右值是C语言中的概念,但C标准并没有给出严格的区分方式,一般认为:可以放在=左边的,或者能够取地址的称为左值,只能放在=右边的,或者不能取地址的称为右值,但是也不一定完全正确。

因此关于左值与右值的区分不是很好区分,一般认为:

1.普通类型的变量,因为有名字,可以取地址,都认为是左值。

  1. const修饰的常量,不可修改,只读类型的,理论应该按照右值对待,但因为其可以取地址(如果只是const类型常量的定义,编译器不给其开辟空间,如果对该常量取地址时,编译器才为其开辟空间),C++11认为其是左值。

3.如果表达式的运行结果是一个临时变量或者对象,认为是右值。

4.如果表达式运行结果或单个变量是一个引用则认为是左值。

总结:

1.不能简单地通过能否放在=左侧右侧或者取地址来判断左值或者右值,要根据表达式结果或变量的性质判断,比如上述:c常量

2.能得到引用的表达式一定能够作为引用,否则就用常引用。

C++11对右值进行了严格的区分:C语言中的纯右值,比如:a+b, 100

将亡值。比如:表达式的中间结果、函数按照值的方式进行返回。

注:左值引用不能引用右值,但是const左值引用可以,
普通引用只能引用左值,不能引用右值,const引用既可引用左值,也可引用右值。
C++11中右值引用:只能引用右值,一般情况不能直接引用左值。

问题:既然C++98中的const类型引用左值和右值都可以引用,那为什么C++11还要复杂的提出右值引用呢?

二、值的形式返回对象的缺陷

如果一个类中涉及到资源管理,用户必须显式提供拷贝构造、赋值运算符重载以及析构函数,否则编译器将会自动生成一个默认的,如果遇到拷贝对象或者对象之间相互赋值,就会出错。

比如在对一个数进行函数调用时,按照值返回,必须床造一个临时对象,临时对象进行返回,又创建一个临时空间,然后进行构造,好了之后又进行销毁临时对象,这样创建又销毁,对空间就是一种浪费,且会造成程序的效率降低。

所以我们引出移动语义,将资源直接转移到另一个对象中。

三、移动语义

C++11提出了移动语义概念,即:将一个对象中资源移动到另一个对象中的方式,可以有效缓解该问题。

cpp 复制代码
String (const String& s)
{
    cout<<"String(const String&s)-深拷贝-效率低"<<endl;
    _str=new char[strlen(s._str)+1];
     strcpy(_str,s._str);
}

这是一个深拷贝,需要创建一个临时对象,然后进行拷贝传值,会影响效率;且对空间有消耗。

cpp 复制代码
String(String&&s)
   :_str(nullptr)
{
      cout<<"String(String&&s)-移动拷贝-效率高"<<endl;
      swap(_str,s._str);
}

这是右值引用,也就是直接将要销毁的值直接交换给新的空间,不用麻烦创建临时对象进行拷贝了;同时避免了拷贝构造整个过程,只需要创建一块堆内存即可,既省了空间,又大大提高程序运行的效率。

注:1.移动构造函数的参数千万不能设置成const类型的右值引用,因为资源无法转移而导致移动语义失效。

2.在C++11中,编译器会为类默认生成一个移动构造,该移动构造为浅拷贝,因此当类中涉及到资源管理时,用户必须显式定义自己的移动构造。

显式构造一个移动构造:

cpp 复制代码
String(String&& s)
    : _str(s._str)
{
    s._str = nullptr;
}

移动构造的一个例子

cpp 复制代码
String s3= s1+=s2;//拷贝构造
String s4=s1+s2;//移动构造

现实中不可避免存在传值返回的场景,传值返回拷贝返回对象的临时对象,

若vector只实现参数为const左值引用深拷贝,那么下面的代价就很大。

vector(const vector<T>& v)->深拷贝

但是若使用了右值引用的移动拷贝,那么这里的效率就会很高;

vector(vector<T> && v)->移动拷贝

注:右值引用本身没有多大的意义,只是右值引用实现的移动拷贝和移动赋值有用

接收函数和传值返回对象的场景,可以提高效率;

四、右值引用左值

按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?因为:有些场景下,可能真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值。C++11中,std::move()函数位于头文件中,该函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义。

cpp 复制代码
template<class _Ty>
inline typename remove_reference<_Ty>::type&& move(_Ty&& _Arg) _NOEXCEPT
{
    // forward _Arg as movable
    return ((typename remove_reference<_Ty>::type&&)_Arg);
}

注意:

1.被转化的左值,其生命周期并没有随着左值的转化而改变,即std::move转化的左值变量lvalue不会被销毁。

  1. STL中也有另一个move函数,就是将一个范围中的元素搬移到另一个位置。
cpp 复制代码
int main()
{
  String s1("hello world");
  String s2(move(s1));
  String s3(s2);
  return 0;
}

注意:以上代码是move函数的经典的误用,因为move将s1转化为右值后,在实现s2的拷贝时就会使用移动构造,此时s1的资源就被转移到s2中,s1就成为了无效的字符串。

学习一下emplace_back

这是push_back的插入

cpp 复制代码
vector<string,string> v;
v.push_back(make_pair("左值","右值"));

而emplace_back模版可变参数的特点

cpp 复制代码
v.emplace_back("右值","左值");

五、完美转发

完美转发是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数

所谓完美:函数模板在向其他函数传递自身形参时,如果相应实参是左值,它就应该被转发为左值;如果=应实参是右值,它就应该被转发为右值。这样做是为了保留在其他函数针对转发而来的参数的左右值属性进行不同处理(比如参数为左值时实施拷贝语义;参数为右值时实施移动义)。

C++11通过forward函数来实现完美转发

cpp 复制代码
void Fun(int &x){cout << "lvalue ref" << endl;}
void Fun(int &&x){cout << "rvalue ref" << endl;}
void Fun(const int &x){cout << "const lvalue ref" << endl;}
void Fun(const int &&x){cout << "const rvalue ref" << endl;}
template<typename T>
void PerfectForward(T &&t){Fun(std::forward<T>(t));}
int main()
{
 PerfectForward(10); // rvalue ref
 int a;
 PerfectForward(a); // lvalue ref
 PerfectForward(std::move(a)); // rvalue ref
 const int b = 8;
 PerfectForward(b); // const lvalue ref
 PerfectForward(std::move(b)); // const rvalue ref
 return 0;
}

PerfectForward为转发的模板函数,Fun为实际目标函数,完美转发是目标函数总希望将参数按照传递给转发函数的实际类型转给目标函数,而不产生额外的开销,就好像转发者不存在一样。

所以使用forward可以解决这个问题。

六、右值引用的作用

C++11中右值引用主要有以下作用:

1.实现移动语义(移动构造与移动赋值)

2.给中间临时变量取别名:

3.实现完美转发

cpp 复制代码
int main()
{
 string s1("hello");
 string s2(" world");
 string s3 = s1 + s2;// s3是用s1和s2拼接完成之后的结果拷贝构造的新对象
 stirng&& s4 = s1 + s2; // s4就是s1和s2拼接完成之后结果的别名
 return 0;
}

总结

对右值引用进行介绍,右值引用最大的作用在于移动构造,减少空间的消耗,提高效率,。

在开发中的作用是使用右值引用,进行对临时对象或者即将消耗的对象(将亡值)进行转移,而不是拷贝构造。

相关推荐
SilentSamsara1 小时前
迭代器协议:`__iter__` / `__next__` 的完整执行流程
开发语言·人工智能·python·算法·机器学习
平凡但不平庸的码农1 小时前
Go Channel详解
开发语言·后端·golang
laomocoder1 小时前
Project-Nexus-WAN-跨公网Agent对话
开发语言·php
子安柠1 小时前
深入理解 Go 语言文件操作:从基础到最佳实践
开发语言·后端·golang
代码中介商1 小时前
C++文件流操作全解析
开发语言·c++
Forget_85501 小时前
RHEL——Kubernetes容器编排平台(二)
java·开发语言
Achou.Wang1 小时前
go语言中使用等待组(waitgroups)和内存屏障(barriers)进行同步
开发语言·后端·golang
MATLAB代码顾问1 小时前
【智能优化】鹈鹕优化算法(POA)原理与Python实现
开发语言·python·算法
lsx2024061 小时前
C 标准库 - `<stdio.h>`
开发语言