C++右值引用

右值引用是C++11中非常重要的特性之一,右值引用的意义通常解释为两大作用:移动语义和完美转发。本文仅从使用性的角度,通过例子来阐述右值引用。
右值,简单来讲,不可寻址的变量即为右值,如函数返回时构建的临时变量/对象,常量,Lamba。不可寻址并不是表示没有地址,事实上(本人理解)很大程度是因为这些变量没有名字/引用名,一旦这些右值可以被名字/引用名索引到,我们还是可以获取他们的地址的。因此右值引用作为对右值的引用,它是可以被寻址的!
右值引用的表示方式为&&,如

int&& i=10;

如果需要将对象的内容(该对象包含指向堆内存的指针成员变量)拷贝给一个新对象,这在c++11之前,需要复写掉C++默认的拷贝构造函数,实现堆内存拷贝操作。但是如果新对象仅仅是想复用老对象的堆内存,此时,如果仅仅将老对象的指针赋值给新对象的指针,老对象析构会对该指针释放一次内存,此时新对象继续被使用,而新对象的指针指向了一个被释放的内存!因此老对象绝对不能被析构,这是不合逻辑的。
C++11新增了移动构造函数,解决了以上问题。
如下代码时一段简单的C++拷贝构造函数,移动构造函数的示例代码。前文已经说过,函数的返回时构建的临时对象时右值。但A没有实现移动构造函数时,虽然函数返回的是右值,但A a = A::GetInstance();调用的是拷贝构造函数。而当A实现了移动构造函数时,由于函数返回的临时变量是右值,所以调用移动构造函数。

class A
{
public:
    A()
    {
        std::cout << "Constructor" << std::endl;
    }

    A(const A& src)
    {
        std::cout << "Copy Constructor" << std::endl;
    }

    //A(const A&& src)
    //{
    //    std::cout << "Move Constructor" << std::endl;
    //}

    ~A()
    {
        free(m_str);
        std::cout << "Destructor" << std::endl;
    }

    static A GetInstance()
    {
        A a;
        return a;
    }

    char* m_str;
};

int main()
{
    A a = A::GetInstance();
    return 0;
}
/*************************output******************/
Constructor
Copy Constructor//如果取消Move Constructor注释,优先调用移动构造,这里会输出“Move Constructor”
Destructor
Destructor

如果对C++的函数return实现稍微有点了解的话,这里有一个非常有趣的问题:为什么这里会分别构造和析构两次对象而不是三次?即为什么不是输出如下的结果?

Constructor
Copy Constructor
Destructor
Copy Constructor
Destructor
Destructor

因为C++在返回一个较大数据类型时(一般是结构体和类),需要额外开辟一块临时内存作为中转。对于返回类对象来讲,则可以理解为需要构建一个临时对象,因此必然(此处存疑)调用(copy)构造函数。即整个返回过程可以作如下理解:
这里写图片描述
而实际上,从逻辑上来讲,这个临时的中转对象是不必要的。因此,一般的编译器都会智能地讲函数内地对象直接copy给main中地a。从而避免了使用临时变量!更甚者,当对象实在函数结尾处构建时(return A())编译器将直接在main中a的内存处构建A对象,这样只调用了一次构造和析构函数,如下图所示。这种技术叫返回值优化(RVO)。一般编译器如G++默认是打开的。
这里写图片描述

当类A内存在堆内存的指针时,如果仅仅进行浅拷贝,如下,运行时出错(因为堆内存释放了两次)
class A
{
public:
    A()
    {
        m_str = (char*)malloc(sizeof(char) * 5);
        strncpy_s(m_str, 5, "1234", 5);
        std::cout << "Constructor" << std::endl;
    }

    A(const A& src)
    {
        m_str = src.m_str;
        std::cout << "Copy Constructor" << std::endl;
    }

    //A(const A&& src)
    //{
    //    std::cout << "Move Constructor" << std::endl;
    //}

    ~A()
    {
        free(m_str);
        std::cout << "Destructor" << std::endl;
    }

    static A GetInstance()
    {
        A a;
        return a;
    }

    char* m_str;
};

int main()
{
    A a = A::GetInstance();
    return 0;
}
/*************************output******************/
Constructor
Copy Constructor//如果取消Move Constructor注释,优先调用移动构造,这里会输出“Move Constructor”
Destructor
在第二次析构的时候,由于重复释放同一块内存,运行出错!因此拷贝构造函数必须重新分配一块内存,然后将内容拷贝过去

因此需要实现深拷贝,如下

class A
{
public:
    A()
    {
        m_str = (char*)malloc(sizeof(char) * 5);
        strncpy_s(m_str, 5, "1234", 5);
        std::cout << "Constructor" << std::endl;
    }

    A(const A& src)
    {
        m_str = (char*)malloc(sizeof(char) * 5);
        strncpy_s(m_str, 5, src.m_str, 5);
        std::cout << "Copy Constructor" << std::endl;
    }

    //A(const A&& src)
    //{
    //    std::cout << "Move Constructor" << std::endl;
    //}

    ~A()
    {
        free(m_str);
        std::cout << "Destructor" << std::endl;
    }

    static A GetInstance()
    {
        A a;
        return a;
    }

    char* m_str;
};

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

而如果所要拷贝对象是一个临时对象(右值)时,由于临时对象的堆内存是无用的,一个很好的方案是将临时对象的堆内存直接移交给目标对象而不是进行堆内存复制。此时,实现A的移动构造函数即可。可以看到,临时对象内的指针直接赋值给目标对象内的指针,接着临时对象的指针被修改为指向NULL,临时对象析构时析构NULL,原堆内存继续存在。

class A
{
public:
    A()
    {
        m_str = (char*)malloc(sizeof(char) * 5);
        strncpy_s(m_str, 5, "1234", 5);
        std::cout << "Constructor" << std::endl;
    }

    A(const A& src)
    {
        m_str = (char*)malloc(sizeof(char) * 5);
        strncpy_s(m_str, 5, src.m_str, 5);
        std::cout << "Copy Constructor" << std::endl;
    }

    A(A&& src)//注意这里因为需要改动src的内容,不能为const
    {
        m_str = src.m_str;
        src.m_str = nullptr;
        std::cout << "Move Constructor" << std::endl;
    }

    ~A()
    {
        free(m_str);
        std::cout << "Destructor" << std::endl;
    }

    static A GetInstance()
    {
        A a;
        return a;
    }

    char* m_str;
};

int main()
{
    A a = A::GetInstance();
    return 0;
}
/*************************output******************/
Constructor
Move Constructor
Destructor //GetInstance()内部对象a的析构,此时该对象的m_str=nullptr
Destructor //释放main()中对象a的m_str,注意这块内存实际上是由GetInstance()申请的。从而避免了拷贝!

以上代码并没有关闭RVO,但调用的仍然是移动构造函数。以此可以推断,函数GetInstance()内的栈内对象A a;是在return给main后才被析构的。按理函数应该先销栈啊?不知道是怎么实现的??

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值