右值和右值引用

本文介绍了C++中的左值和右值的概念,强调了它们与内存、生命周期的关系,并通过示例说明了如何区分两者。接着讨论了左值引用和右值引用,指出常量左值引用可以引用左值和右值,而右值引用则用于可修改的右值引用。文章还阐述了移动语义的重要性,通过移动构造函数实现性能优化,减少了不必要的对象复制。
摘要由CSDN通过智能技术生成

本篇文章主要探讨的是右值和右值引用。但是会以和左值对比的方式探讨。

一、左值和右值

很多初学者对左值和右值的理解是从字面上的---表达式等号左侧的值为左值,等号右侧的值为右值。

但这是一种片面不准确的说法。

在C++中左值一般指 一个指向特定内存的具有名称的值,这个值具有名称和相对稳定的内存地址,生命周期相对较长。相对的,右值不具有名称,且不指向稳定的内存地址,生命周期相对短暂,通常是暂时性的。

基于上诉特征,我们可以用取址符判定左值和右值,左值可用取址符取址,而对右值取址编译器会报错。

下面是一些左值和右值的例子

int a = 6; //a是左值 6是右值
int get_value()
{
    return a;
}

void set_value(int v)
{
    a = v;
}

int main()
{
    int b = get_value(); //b是左值 但是get_value()是右值,&get_value()无法编译成功
    int c = a + b; //a,b,c都是左值 但是a+b计算结果是右值
    set_value(2); //常量2是右值,但是set_value的形参v是左值
    c++; //c虽然是左值,但是c++是右值,因为后置++操作会先生成一个c的临时复制,再对c进行++操作。
         //如果&c++则是对c的临时复制的取址,无法编译通过
    ++c; //++c是左值,前置++不会生成c的临时拷贝
         //&++c则是c先自增,然后对c取址
}

我们需要仔细理解几个点:

函数的返回值是不具名的,生命周期很短的值,所以它是右值;

形参都是左值,因为其是具名的,在其函数体内具有十分稳定的内存空间和生命周期;

二、左值引用和右值引用

  1. 左值引用

在C++开发中,我们经常使用左值引用。

int a = 1;
int &b = a; //b为左值引用,可以引用非常量左值

上述的b为非常量引用,只能引用非常量左值,常量左值和右值都不可以被其引用。

const int ca = 1;
int &b = ca; //error,ca为常量左值,不能被非常量引用b引用
int &b = 1;  //error,1为右值,不能被非常量引用b引用

但是,常量引用,既可以引用左值也可以引用右值;即可以引用常量也可以引用非常量。

int a = 1;
const int ca = 1;
const int &cb = a;  //ok,常量左值cb引用非常量左值a
const int &cb = ca; //ok,常量左值cb引用常量左值ca
const int &cb = 1;  //ok,常量左值cb引用右值1。

在第5行,const int &cb = 1 这里不是赋值,和const int cb = 1不一样。前者是引用,右值1的生命周期被延长;后者是赋值,结束后右值1将被释放。

2、右值引用

虽然上面介绍的常量左值引用好像适用性很广,能够满足我们的各种引用需要。但是常量左值有一个很明显的弊端,就是其常量性。被其引用的值将无法修改。见下列代码

struct A{
    int val = 5;
};
A getA(){
    return A();
}
int main(){
    const A &ca = getA(); //getA()的返回值为右值,只能用被常量引用ca引用
    ca.val = 666;         //error, 由于ca为常量引用,此句会编译失败
}

所以,我们有时需要既能引用右值,又能修改被引用的对象的办法——右值引用

同样是上面的例子,如果使用右值引用,即可达到目的

A &&ra = getA(); //ok,ra为右值引用,引用getA的返回值(右值)
ra.val = 666;    //ok

定义右值引用需要在类型后添加 &&,可以和左值引用(在类型后添加&)一起记忆。

右值引用是专为引用右值而来,用其引用右值就像左值引用引用左值一样方便和正确。

三、针对右值的性能优化-移动语义

经过第一节对左值和右值的介绍。我们可以认识到在我们日常的C++编码中,需要经常处理的各种类型常量(除字符串常量外)、函数返回值,一些不具名的表达式(例如a+b)等都是右值,但是由于右值作为一种临时的对象,具有较短的生命周期,往往在使用后就被销毁,并且还伴随这一些对象的构造。这会引发一个性能问题,对于一个对性能有着要求的项目而言,我们需要尽量减少不必要的对象销毁和建立。

例如如下代码

class A {
public:
    A() :data_(new char[size_]) {
        std::cout << "A被构造" << std::endl;
    }
    ~A() {}
    A(const A& cp) :data_(new char[size_]) {
        memcpy(data_, cp.data_, size_); //进行深拷贝
        std::cout << "A的拷贝构造被调用" << std::endl;
    }
private:
    char *data_;
    int size_ = 4096;
};
A getA() { //2:返回值进行一次拷贝构造
    return A(); //1:A()为右值
}

int main() {
    A a(getA()); // 3:getA()为右值,又进行了一次拷贝构造
    system("pause");
}

上述代码,将会输出如下结果(注意,需要加上编译参数-fno-elide-constructors关闭编译优化):

A被构造
A的拷贝构造被调用
A的拷贝构造被调用

在这个过程中,将会有3次对data_的开辟,然而我们知道这是没有必要的。

通过上面对右值引用的介绍,我们知道,如果我们使用右值引用,而不使用拷贝构造(参数为常量左值引用),我们就能将上诉代码中1处A()这个右值的生命周期延长,一直使用,而不产生不必要的拷贝和开辟data_的操作。所以我们更改下上诉代码,增加一个构造函数

A(A&& mv)
{ 
    data_ = mv.data_;
    mv.data_ = nullptr; 
    std::cout << "移动构造被调用" << std::endl;
}

加入此构造函数之后,输出结果将变成

A被构造
移动构造被调用
移动构造被调用

因为后两次构造传入的参数都是右值,编译器会优先使用最新加入的这个构造函数。而调用此构造函数,将不会再次开辟data_的空间,达到了我们的目的。而这种构造函数,我们称为移动构造函数,它的形参为右值引用类型。

使用移动构造,我们能对我们的项目进行合理有效的优化。事实上,我们的编译器已经对我们的代码进行了一些优化。

【参考&致谢】

《现代C++语言核心特性解析》---谢丙堃

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值