左值/右值/左值引用/右值引用/万能引用/移动语义/完美转发,你真的懂了吗?

C++整了这么一套花活,最终的目的就是:减少拷贝,节省内存,提高运行效率。

在看本文之前,如果你能清晰区分下面前两个函数的异同,并知道为啥第三个函数调用非法,你就可以继续阅读本文了。

void F1(int a){a++;}

void F2(int& a){a++;}

F2(100);  //compile error

一、左值和右值

左值就是一个数据的表达式,这个表达式指向一块内存,并且允许我们对这块内存取地址,可以这么认为,可以取地址的对象或变量就是左值,左值通常可以被赋值,但被 const 修饰后的左值,不能给它赋值,但是仍然可以取它的地址。

右值不能取地址,也不能被赋值,即:右值不能出现在赋值表达式的左边。

根据上述定义,下面的a、p、b都是左值

int a = 3;

int* p = &a;

const int b = 2;

问:*p是左值还是右值。*p,又叫解引用,就是解释引用,或者说直接去寻找指针所指的地址里面的内容,于是这个表达式指向一块内存,内存的地址就是p的值,因此解引用也是个左值。

下面这些都是右值

double x = 1.3, y = 3.8;

 10; // 字面常量

x + y; // 表达式返回值

fmin(x, y);

问下述函数是左值还是右值?

int a=10;

int& Incerease(int &b)

{

b++;

return b;

}

我们可以写出如下代码,因此int& Incerease(int &b)是个左值。

Incerease(a)=50;

二、左值引用和右值引用

记住一句话:无论左值引用还是右值引用,都是给对象取别名

左值引用:

左值引用就是对左值的引用,给左值取别名,左值引用使用一个&来表示。主要作用是避免对象拷贝。

  1. 左值引用只能引用左值,不能直接引用右值。 
  2. 但是const左值引用既可以引用左值,也可以引用右值 

int& ra = a;

int*& rp = p;

int& r = *p;

const int& rb = b;

const double& d1=10.7

右值引用

右值引用就是对右值的引用,给右值取别名,具体表示就是在变量类型名称后加&&。

int&& n1=10;

double&& d1=x+y;

double&& d2=fmin(x,y);

右值引用引用右值,会使右值被存储到特定的位置。
也就是说,右值引用变量其实是左值,可以对它取地址和赋值(const右值引用变量可以取地址但不可以赋值,因为 const 在起作用)。
当然,取地址是指取变量空间的地址(右值是不能取地址的)

  1. 右值引用只能引用右值,不能直接引用左值。 
  2. 但是右值引用可以引用被move的左值。 

无论是左值引用变量还是右值引用变量,都是左值。

三、使用左值引用和右值引用的意义
1. 不使用左值引用

看下图,如果在函数Func1中不使用引用的话,当str被传入Func1的时候,在Func内部首先产生一次拷贝构造,外部的str拷贝给函数内部的str,数字1所示。

在str被处理以后,该str将被传回到函数外部,于是在数字2处也产生一次拷贝构造,内部的str传出到外部的一个临时变量。

该临时变量也被调用一次拷贝构造,给外部的str1赋值,数字3,于是我们可以看到,一共产生了三次调用拷贝构造函数(不考虑编译器优化的情况)

2. 使用左值引用

如果传参使用左值引用,那么只有两次拷贝构造函数的调用

如果函数返回一个左值引用,那么就只有一次拷贝构造函数的调用

2. 使用右值引用

在讲右值引用之前,先来讲一下一个类中都有哪些函数。在C++ 11之前,编译器通常会为一个类准备4个函数,如果你不为这个类准备的话:

默认无参构造函数

拷贝构造函数

赋值运算符重载

析构函数。

在c++11之后,有增加了两个函数:移动构造函数和移动赋值运算符。这两个函数都比相应的拷贝构造函数和赋值运算符重载要高效的多。

看下面这个例子:

在Func1被调用的时候,1处有一个右值引用,右值引用只是给右值(str)起一个名字(str2),在新名字上的操作等同于对原始右值的操作。参见4处的两个对象的地址完全一致。

2处有一个右值返回,函数返回一个右值引用的对象(str2, 也就是str),是个右值。这个对象要赋值给一个新的对象str1,于是右值移动构造函数被调用。在3处有一个移动构造函数。当然新的对象str1除了把str的内容借过来,已经与str是两个完全不同的两个对象了,于是我们看到str内容为空了。

move 本意为 "移动",但该函数并不能移动任何数据,它的功能很简单,就是将某个左值强制转化为右值,以便可以通过右值引用使用该值。看下面的例子,r是右值引用,就是x的别名,我们可以通过操作r来改变x。

我们知道,非 const 右值引用只能操作右值,程序执行结果中产生的临时对象(例如函数返回值、lambda 表达式等)既无名称也无法获取其存储地址,所以属于右值。当类中同时包含拷贝构造函数和移动构造函数时,如果使用临时对象初始化当前类的对象,编译器会优先调用移动构造函数来完成此操作。只有当类中没有合适的移动构造函数时,编译器才会退而求其次,调用拷贝构造函数

四、万能引用

在此之前我们需要知道什么是万能引用

确定类型的 && 表示右值引用(比如:int&& ,string&&),
函数模板中的 && 不表示右值引用,而是万能引用模板类型必须通过推断才能确定,其接收左值后会被推导为左值引用,接收右值后会被推导为右值引用。

注意区分右值引用和万能引用:下面的函数的 T&& 并不是万能引用,因为 T 的类型在模板实例化时已经确定。

template<typename T>
class A
{
    void func(T&& t);  // 模板实例化时T的类型已经确定,调用函数时T是一个确定类型,所以这里是右值引用
};

让我们通过下面的程序来认识万能引用:

template<typename T>
void F1(T& t1)
{
    cout<<t1<<endl;
}

int main()
{
    int n=1;
    F1(n);  // good
    F1(1);  //error
    return 0;   
}

嫩F1(1)导致编译错误。为了修正这个错误,我们重载另一个函数:

template<typename T>
void F1(T& t1)
{
    cout<<t1<<endl;
}
template<typename T>
void F1(T&& t1)
{
    cout<<t1<<endl;
}
int main()
{
    int n=1;
    F1(n);  // good
    F1(1);  //error
    return 0;   
}

能否把两个函数合二为一,可以:

使用T&&类型的形参既能绑定右值,又能绑定左值。

但是注意了:只有发生类型推导的时候,T&&才表示万能引用;否则,表示右值引用

五、完美转发

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

因此,为了实现完美转发,除了使用万能引用之外,我们还要用到std::forward(C++11),它在传参的过程中保留对象的原生类型属性

这样右值引用在传递过程中就能够保持右值的属性

继续利用前面的一个例子,我们把函数的返回类型不使用右值引用,这次返回一个临时变量,这个临时变量会被拷贝构造函数赋值给str1. str2是一个右值引用,但str2本身是一个左值。因此return str2 就会通过拷贝构造str2,返回一个临时变量。这也就是我们说的右值引用失去了右值的属性,成为了一个左值,如何能保持str2的右值属性呢?这就引入了完美转发。

下面看如何使用完美转发来保持str2的右值属性,

总结:

右值引用和左值引用类似,都是为一个量(对象)起了个别名,操作右值引用和左值引用等价于操作被引用量本身。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值