【C++11】右值引用和移动语义

简介: 目录右值引用和移动语义1.1 左值引用和右值引用1.1.1 左值和左值引用1.1.2 右值和右值引用1.2 左值引用与右值引用比较1.3 左值引用使用场景和意义1.4 右值引用的使用场景和意义1.5 右值引用引用左值及其一些更深入的使用场景分析1.6 完美转发1.6.1 万能引用1.6.2 完美转发

目录

右值引用和移动语义

1.1 左值引用和右值引用

1.1.1 左值和左值引用

1.1.2 右值和右值引用

1.2 左值引用与右值引用比较

1.3 左值引用使用场景和意义

1.4 右值引用的使用场景和意义

1.5 右值引用引用左值及其一些更深入的使用场景分析

1.6 完美转发

1.6.1 万能引用

1.6.2 完美转发


补充:C++11中STL的一些变化

C++11在string中增加了一些函数

字符串转其他类型

image.png

其他类型转字符串

image.png

右值引用和移动语义

1.1 左值引用和右值引用

传统的C++语法中就有引用的语法,,而C++11中新增了的右值引用语法特性,所以前面篇章所提到的引用都是叫做左值引用。无论左值引用还是右值引用,都是给对象取别名

1.1.1 左值和左值引用

什么是左值?

左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址 + 可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边

定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。

什么是左值引用?

左值引用(&)就是给左值的引用,给左值取别名

例如:

intmain()
{
// 以下的p、b、c、*p都是左值int*p=newint(0);
intb=1;
constintc=2;
// 以下几个是对上面左值的左值引用int*&rp=p;
int&rb=b;
constint&rc=c;
int&pvalue=*p;
return0;
}

1.1.2 右值和右值引用

什么是右值?

右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边右值不能取地址

什么是右值引用?

右值引用(&&)就是对右值的引用,给右值取别名

例如:

intmain()
{
doublex=1.1, y=2.2;
// 以下几个都是常见的右值10;
x+y;
fmin(x, y);
// 以下几个都是对右值的右值引用int&&rr1=10;
double&&rr2=x+y;
double&&rr3=fmin(x, y);
// 这里编译会报错:error C2106: “=”: 左操作数必须为左值,右值不能出现在赋值符号的左边//10 = 1;//x + y = 1;//fmin(x, y) = 1;return0;
}
  • 右值本质就是一个临时变量或常量值,比如代码中的10就是常量值,表达式x+y 和函数fmin的返回值就是临时变量,这些都叫做右值。
  • 这些临时变量和常量值并没有被实际存储起来,这也就是为什么右值不能被取地址的原因,因为只有被存储起来后才有地址。
  • 但需要注意的是,这里说函数的返回值是右值,指的是传值返回的函数,因为传值返回的函数在返回对象时返回的是对象的拷贝,这个拷贝出来的对象就是一个临时变量

需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用,这个了解一下即可,实际中右值引用的使用场景并不在于此,这个特性也不重要

intmain()
{
doublex=1.1, y=2.2;
int&&rr1=10;
constdouble&&rr2=x+y;
rr1=20;
rr2=5.5; // 报错return0;
}

编译报错

image.png

1.2 左值引用与右值引用比较

无论左值引用还是右值引用,两者都是给对象取别名

左值引用总结:

  1. 左值引用使用的符号是 “ & ”
  2. 左值引用只能引用左值,不能引用右值
  3. 是const左值引用既可引用左值,也可引用右值

测试代码

intmain()
{
// 左值引用只能引用左值,不能引用右值。inta=10;
int&ra1=a; // ra为a的别名//int& ra2 = 10; // 编译失败,因为10是右值//const左值引用既可引用左值,也可引用右值。constint&ra3=10;
constint&ra4=a;
return0;
}

注意:

  • 左值引用不能引用右值,这里涉及权限放大的问题,权限只能平移或缩小,不能放大,右值是不能被修改的(只读),而左值引用是可以对变量进行读取和修改(可读可写),比如,int& ra2 = 10,10是一个右值,且是一个常量,10(只读),左值引用ra2(可读可写),10 赋值给 ra2,10的权限被放大了
  • 但是 const左值引用(只读)可以引用右值(只读),因为 const左值引用能够保证被引用的数据不会被修改,权限可以平行

