1 C++左值右值

在看介绍C++11标准书籍的时候,经常在书中看到"左值"和"右值"的概念,这两个东西理解起来比较抽象。但是在C++11之后变得非常重要,其也是理解std::move()std::forward等新语义的基础。

int a = 100;

上述示例代码中,a为左值,100为右值。

1.1 C++左值和右值的基本概念

在C++中,我们需要用一个名字来表示内存中的一些东西(比如new一个数组时为数组起一个名字),这个名字我们经常称为"对象"。从上文中,对象表示一块连续的存储区域,而左值则表示指向对象的一条表达式。按照字面意思,左值表示可以用在赋值运算符左侧的东西(但是其实不是所有的左值都能用在赋值运算符的左侧,左值也有可能是一个常量),而为了补充和完善左值的含义,对应的右值诞生了,右值表示不能作为左值的值,比如函数返回的临时值,数值常量等。

1.2 判断左值还是右值的标准

当考虑对象的寻址,拷贝、移动等操作时,有两种属性非常关键:

  • 有身份:在程序中有对象的名字、指向该对象的指针,或者该对象的引用,我们就能判断两个对象是否相等或者对象的值是否发生了改变;
  • 可移动:能把对象的值移动出来(比如,我们能把它的值移动到其他的地方,剩下的对象处于合法但未指定的状态,与拷贝是有差别的);

从上面的两个属性进行判断,一个经典的左值是有身份的但是不能移动(因为我们可能会在移动后仍然使用它),一个经典的右值是允许执行移出操作的对像。

所以说:

  • 左值通常是可寻址的变量,生命周期具有持久性,不能被移动;
  • 而右值一般是不可寻址的变量,或者表达式中求值过程中创建的无名临时对象,其生命周期比较短暂,但是可以被移动;

1.3 左值引用和右值引用

左值引用和右值引用的概念如下:

  • 左值引用:引用一个对象;
  • 右值引用:必须绑定到右值的引用,C++11中右值引用可以实现移动语义,通过&&获得右值引用;
int a = 100;
int &y = x;

上述代码中,a为左值,100为右值,y为左值引用。

int a = 100;
int &y = x;
int &z = x * 100; // 错误,x * 100是一个右值,不能被引用
const int &z1 = x * 100; // 正确,可以将一个const引用绑定到一个右值上

int &&z2 = x * 100; // 正确,z2为右值引用
int &&z3 = x; // 错误,x是一个左值,不能作为右值引用

右值引用和相关的移动语义是C++11标准中引入的最强大的特性之一,通过std::move()可以避免无谓的复制,提高程序性能。

为什么这么说呢?

在C++11中,声明一个类时,除了构造函数和析构函数之外,我们经常还会声明拷贝构造函数、拷贝赋值函数、移动构造函数和移动赋值函数,比如看以下代码

#include <iostream>

class Vector
{
public:
    // 构造函数
    Vector(int length) :m_pData(new double[length]),m_DataSize(length)
    {
        for (int i = 0; i < length; ++i)
        {
            m_pData[i] = 0.0;
        }
    }

    // 析构函数
    virtual ~Vector()
    {
        delete[] m_pData;
    }

    // 拷贝构造函数
    Vector(const Vector& vec):m_pData(new double[m_DataSize]),m_DataSize(vec.m_DataSize)
    {
        for (int i = 0; i < m_DataSize; ++i)
        {
            m_pData[i] = vec.m_pData[i];
        }
    }

    // 拷贝赋值函数
    Vector& operator=(const Vector& vec)
    {
        double* temp = new double[vec.m_DataSize];
        for (int i = 0; i < vec.m_DataSize; ++i)
        {
            temp[i] = vec.m_pData[i];
        }

        delete[] m_pData; // 删除旧数据
        m_pData = temp;
        m_DataSize = vec.m_DataSize;

        return *this;
    }

    // 移动构造函数
    Vector(Vector&& vec):m_pData(vec.m_pData),m_DataSize(vec.m_DataSize)
    {
        vec.m_pData = nullptr;
        vec.m_DataSize = 0;
    }

    // 移动赋值函数
    Vector& operator=(Vector&& vec)
    {
        m_pData = vec.m_pData;
        m_DataSize = vec.m_DataSize;

        vec.m_pData = nullptr;
        vec.m_DataSize = 0;
    }


private:
    double* m_pData;
    int m_DataSize;
};

int main()
{
    return 0;
}

从上述代码中,我们在处理移动构造和移动赋值函数时,直接将右值引用对象vec的值直接赋值给了新的对象的值,这使得新对象可以直接使用右值引用对象内部已经分配好的这块内存空间,而不需要再像拷贝构造函数一样需要去重新分配内存空间。然后将临时值对象成员变量m_pData的值置为 nullptr 空指针,这样做的目的是为了防止临时值对象的析构函数在执行时将这块已经分配的内存区域清除,因为这快内存区域实际上已经被新对象中的成员变量m_pData直接使用了。而我们之前提到的“移动语义”便可以简单地理解为将临时对象内已经分配好的内存区域直接“偷”过来使用这样一个过程。

总的来讲,使用基于右值引用的移动语义,可以使我们在复制具有大块内存空间的对象时可以直接使用原对象已经分配好的内存空间进而省去重新分配内存空间的过程,与拷贝操作相比,可以提升程序的执行效率。

参考连接