作用

C++ 11引进右值引用,主要为了实现对象转移和完美转发。主要作用体现在避免对象之间无必要的拷贝,节省存储资源以及提高效率

什么是右值

在C++中表达式和变量要么是左值要么是右值,通常临时对象为右值。不可利用&取得内存地址的为右值。而左值在内存中有确定的存储地址。

int i = 0;          // 0 为右值   i为左值
int j = i + 1;      // i+1 为右值
int& test(); 
test() = 10;        // test()为左值
int test0();
int n = test0();     // test0() 为右值
int *p = &test0();   // 错误, p为右值不能取地址

右值引用

C++11 之前,右值不能被引用,仅能用常量引用绑定一个右值:

const int & test = 1;

上述中常量引用绑定的右值是无法改变的,但是其实右值是可以改变的:

struct _tagTest{
    int nTest;
    void Set(const int& t)
    {
        nTest = t;
    }

    const int& Get() const
    {
        return nTest;
    }
}

/*
_tagTest() 实例化一个右值(临时对象)
调用Set改变了这个右值
*/
_tagTest().Set(1).Get();

既然右值可以改变, 那么就有了右值引用。

符号标识

右值引用利用两个&进行标识,如:int && a;

void test(int & i)
{
    std::cout << "LValue:" << i << std::endl; 
}

void test(int && i)
{
    std::cout << "RValue:" << i << std::endl; 
}

int main()
{
    test(0);
    int a = 1;
    test(a);
}
/*输出:
    RValue: 0
    LValue: 1
*/

上述**test(&& i)**中将0传入后,将变成左值。这个临时变量在传递后,变成了命名对象。

void test(int & i)
{
    std::cout << "LValue:" << i << std::endl; 
}

void test(int && i)
{
    // 此处i 已经转变成左值
    test(i);
}

int main()
{
    test(0);
    int a = 1;
    test(1);
}
/*输出:
    LValue: 1
    LValue: 1
*/

转移语义

右值引用主要用来支撑转移语义。转移语义可以将资源 ( 堆,系统对象等 ) 从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,能够大幅度提高 C++ 应用程序的性能。临时对象的维护 ( 创建和销毁 ) 对性能有严重影响。

转移语义是和拷贝语义相对的,可以类比文件的剪切与拷贝,当我们将文件从一个目录拷贝到另一个目录时,速度比剪切慢很多

通过转移语义,临时对象中的资源能够转移其它的对象里。

如下,拷贝带来的消耗(极端例子,可优化):

int test(int param)
{
    std::cout << param << std::endl;
    return param;
}

int n = test(0)

分析以上代码:

  • 入参param进行一次拷贝
  • 返回param进行一次拷贝
  • n = test(0)进行一次拷贝

移动构造与移动赋值操作符

struct _tagTest {
    size_t nLen;
    char* pData;

    void IniData(const char* psz)
    {

        pData = new char[nLen + 1];
        memset(pData, 0, nLen + 1);
        memcpy(pData, psz, nLen);
    }

    //_tagTest() : nLen(0), pData(nullptr) { std::cout << "默认构造" << std::endl; }

    _tagTest(const char* psz) {
        nLen = strlen(psz);
        IniData(psz);

        std::cout << "重载构造" << std::endl;
    }

    ~_tagTest() {


        std::cout << "析构" << (pData? pData:"") << std::endl;
        if (pData != nullptr) {

            delete[]pData;
            pData = nullptr;
        }

        nLen = 0;

    }
    _tagTest(const _tagTest& inst)
    {
        nLen = inst.nLen;
        IniData(inst.pData);
        std::cout << "拷贝构造函数" << std::endl;
    }

    _tagTest(_tagTest&& inst)
    {
        nLen = inst.nLen;
        pData = inst.pData;
        inst.nLen = 0;
        inst.pData = nullptr;
        std::cout << "移动构造函数" << std::endl;
    }

    _tagTest& operator= (const _tagTest& inst)
    {
        if (this == &inst)
            return *this;

        nLen = inst.nLen;
        IniData(inst.pData);
        std::cout << "拷贝赋值" << std::endl;

        return *this;
    }

    _tagTest& operator= (_tagTest&& inst)
    {
        if (this == &inst)
            return *this;

        nLen = inst.nLen;
        pData = inst.pData;
        inst.nLen = 0;
        inst.pData = nullptr;
        std::cout << "移动赋值" << std::endl;

        return *this;
    }

};

int main()
{
    _tagTest test;
    test = _tagTest("test");
    std::vector<_tagTest> vecTest;
    vecTest.push_back(_tagTest("test"));
}

/*输出
移动赋值
移动构造函数
*/

库函数std::move的使用

显然编译器只对右值引用才调用移动构造函数以及移动复制函数,而几乎我们声明的命名对象都是左值引用。当你了解某个对象将不再使用又想嫁接给另一个变量触发移动构造或是赋值,那么可以使用std::move进行转换,将左值引用转换为右值引用。

示例程序:

void test(int & i)
{
    std::cout << "LValue:" << i << std::endl; 
}

void test(int && i)
{
    std::cout << "RValue:" << i << std::endl; 
}

int main()
{
    test(0);
    int a = 1;
    test(std::move(1));
}
/*输出
RValue: 0;
RValue: 1;
*/

本文Quenwaz版权所有,转载请说明出处