右值引用总结:

  1. 右值引用使用的符号是 “ && ”
  2. 右值引用只能右值,不能引用左值
  3. 但是右值引用可以 move以后的左值

测试代码

intmain()
{
// 右值引用只能右值,不能引用左值。int&&r1=10;
// error C2440: “初始化”: 无法从“int”转换为“int &&”: 无法将左值绑定到右值引用inta=10;
int&&r2=a;
// 右值引用可以引用 move以后的左值int&&r3=std::move(a);
return0;
}

编译结果

image.png

注:用move函数是C++11标准提供的一个函数,被move后的左值能够赋值给右值引, move以后的左值就变成了右值

1.3 左值引用使用场景和意义

在谈这个之前,先谈一下左值引用的缺陷,左值引用对于出了作用域就销毁的对象无法使用,这是左值引用的缺陷,而 C++11提出的右值引用就是用来解决左值引用的缺陷

左值引用的使用场景:

  1. 左值引用做参数,作用:a、做输出型参数  b、大对象传参提高效率(左值引用做参数,能够完全避免传参时不必要的拷贝操作
  1. 左值引用做返回值,作用:a、输出型返回对象,调用者可以修改返回对象  b、减少拷贝,提高效率(左值引用做返回值,并不能完全避免函数返回对象时不必要的拷贝操作)

左值引用的缺陷:

  • 左值引用对于出了作用域就销毁的对象无法使用,比如函数返回的对象是一个局部变量,该变量出了函数作用域就被销毁了,这种情况下不能用左值引用作为返回值,只能以传值的方式返回,这就是左值引用的短板

下面进行测试,测试代码是一个简化版的string,拷贝构造函数和赋值运算符重载函数当中打印了一条提示语句,当调用这两个函数时可以让我们知道知道调用了几次

namespacefy{
classstring    {
public:
//构造函数string(constchar*str="")
        {
_size=strlen(str);//字符串大小_capacity=_size;//构造时,容量大小默认与字符串大小相同_str=newchar[_capacity+1];//为字符串开辟空间(多开一个用于存放'\0')strcpy(_str, str);//将C字符串拷贝到已开好的空间        }
//析构函数~string()
        {
delete[] _str; //释放_str指向的空间_str=nullptr;
_size=_capacity=0;
        }
//拷贝构造 -- 现代写法string(conststring&s)
            :_str(nullptr)
            , _size(0)
            ,_capacity(0)
        {
cout<<"string(const string& s) -- 深拷贝"<<endl;
stringtmp(s._str);//复用构造函数,构造 tmp对象swap(tmp);//交换        }
//赋值重载 -- 现代写法1string&operator=(conststring&s)
        {
cout<<"string& operator=(const string& s) -- 深拷贝"<<endl;
if (this==&s)//检查自我赋值            {
return*this;
            }
stringtmp(s);//复用拷贝构造函数,用s拷贝构造出对象tmpswap(tmp);
return*this;//返回左值,目的是为了支持连续赋值        }
typedefchar*iterator;//迭代器iteratorbegin()
        {
return_str;
        }
iteratorend()
        {
return_str+_size;
        }
//更改容量大小voidreserve(size_tn)
        {
if (n>_capacity)//n大于现有容量,增容,n小于现有容量则不改变容量            {
char*tmp=newchar[n+1];
strcpy(tmp, _str);
delete[] _str;
_str=tmp;
_capacity=n;//更新容量            }
        }
//交换两个字符串voidswap(string&s)
        {
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
        }
constchar*c_str()const        {
return_str;
        }
//尾插一个字符voidpush_back(charc)
        {
if (_size==_capacity)//判断是否需要增容            {
size_tnewCapacity=_capacity==0?4 : _capacity*2;
reserve(newCapacity);
            }
_str[_size] =c;
++_size;
_str[_size] ='\0';
        }
//+= 一个字符string&operator+=(charc)
        {
push_back(c);
return*this;
        }
private:
char*_str;
size_t_size;
size_t_capacity;
    };
}

下面我们模拟实现一个 int版本的 to_string函数,这个 to_string函数就不能使用左值引用返回,因为to_string函数返回的是一个局部变量

namespacefy{
//模拟实现一个int版本的to_string函数stringto_string(intvalue)
    {
boolflag=true;
if (value<0)
        {
flag=false;
value=0-value;
        }
fy::stringstr;
while (value>0)
        {
intx=value%10;
value/=10;
str+= ('0'+x);
        }
if (flag==false)
        {
str+='-';
        }
std::reverse(str.begin(), str.end());
returnstr;
    }
}

此时调用to_string函数返回时,就一定会调用string的拷贝构造函数,因为以传值传参的方式一定会调用拷贝构造函数

intmain()
{
fy::strings1=fy::to_string(11111);
return0;
}

运行结果

image.png

C++11提出右值引用就是为了解决左值引用的这个缺陷:左值引用对于出了作用域就销毁的对象无法使用

下面说一下编译器优化的问题

实际当一个函数在返回局部对象时,会先用这个局部对象拷贝构造出一个临时对象,然后再用这个临时对象来拷贝构造我们接收返回值的对象

image.png

对于深拷贝的类来说这里就会进行两次深拷贝,所以大部分编译器为了提高效率都对这种情况进行了优化这种连续调用构造函数的场景通常会被优化成一次,对于比较老的编译器可能是拷贝两次(没有进行优化)

image.png

注意:这种优化只会在连续构造的时候,编译器才进行优化

image.png

下面开始讲解右值引用和移动语义

1.4 右值引用的使用场景和意义

右值引用解决上述问题的方式就是:增加移动构造和移动赋值方法

移动构造

移动构造是一个构造函数,该构造函数的参数是右值引用类型的,移动构造本质就是将传入右值的资源窃取过来,占为己有,这样就避免了进行深拷贝,所以它叫做移动构造,就是窃取别人的资源来构造自己

比如,在上面的模拟实现的string加上移动构造,函数要做的就是调用swap函数将传入右值的资源窃取过来

// 移动构造string(string&&s)
    :_str(nullptr)
    , _size(0)
    , _capacity(0)
{
cout<<"string(string&& s) -- 移动语义"<<endl;//更明显观察是否调用了该函数swap(s);//与右值的资源进行直接交换,不进行深拷贝}

给string类增加移动构造后,对于返回局部string对象的这类函数,在返回string对象时就会调用移动构造进行资源的移动,而不会再调用拷贝构造函数进行深拷贝

intmain()
{
fy::strings1=fy::to_string(11111);
return0;
}

运行结果

image.png

进行调试查看

image.png

image.png

从调试结果可以明显看出,s1 把 str的资源窃取过来了

注意:

  • to_string当中返回的局部string对象是一个左值,但由于该 string对象在当前函数调用结束后就会立即被销毁,这种即将被消耗的值叫做 “将亡值”,比如匿名对象也可以叫做“将亡值”。
  • 既然 “将亡值” 马上就要被销毁了,那还不如把它的资源转移给别人用,因此编译器在识别这种“将亡值”时会将其识别为右值,这样就可以匹配到参数类型为右值引用的移动构造函数

移动构造和拷贝构造的区别

  • C++11之前,是没有增加移动构造的,由于拷贝构造采用的是 const左值引用接收参数,因此无论拷贝构造对象时传入的是左值还是右值,都会调用拷贝构造函数。
  • 在C++11之后,增加了移动构造,由于移动构造采用的是右值引用接收参数,因此如果拷贝构造对象时传入的是右值,那么就会调用移动构造函数(最匹配原则)。

比如,上面模拟实现是 string的拷贝构造函数做的是深拷贝,而移动构造函数中只需要调用 swap函数进行资源的转移,因此调用移动构造的代价比调用拷贝构造的代价小

还有一点要注意:

编译器对拷贝的优化,对这里的移动构造依旧生效

  • 如果编译器不优化这里应该调用两次移动构造,第一次调用移动构造用返回的局部string对象构造出一个临时对象,第二次调用移动构造用这个临时对象构造接收返回值的对象
  • 而经过编译器优化后,最终这两次移动构造就被优化成了一次,也就是直接将返回的局部string对象的资源移动给了接收返回值的对象

注意:这种优化只会在连续构造的时候,编译器才进行优化

intmain()
{
//fy::string s1 = fy::to_string(11111);//不是连续构造,编译器不进行优化fy::strings1;
s1=fy::to_string(11111);
return0;
}

运行结果,这里依旧存在赋值重载的深拷贝的原因是我们没有实现移动赋值,又调用了一次左值引用拷贝构造的深拷贝原因是:赋值重载里面调用了左值的拷贝构造函数

image.png

这里仍然需要再调用一次赋值运算符重载函数进行深拷贝,因此深拷贝的类不仅需要实现移动构造,还需要实现移动赋值

下面实现移动赋值

移动赋值

移动赋值是一个赋值运算符重载函数,该函数的参数是右值引用类型的,移动赋值也是将传入右值的资源窃取过来,占为己有,这样就避免了深拷贝,所以它叫移动赋值,就是窃取别人的资源来赋值给自己

比如,在上面的模拟实现的string加上移动赋值,函数要做的就是调用swap函数将传入右值的资源窃取过来

// 移动赋值string&operator=(string&&s)
{
cout<<"string& operator=(string&& s) -- 移动语义"<<endl;//使更明显观察是否调用了该函数swap(s);//与右值的资源进行直接交换,不进行深拷贝return*this;//支持连续赋值}

给string增加移动构造和移动赋值以后,进行赋值时不会存在深拷贝的问题

intmain()
{
fy::strings1;
s1=fy::to_string(11111);
return0;
}

运行结果,不存在深拷贝了

image.png

右值引用的移动赋值和左值引用的赋值重载的区别:

  • 在C++11之前,没有增加移动赋值,由于原有 operator=函数 采用的是 const左值引用接收参数,因此无论赋值时传入的是左值还是右值,都会调用原有的operator=函数。
  • 在C++11之后,增加了移动赋值,由于移动赋值采用的是右值引用接收参数,因此如果赋值时传入的是右值,那么就会调用移动赋值函数(最匹配原则)。
  • 因此无论赋值时传入的是左值还是右值,都会调用原有的operator=函数。
  • 在C++11之后,增加了移动赋值,由于移动赋值采用的是右值引用接收参数,因此如果赋值时传入的是右值,那么就会调用移动赋值函数(最匹配原则)。

下面看看 STL容器,比如string

image.png

image.png

其他容器也是如此,都增加了移动构造和移动赋值这两个函数

1.5 右值引用引用左值及其一些更深入的使用场景分析

按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?

因为:有些场景下,可能真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值。

C++11中,std::move()函数位于 头文件中,该函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义

move 函数的定义如下

template<class_Ty>inlinetypenameremove_reference<_Ty>::type&&move(_Ty&&_Arg) _NOEXCEPT{
// forward _Arg as movablereturn ((typenameremove_reference<_Ty>::type&&)_Arg);
}

测试代码,依旧是上面模拟实现的string

intmain()
{
fy::strings1("hello world");
// 这里s1是左值,调用的是拷贝构造fy::strings2(s1);
// 这里我们把s1 move处理以后, 会被当成右值,调用移动构造// 但是这里要注意,一般是不要这样用的,因为我们会发现s1的资源被转移给了s3,s1被置空了。fy::strings3(std::move(s1));
return0;
}

运行结果

image.png

调试查看,发现s1的资源被转移给了s3,s1被置空了

image.png

image.png

一个左值被move以后,它的资源可能就被转移给别人了,因此要慎用move

1.6 完美转发

1.6.1 万能引用

在模板中的 && 不代表右值引用,而是万能引用,其既能接收左值又能接收右值

右值引用和万能引用的区别就是,右值引用需要是确定的类型,而万能引用是根据传入的参数的类型进行推导,如果传入的参数是一个左值,那么这里的 t 就是左值引用,如果传入的参数是一个右值,那么这里的 t 就是右值引用

 比如:

template<classT>voidPerfectForward(T&&t)// 这里的 && 是万能引用{
//...}

下面重载了四个Func函数,这四个Func函数的参数类型分别是左值引用、const左值引用、右值引用和const右值引用。在主函数中调用PerfectForward函数时分别传入左值、右值、const左值和 const右值,在PerfectForward函数中再调用Func函数

代码如下:

voidFun(int&x) 
{ 
cout<<"左值引用"<<endl; 
}
voidFun(constint&x) 
{ 
cout<<"const 左值引用"<<endl; 
}
voidFun(int&&x) 
{ 
cout<<"右值引用"<<endl; 
}
voidFun(constint&&x) 
{ 
cout<<"const 右值引用"<<endl; 
}
// 模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。// 模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,// 但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,// 我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发template<typenameT>voidPerfectForward(T&&t)
{
Fun(t);//调用Fun函数}
intmain()
{
PerfectForward(10); // 右值inta;
PerfectForward(a); // 左值PerfectForward(std::move(a)); // 右值constintb=8;
PerfectForward(b); // const 左值PerfectForward(std::move(b)); // const 右值return0;
}

运行结果

image.png

由于 PerfectForward函数的参数类型是万能引用,因此既可以接收左值也可以接收右值,而我们在 PerfectForward函数中调用 Func函数,就是希望调用 PerfectForward函数时传入左值、右值、const左值、const右值,能够匹配到对应版版本的Func函数

但实际调用 PerfectForward函数时传入左值和右值,最终都匹配到了左值引用版本的Func函数,调用PerfectForward函数时传入const左值和const右值,最终都匹配到了const左值引用版本的Func函数

这个结果并不是我们所预期的,根本原因就是右值被引用后会导致右值被存储到特定位置,这时这个右值可以被取到地址,并且可以被修改,所以在 PerfectForward函数中调用Func函数时会将t识别成左值

也就是说,右值经过一次参数传递后其属性会退化成左值,我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要使用完美转发

1.6.2 完美转发

完美转发的作用是:完美转发在传参的过程中保留对象原生类型属性

使用的函数是 std::forward,在传参时需要调用 forward函数

image.png

该函数使用如下 :

template<typenameT>voidPerfectForward(T&&t)
{
Fun(std::forward<T>(t));//调用Fun函数}

经过完美转发后,调用PerfectForward函数时传入的是右值就会匹配到右值引用版本的Func函数,传入的是const右值就会匹配到const右值引用版本的Func函数,这就是完美转发的价值

运行结果,符号我们的预期

image.png

----------------我是分割线---------------

文章到这里就结束了,下一篇即将更新

相关文章
|
2月前
|
存储 安全 C++
【C++11】右值引用
C++11引入的右值引用(rvalue references)是现代C++的重要特性,允许更高效地处理临时对象,避免不必要的拷贝,提升性能。右值引用与移动语义(move semantics)和完美转发(perfect forwarding)紧密相关,通过移动构造函数和移动赋值运算符,实现了资源的直接转移,提高了大对象和动态资源管理的效率。同时,完美转发技术通过模板参数完美地转发函数参数,保持参数的原始类型,进一步优化了代码性能。
40 2
|
4月前
|
编译器 C++
C++ 11新特性之右值引用
C++ 11新特性之右值引用
57 1
|
8月前
|
编译器 C语言 C++
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值(中)
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值
45 1
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值(中)
|
8月前
|
存储 安全 C语言
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值(上)
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值
40 2
|
8月前
|
编译器 C语言 C++
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值(下)
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值
49 1
|
7月前
|
编译器 C++ 开发者
C++一分钟之-右值引用与完美转发
【6月更文挑战第25天】C++11引入的右值引用和完美转发增强了资源管理和模板灵活性。右值引用(`&&`)用于绑定临时对象,支持移动语义,减少拷贝。移动构造和赋值允许有效“窃取”资源。完美转发通过`std::forward`保持参数原样传递,适用于通用模板。常见问题包括误解右值引用只能绑定临时对象,误用`std::forward`,忽视`noexcept`和过度使用`std::move`。高效技巧涉及利用右值引用优化容器操作,使用完美转发构造函数和创建通用工厂函数。掌握这些特性能提升代码效率和泛型编程能力。
59 0
|
2月前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
59 2
|
2月前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
110 5
|
2月前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
109 4
|
2月前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
140 4