C++ 左值引用和右值引用

左值和右值

在以前,大家可能听说过“在赋值符号左边的就是左值,在赋值符号右边的就是右值”,这是错误的说法,例如将一个变量的赋值给另一个变量时,该表达式的左右两个值都是左值,左值引用其实就是给左值起一个别名

平常使用的引用传参(int&)不需要进行拷贝,按值传参需要进行拷贝(实参->形参),使用引用传参可以提高性能,在C++11引入了右值引用之后,这种引用方式就被称为左值引用

左值:表示可以获取地址的表达式,它能出现在赋值语句的左边,对该表达式进行赋值。

int a = 11;

const常量虽然可以取得地址,需要在声明时进行初始化,但是不可改变值

const int b = 11;

b = 12;  //报错

右值:表示无法获取地址的表达式,有字面常量函数返回值表达式返回值lambda表达式表达式返回值等(都是临时对象)。无法获取地址,所以不可以在表达式的左边,所以不能通过右值进行赋值,但不表示其不可改变,当定义了右值的右值引用时就可以更改右值。左值引用其实就是给右值起一个别名,使得右值可以进行赋值改变

int fun()

{

    int a = 1;

    return a;

}

fun() = 2;   //报错

int 'hello' = 'hi';   //报错

总结:所以区分左值与右值的规则是:“左值可以取地址,右值不能取地址”,因为左值一般是被放在内存地址空间中,有明确的存储地址;而右值一般可能是计算中产生的中间值、临时变量,也可能是被保存在寄存器上的一些值,总的来讲就是,右值并没有被保存在内存地址空间中,也就无法取地址。

左值引用和右值引用

左值引用:传统的C++中引用被称为左值引用,作用是起别名(绑定)

在类型的右边加一个&即可

int a = 10;

int& b = b;

右值引用:C++11中增加了右值引用,右值引用关联到右值时,右值引用指向右值存储的地址。右值引用可以获取该地址表示临时对象存储位置

在类型的右边加俩两个&

int&& a = 10;

左值引用的引用范围

普通的左值引用能引用左值,但是加上const后就变成万能引用,既可以引用左值又可以引用右值

const int& a = 10

因为右值都是不能被修改的值,左值引用加上const后也不能修改了,否则权限放大,因此会报错。但左值引用加上const后权限一致,此时是权限平移,就可以引用右值了

右值引用的引用范围

右值引用只能引用右值,但是左值可以使用move()函数,将左值转换为右值,就可以对其进行引用

int a = 10;

int& b = a;

int&& c = move(b); 

右值引用的作用

解决传值返回拷贝时的局部变量出作用域生命周期结束的问题

传值返回需要进行两次拷贝,有的情况下,在编译器优化后只需要进行一次拷贝,省略了临时拷贝,而传引用返回则不需要进行拷贝

当使用以下左值引用时,const将左值引用变为万能引用,即可以接收左值也可以接收右值,并且节省了传参以及返回值的拷贝

const int& func(const int& a)

{

    return a;

}

但是当返回值是函数内的局部变量时,因为函数返回后出了作用域,生命周期就结束了,局部变量的生命周期也就结束了,此时如果在调用函数时接收了函数返回值,那么将出现非法寻址。

此时不能使用传引用返回,只能使用传值返回,如果返回的是一个包含大量数据的容器,那么拷贝的消耗很高。

const int& func(const int& a)

{

    int b = a*2;

    return b;

}

解决方式一:使用堆内存,用传指针的方式传递数据,则需要考虑将堆内存回收的时机,否则将造成内存泄露

解决方式二:使用输出型参数来解决,遇到参数多且重载函数也多的情况,并不好使用

解决方式三:使用右值引用的方式

const int&& func(const int& a)

{

    int b = a*2;

    return b;

}

右值的分类

普通的右值称为纯右值

即将被销毁的右值称为将亡值

在右值引用中,如果某些右值是将亡值,可以将亡值与接收值及进行交换,因为是直接交换两个对象中的数据,所以不会进行拷贝。通过这种方式,就可以在数据量非常庞大的情况下提高效率。但是使用不当有可能造成变量的值被修改

int main()
{
    string s1("hello");
    string s2(move(s1));
    system("pause");
    return 0;
}

使用字符串常量"hello"构造了s1后,在构造s2时,s1被转为右值,用于初始化s2,因为输入参数是右值,所以调用了s2的移动拷贝构造,交换了s1和s2的值,导致s1的值被置空(因为原来s2就是空的)

所以使用右值引用移动构造的方法,只适用于将亡值,因为将亡值马上要被销毁释放,被交换了任何值都无影响

移动赋值同理,也是采用交换数据的方式进行赋值

有了右值后,返回值是左值时会被编译器识别为右值,然后进行移动拷贝,对拷贝过程进行了优化

stl容器的右值引用

很多容器的插入元素的接口函数都提供了右值引用的重载函数:

list::push_back

void push_back(value_type&& val);

vector::push_back

void push_back(value_type&& val);

map::insert

template<class P> pair<iterator,bool> insert(P&& val);

右值可以被右值引用间接修改

右值不能直接修改,但是右值被右值引用进行引用后,就可以被间接修改

int&& num = 10;

++num;

右值不能被修改是因为右值不能被取地址(或者说是没有地址),但是在右值被右值引用进行引用后,就被存储到栈上,有了地址且可以取得地址,因此就可以被修改了。也可以加上const关键字使得右值引用不可修改

在上面讲了可以通过右值引用,将将亡值的数据与接收对象的数据进行交换。但是将亡值是右值,不能取得地址,无法和接收对象的数据完成交换,所以需要使用右值引用,才可以取得地址,完成交换

要注意,虽然右值引用后右值可以被修改,但它修改的并不是对应的常量,而是在对应地址上存储的值。因为常量是不允许修改的

右值引用后的值被视为左值

int func(int&& num)

{

    list.push_back(num);

}

调用func函数,当实参为右值时调用形参为右值引用的func函数,此时形参num将被视为左值,调用list.push_back时,将不会调用移动拷贝,而调用深拷贝,影响效率

解决:

使用move(),将左值转为右值即可

void func(int&& num)

{

    list.push_back(move(num));

}

完美引用

如果函数需要重载参数是右值引用或左值引用两个版本的函数,可通过使用"完美引用"一个函数解决,但仅限于有模板的情况

template<class T>

void func(T&& num)

{

    list.push_back(move(num));

}

当使用模板时,既可以接收右值,又可以接收左值,其实在传入左值的时候会折叠一个&,相当于T&

完美转发

上文使用move保持num的右值特性,如果函数内要多次使用num,将会比较麻烦,而万能引用只是在传入时识别两种变量,但是在右值二次引用时依旧被视为左值。如果要传入的右值被二次使用时还需要保持右值特性,则需要使用"完美转发"

void func(int&& x)

{

    cout<<"右值引用"<<endl;

}

void func(int& x)

{

    cout<<"左值引用"<<endl;

}

template <class T>

void PerfectForward(T&& x)

{

    func(std::forward<T>(x));

}

使用std::forward<T>在传参的过程中保留对象原生类型的特性,也就是传入左值,第二次传递也是左值;传入右值,第二次传递也是右值

  • 30
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

qq_54881777

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值