C++11--右值引用

1、左值、右值

  左值、右值是表达式的属性,单独一个变量也是表达式,称为变量表达式,它是最简单的表达式,没有运算符,只有一个运算对象。

  左值表达式即为返回左值的表达式,他们有:变量表达式、前++(前--)运算、*解引用运算、[]下标运算、返回左值引用类型的函数表达式。

  右值表达式即为返回右值的表达式,如:字面常量、非左值运算的运算表达式(如100 * i、i + n等表达式)、返回非左值引用类型的函数表达式。

  从以上可知左值持久,右值短暂,左值表达式表示的是一个对象的身份,而右值表达式表示的是对象的值,只有左值才能被赋值,如:*p = 1,++i = 0,ary[0] = 100。

2、左值引用

  我们原来学过使用&来定义引用类型,其实使用&定义的是左值引用,因为绑定的都是左值表达式,如:

int o = 5;
int& h = o; //o是变量(变量表达式)

int& e = ++o; //前++运算

int* p = &o;
int& f = *p;  //*解引用运算

int ary[10] = { 0 };
int& s = ary[0]; //[]下标运算

int& func(){...}
int&q = func(); //func()是返回引用类型的函数表达式

  &定义的引用类型不能用右值表达式来初始化,即左值引用不能绑定右值表达式,如:

int& m = 100; //错误,将左值引用绑定到字面常量上

int j = 100;
int& n = j + 50; //错误,将左值引用绑定到右值表达式上

int func() { ... }
int& x = func(); //错误,将左值引用绑定到返回非左值引用类型的函数表达式

 左值引用&只能使用左值表达式来初始化即只能绑定左值表达式,那右值表达式使用哪种类型呢?答案就是C++11中增加的右值引用。

3、右值引用

  右值引用只能绑定到临时对象,该临时对象将要被销毁且该对象没有其他用户,右值引用使用&&来定义,如:

int&&r = 100; //将右值引用绑定到字面常量上,即使用字面常量来初始化右值引用

int k = 5;
int&& n = k + 10; //将右值引用绑定到右值表达式上

int func() { ... }
int&& x = func(); //将右值引用绑定到返回非左值引用类型的函数表达式

  我们可以通过std::move()函数来将一个右值引用绑定到左值上,为了避免冲突,对于move()函数我们应该在调用的时候总是加上命名空间std::,如下所示。

int i = 100;
int&& r = std::move(i);

  因为右值引用只能绑定到临时对象上,所以对左值调用std::move()后,除非对原左值赋新值或销毁它外,我们将不再使用它,比如下面的代码,unique_ptr有一个构造函数支持右值引用,当我们执行这个构造函数后u1不再拥有原始指针,因为在这个构造函数中会将u2保存u1中的原始指针后将u1中的指针设为nullptr,这就保证了通过u1不再会使用原来的值,我们通过reset()对u1赋新值后可以再使用它。

    std::unique_ptr<int> u1(new int);
    std::unique_ptr<int> u2(std::move(u1));
    cout << "u1: " << (u1 ? "not null" : "null") << endl; //输出为 "u1: null"
    u1.reset(new int);

  我们知道左值引用的一个重要作用就是声明函数的参数,避免值传递和拷贝参数对象,右值引用同样是提供这个功能,我们称右值引用可以移动对象而非拷贝对象,而且它针对的是传入的参数是右值的情况,如:

void func(int&& i) {}
void test(std::string&& str) {}

func(100); //避免值传递拷贝参数

int k = 100;
func(k + 5); //避免值传递拷贝参数

