C++的右值引用和move语法

C++11有一个非常重要的新功能,就是推出了右值引用的概念。那么什么是右值引用,它的意义在什么地方,本文将对此予以解释。

为什么要有右值引用

我们先来看一段代码:

class A
{
public:
    A() {
    std::cout << "construct A" << std::endl;
    val = new int(1); }
    ~A() 
    { 
    delete val; 
    std::cout << "destruct A" << std::endl;
    }
    A(const A&a) {
    std::cout << "copy construct A" << std::endl;
    val = new int(*a.val);
    }
    int * val = nullptr;
};

int main()
{
    A a;
    A b = a;
    return 0;
}

执行上述的代码,得到的输出为:

construct A
copy construct A
destruct A
destruct A

最开始的构造函数是在创建a,接下来从a赋值给b是调用了b的拷贝构造函数。最后程序结束的时候调用了a和b的析构函数。
问题就在于中间的这个拷贝构造函数:对于存在堆中分配的成员的类(比如类A中有一个val值需要在创建时new出来)来说,它的拷贝构造函数一般需要根据传入的值从堆中为其分配内存,如果是一个比较大的对象的话,就会在分配内存上花费比较多的时间。
这只是一个简单的案例,考虑这样一种情况:

A getA()
{
    return A();
}
A a = getA();

上述代码如果没有经过返回值优化的话,会在getA函数return时返回一个临时变量,该临时变量会调用拷贝构造函数从return后面创建的A对象中得到值,同时A对象本身销毁;然后a会再次调用拷贝构造函数从临时对象处赋值,同时也销毁临时对象。这样其实一共调用了一次构造函数(return A()),两次拷贝构造函数(函数内创建的A对象赋值给临时变量,临时变量赋值给a),两次析构函数(临时变量和函数内创建的A对象),每次构造和析构的过程都要new和delete资源,这对于计算来说是极大的浪费。

移动构造函数和右值引用

为此,C++11提出了移动构造函数和右值引用来解决这个问题。
我们看看运用了移动构造函数的A:

class A
{
public:
    A() {
        std::cout << "construct A" << std::endl;
        val = new int(1); }
    ~A() 
    { 
    delete val; 
    std::cout << "destruct A" << std::endl;
    }
    A(const A&a) {
    std::cout << "copy construct A" << std::endl;
    val = new int(*a.val);
    }
    //注意这个移动构造函数
    A(A&&a) :val(a.val)
    {
    a.val = nullptr;
    }
    int * val = nullptr;
};

上述代码中的A(A&&a)被称作移动构造函数,它的参数A&&a就是一个右值引用。该函数的作用是将传入的参数a的val的地址直接移植到自己的val中,相当于自己“偷”来了a.val的内存,并且需要在函数内将a.val的值置为空(否则会有两个对象对该内存持有引用,则析构的时候就会delete两次)。这么一来,就省略了拷贝构造函数分配内存的过程,大大优化了性能。
关于左值和右值,可以简单的理解为左值是赋值时等号左边的变量,它可以通过&运算符取到内存;而右值则包括纯右值和将亡值。纯右值类似于a=1+1这个代码等号右边的表达式,不能取到它的内存;将亡值就是和右值引用有关的变量,之所以叫做“将亡值”是因为它一般很快就会被移动拷贝到其他的对象中,命不久矣。

move函数

那么如何产生一个右值引用呢?C++提供了move函数,它可以将一个左值强行转化为右值引用,相当于干了以下的事情:

T&& t1 = static_cast<T&&>(t);

那么,就可以通过move来实现移动构造的过程了:

A a;
std::cout << a.val << std::endl;
A && b = std::move(a);
*b.val = 2;
std::cout << b.val << std::endl;
A c(std::move(b));
std::cout << c.val << std::endl;

通过观察输出,可以发现a、b、c的val都是同一个地址,并且在生成c的过程中,不用再调用c的拷贝构造函数,而是调用移动构造函数,直接将b.val的内存赋值给c,省去了分配内存的过程。
另外,需要注意的一点是,如果A类没有实现移动构造函数,那么由b构造c的时候仍然调用c的拷贝构造函数,这也是一种相当安全的做法————移动不成,至少还可以拷贝。

原文链接: https://www.cnblogs.com/wickedpriest/p/12662746.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍;

也有高质量的技术群,里面有嵌入式、搜广推等BAT大佬

    C++的右值引用和move语法

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/340653

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年3月2日 上午12:42
下一篇 2023年3月2日 上午12:43

相关推荐