C++11中的右值引用

1.左值与右值

1.1 什么是左值

定义:左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址并且可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。

特点:

  • 可以被赋值,例如:a = 5; 中的 a 是左值。
  • 通常指向内存中的持久对象,比如全局变量、局部变量或动态分配的对象。
  • 可以出现在赋值操作符的左侧或右侧。
int main()
{
    // 以下的p、b、c、*p都是左值
    int *p = new int(0);
    int b = 1;
    const int c = 2;
    // 以下几个是对上面左值的左值引用
    int *&rp = p;
    int &rb = b;
    const int &rc = c;
    int &pvalue = *p;
    return 0;
}

1.2 什么是右值

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

特点:

  • 不能被赋值,不能出现在赋值表达式的左侧。
  • 通常是字面量、表达式的求值结果,或者是即将销毁的对象。
  • 只能出现在赋值表达式的右侧,或者作为函数调用的参数等。
int main()
{
    double x = 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;
    return 0;
}

2 左值引用和右值引用的比较

2.1 左值引用和右值引用的概念

根据之前所学的内容,我们可以理解左值引用就是给左值取别名,那么右值引用就是给右值取别名。那么我们还有几点需要注意:

1. 左值引用只能引用左值,不能引用右值。
2. 但是const左值引用既可引用左值,也可引用右值。(因为右值通常是不可以改变的值,所以用const左值引用是可以的)

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

3. 右值引用只能右值,不能引用左值。
4. 但是右值引用可以move以后的左值。

move()可以使左值右值相互转化

int main()
{
    // 右值引用只能右值,不能引用左值。
    int &&r1 = 10;

    // error C2440: “初始化”: 无法从“int”转换为“int &&”
    // message : 无法将左值绑定到右值引用
    int a = 10;
    int &&r2 = a;
    // 右值引用可以引用move以后的左值
    int &&r3 = std::move(a);
    return 0;
}

2.2 右值引用存在的必要性

根据 2.1 的内容我们知道,只有左值可以修改。那如果我们想修改右值怎么办?所以我们才需要右值引用。

换个角度看:左值引用的意义在于其一,函数传参,当实参传给形参时,可以减少拷贝;其二,函数传返回值时,只要是出了作用域还存在的对象,那么就可以减少拷贝。

但是左值引用没有彻底解决的问题是当函数传返回值时,如果返回值是出了作用域销毁的(出了作用域不存在的),那还需要多次的拷贝构造,导致消耗较大,效率降低。因此我们需要右值引用。

例如:

A& f() 
{
	A a;
	return a;
}

当返回对象a的地址时,a作为在栈上的临时对象,作用域已经结束,被析构。这样当外界再对这个地址进行访问时,就会出现问题。而右值引用的出现就是为了解决这个问题。 当返回值为右值引用时,返回的临时变量中的内存被居为己用,这样仍保持有效性,也避免了拷贝。

3 移动构造与移动赋值

下面先展示正常构造的过程(函数返回值出了作用域就被销毁)

根据分析,str走拷贝构造产生临时变量,临时变量再次拷贝构造给主函数中的ret,一共经历两次拷贝构造。然而,编译器会对此进行优化,将两个连续的拷贝构造优化为一个拷贝构造,直接跳过中间的临时变量:

 

但对于自定义类型,即使存在这样的优化方式,仍有可能占据很大的空间,因而我们有移动构造。

 

右值分为纯右值(字面常量)和将亡值("将亡值"是指一个指针变量,它曾经指向一个动态分配的内存块,但该内存块已经被释放。一旦内存被释放,这个指针就不再指向一个有效的内存地址,但指针本身仍然保留着之前指向的地址。如果尝试通过这个将亡指针访问或修改内存,就可能导致未定义行为)。当构造传左值,就走拷贝构造,当构造传右值,就走移动构造。对于左值,我们后续还要使用,所以只能进行深拷贝,完成拷贝构造。

对于复制拷贝,则是相同的道理:

 

 

这里运行后调用了一次移动构造和一次移动赋值。如果是用一个已经存在的对象接收,编译器就没办法优化了。mj::to_string函数中会先用str生成构造生成一个临时对象,但是我们可以看到,编译器很聪明的在这里把str识别成了右值,调用了移动构造。然后在把这个临时对象做为mj::to_string函数调用的返回值赋值给ret,这里调用的移动赋值。(直接资源交换)

4 万能引用和完美转发

4.1 万能引用

首先我们需要明确,万能引用不是一种引用类型,而是代表要么是左值引用要么是右值引用。而万能引用主要用于下面两种情况:

1.用于模板

template<typename T>
void  tempFun(T&& t) {}  //模板类型的这种用法 T&&是万能引用最常见的使用场合

2.auto推断类型

auto&& var2 = var1;  //auto这种需要推断类型的地方

其次,万能引用能够接收左值或右值,返回左值引用或右值引用

template<typename T>
void tempFun(T&& t)
{
	t = 40;
	cout << t << endl;
}
 
int main()
{
	int x = 19;
	tempFun(x); //T为int, t为int & ,即左值引用, 函数模板中对t的改动为影像x的值
	tempFun(30);//T为int, t为int&&,即右值引用
 
	int &&r = 100;
	tempFun(r);   //虽然r绑定到一个右值,但r变量本身是个左值(因为他出现在=的左边)
	return 0;
}

此外,auto使用万能引用具体来说,auto推断类型是在编译器就确定了,不是在运行期。

	int v1 = 100;
	auto && r1 = v1;  //auto为int,r1的类型为int &
	auto&& r2 = 200;  //auto为int,r2的类型为int &&

 我们还需要区分万能引用和右值引用,在模版实例化时会确认T的类型,则T&&不是万能引用

template<typename T>
class A
{
	void fun(T && t); //这里是右值引用
};

template<typename T>
class A
{
	template<typename U>
	void fun(U && u); //这里是万能引用
};

最后我们将其与C++11之前的版本进行比较,老版本C++没有右值引用的概念,当然也没有万能引用的概念,如果我要实现既能接收左值又能接收右值,需要用const重载。

template<typename T>
void tempTest(T & t) {}   //接收左值
 
template<typename T>
void tempTest(const T & t) {}  //虽然能同时匹配左值和右值,但会给左值强制const

4.2 完美转发

突破编程_C++_C++11新特性(完美转发 forward)_c++完美转发-CSDN博客

建议参考这一篇,非常详细。 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值