C++11之右值引用理解

tech2022-07-13  166

1,右值引用

编译代码的时候出现出现左值右值这样的错误,那么究竟什么是左值和右值呢?

通常可以使用一个值再表达式中的位置来判断是左值还是右值,

 

int  x =10;

int  y  =20;

int   sum  =  x+y;    sum就是左值  x+y  就是右值

 

还有一种就方法:可以取地址 有名字的就是左值,反之 不能取地址 ,没有名字的就是右值。 上面的sum 就可以取地址操作。而不能对x+y 取地址操作。

接下来我们在C++11中进一步定义:

C++ 11中  对右值分为了 :

1,  xvalue      2 ,prvalue 

对应的单次:

xvalue:  Expiring   Value   (中文:将亡值)

prvalue:  Pure  Right  Value(中文:纯右值)

当然翻译并不唯一,重点放到概念上,纯右值就是我们指的最常见的那种右值类型。

纯右值: 函数返回的临时变量,字面量值  , lambda 表达式 这些都是不能取地址的临时值类型,当然也可以将他们赋值给一个左值,继续使用。

将亡值:C++11版本中新新引入的跟右值引用相关的表达式类型,这样的表达式通常是要被移动的对象。std::move 返回右值引用。

注意:纯右值   将亡值 都是右值。

右值引用:顾名思义就是对右值的一个引用,通常来说由于右值本身并不具有实际的名字所以你也只能通过引用的这种方式呢来关联它们的存在。

在C++11中使用两个按位与符号  && 来表示一个右值引用类型。如下例子

int  && x =10;//这里的变量x 就代表一个 整型临时值的右值引用类型。

右值引用用在什么地方呢?

答:一般来说你可以通过右值引用来完成对一个将亡值的语义转移过程。通常来讲在C++11中我们会通过拷贝构造函数来完成类对象的赋值过程。

拷贝构造函数就是 :类对象的赋值过程。这个没有异议。基本语法操作符重载。如下代码:


class A{ public: A(size_t size ):size(size),array((int*) malloc(size)){ stu::cout << "constructor called." << std::endl; } ~A(){ free(array); } A(const A & a):size(a.szie){ array = (int*)malloc(a.szie); std::cout<<"normal copied,memory at: "<< array<<std::endl; } size_t size; int * array; }; int main(int argc,char ** argv) { auto x =A(100); //临时值赋值给变量x auto y =x; //以及x 被赋值给变量y时,会分别执行两次你在类A中定义的拷贝构造函数。而在这次拷贝构造函数中,每一次都会对类对象内的array数组重新进行内存分配的过程 //x: 一次 y:一次 }

 

为了完成语义转移的过程(即:我们其实是想要的y 指向x 的分配的。),你就需要C++中另外的一个名字为移动构造函数得成员函数类型。一段完整的C++代码是下面这样的,先从main函数分析:

class A{ public: A(size_t size ):size(size),array((int*) malloc(size)){ //构造函数 stu::cout << "constructor called." << std::endl; } ~A(){ free(array); } A(A && a):array(a.array),size(a.size){ //C++11 移动构造函数 a.array =nullptr; std::cout << "xvalue copied,memory at:"<< array<<std::endl; } A(const A & a):size(a.szie){ //拷贝构造函数 array = (int*)malloc(a.szie); std::cout<<"normal copied,memory at: "<< array<<std::endl; } size_t size; int * array; }; int main(int argc,char ** argv) { auto getTempA = [](size_t size =100)->A{ auto tmp =A(szie); std::count <<"Memory at:" << tmp.array <<std::endl; return tmp; } std::cout << std::is_rvalue_reference<decltype(getTempA())&&>::value << std::endl;//true; auto x = getTempA(1000); auto y = x; return 0; }

 1,我们首先编写了一个用于返回临时值的lambda函数 getTempA,通过这个函数可以返回一个类A的临时值对象。也就是一个右值,接下来我们使用了宏函数is_rvalue_reference,来判断前面的lambda函数返回的值是否是一个右值引用,调试发现这里呢证实了我们的结论 ,这个宏函数返回了true。

在接下来的两行代码中我们分别把之前右值赋值给了变量x,又把左值赋值给变量y。

我们去看看我们实现的移动构造函数,一共做了两件事情。

第一件事是直接把临时值对象,也就是右值引用对象A 内部的成员变量array的值 , 直接赋值给了新生成的对象array变量。

这就使得新对象可以直接使用临时值内部已经分配好的这块内存空间而不再像拷贝构造函数一样,需要去重新分配内存空间。

第二件事就是将临时值对象成员变量array的值置为nullptr也就是空指针,那么这样做的目的是什么?

这就是为了防止临时值对象的析构函数再执行时将这块已经分配的内存区域清除掉。因为这块内存区域实际上已经被新对象成员中的成员变量array直接使用了。

而我上面说的语义转移可以简单的理解为将临时对象内已经分配好的内存区域,直接偷过来使用的一个过程。

总体来讲使用基于右值引用的语义转移过程,可以使你在复制具有大块内存空间对象时可以直接使用原对象已经分配好的内存空间。进而省去重新分配内存空间的过程。因此在某种程度上来讲可以在一定条件下提升应用的整体运行效率。

 

省去重新分配内存空间过程。,可以在一定条件下提升应用的整体运行效率。

我来延时将一下,除此之外还要注意的是我前面说的介绍的C++标准特性是一方面,除此之外各大编译器厂商还会同时使用名为RVO和NRVO的编译器优化技术。

RVO NRVO

来对函数的对象返回值类型进行临时值上的优化因此呢,对于拷贝构造函数在代码中的实际调用次数可能在不同的编译器下会有这不同的表现。

最后我在提出一个问题:基于我上面介绍的左值 右值以及右值引用的概念 你可以猜想一下。

问题:为什么拷贝构造函数的参数会使用常量左值类型(const  T&)?或者说使用这个类型有什么样的好处呢?

最后我来总结一下在C++中:

左值 ==》 位于运算符左侧的值 ==》 可以被进行取地址操作(有名)

右值 =》 位于运算符右侧的值 =》 无法被取地址操作(无名)

 

在c++11中右值又被细分为纯右值和将亡值

比如常见的字面值,表达式产生的临时变量值灯均是纯右值。而将亡值 就代表资源能够被重新使用的对象。

将亡值 ==》 资源能够被重新使用的对象

常见的如:std::move函数的返回值或者返回值被标记为右值引用的函数等。,返回值被标记为右值引用(&&)

对于纯右值和将亡值在实际编码中你并不需要进行细致的区分在大多数情况下将他们统一看成右值来使用即可。

 

 

最新回复(0)