test(std::string("abc")); //避免值传递拷贝参数

 4、拷贝和移动对象

  右值引用参数在类的构造函数和成员函数中用的比较多,比如C++11中很多类的构造函数、成员函数除了拷贝版本外还增加了一个移动版本,如string::string(string&& r)、vector<value_type>::push_back(value_type&& var),区分移动和拷贝的重载函数即通过函数的参数:拷贝版本通常是const T&,而移动版本是一个T&&(因为我们要从实参"窃取"数据,所以不是const T&&)。

  我们可以让自己的类也支持移动操作,通过为其定义移动构造函数和移动赋值运算符。为了完成资源移动必须确保移动后源对象的销毁是无害的,移动完成后源对象的资源所有权已经归属新创建的对象。

  由于移动操作是“窃取”资源,它通常不非配任何资源,所以我们通常都应该使用noexcept来声明移动函数不会抛出异常,而且对于标准容器来说,如果不为移动构造函数标记为noexcept的话,容器会使用拷贝构造函数而非移动构造函数。

  下面为类中添加移动构造函数和移动赋值运算符的一个例子:

class CFoo
{
public:
    CFoo(const CFoo& lf)
    {
        if (lf.m_pData)
        {
            m_pData = new char(iDataSize);
            memcpy(m_pData, lf.m_pData, iDataSize);
        }
        else
        {
            if (m_pData)
            {
                delete[] m_pData;
                m_pData = nullptr;
            }
        }
    }
    CFoo(CFoo&& rf)noexcept
    {
        m_pData = rf.m_pData; //从源对象“窃取”资源
        rf.m_pData = nullptr; //防止源对象析构后资源失效,源对象可安全销毁
    }
    CFoo& operator=(const CFoo& rhs)
    {
        if (&rhs == this)
            return *this;

        if (m_pData)
            delete[] m_pData;

        if (rhs.m_pData)
        {
            m_pData = new char[iDataSize];
            memcpy(m_pData, rhs.m_pData, iDataSize);
        }
        else
        {
            if (m_pData)
            {
                delete[] m_pData;
                m_pData = nullptr;
            }
        }
        
        return *this;
    }
    CFoo& operator=(CFoo&& rhs)noexcept
    {
        if (&rhs == this)
            return *this;
        m_pData = rhs.m_pData; //从源对象“窃取”资源
        rhs.m_pData = nullptr; //防止源对象析构后资源失效,源对象可安全销毁
        return *this;
    }
    virtual ~CFoo()
    {
        if (m_pData)
        {
            delete[] m_pData;
            m_pData = nullptr;
        }
    }
private:
    char* m_pData = nullptr;
    int iDataSize = 0;
};

  5、引用限定符&和&&

  &用来限定函数只能被左值对象来调用,&&用来限定函数只能被又值对象来调用,需要注意的是&和&&只能用于非static的成员函数,且必须同时出现在函数的声明和定义中,在const限定符之后,如:

class CFoo
{
public:
    void fun()&{}
};

CFoo().fun(); //错误,fun()只能被左值对象来调用
CFoo f;
f.func(); //正确

//======================================

class CShow
{
public:
    CShow& operator=(const CShow& rhs)&&
    {
        return *this;
    }
};

CShow s;
s = CShow(); //错误,operator=只能被右值对象调用
CShow() = s; //正确

   6、C++11中新增的使用{}初始化

  原来在C++中,对于普通数组、结构体、没有构造析构和虚函数的类可以使用 {} 进行初始化,也就是我们所说的初始化列表,而对于类对象或容器的初始化不支持{},只能使用()或拷贝构造函数,C++11中则没有了这个限制,使用初始化列表还可以防止类型收窄:

int i = { 100 };
    int num{ 100 };
    int n{ 100.0 }; //从double转换为int为类型收缩,编译出错

    std::vector<int> v = { 0, 1, 2 };
    std::map<int, std::string> m = { { 0, "c++" },{ 1, "java" },{ 2, "c#" } };
    int* p = new int[3]{ 0, 1, 2 };

    class CFoo
    {
    public:
        CFoo(int a, int b) { ; }
    };

    class CBar
    {
    private:
        CFoo f{ 0, 0 };
    };

    CFoo f = { 8, 9 };

    CFoo func(CFoo f)
    {
        return{ 8, 9 };
    }

    func({ 10, 11 });
原文地址:https://www.cnblogs.com/milanleon/p/8655608.html