快速了解C/C++的左值和右值

最近在segmentfault上看到一个提问《c++隐式的类类型转换问题》:一时不知怎么回答,查阅相关资料后整理了本文,以供参考学习。html

定义

早期的C给出的定义:左值是一个表达式,可能出如今赋值操做的左边或右边,但右值只能出如今右边。好比:ios

a * b = 42; // 编译错误, 说明 a * b 不是左值

由于上面的定义实在太模糊,致使左值和右值很难被理解,下面给出的定义,更简单更好理解:
左值(lvalue)是一个表达式,它表示一个可被标识的(变量或对象的)内存位置,而且容许使用&操做符来获取这块内存的地址。若是一个表达式不是左值,那它就被定义为右值。c++

int i = 42;
  i = 43; 
  int* p = &i; // ok, i 是左值
  int& foo();
  foo() = 42; // ok, foo() 是左值
  int* p1 = &foo(); // ok, foo() 是左值

  int foobar();
  int j = 0;
  j = foobar(); // ok, foobar() 是右值
  int* p2 = &foobar(); // 错误,不能获取右值的地址
  j = 42; // ok, 42 是右值

左值和右值之间的转换

通常上讲,对象之间的运算,对象是以右值的形式参与的。好比二元运算符+两边的参数以右值传入,加后的返回结果也是右值:segmentfault

int a = 1;     // a 是左值
int b = 2;     // b 是左值
int c = a + b; // a和b自动转换为右值求和

那些表示数组、函数和非完整类型的左值是不能转换为右值的,由于没法对那些类型进行求值。incomplete types指的是类型定义不完整,只能用指针形式声明的类型,在头文件中常常会使用。数组

左值引用

C++中可使用&符定义引用,若是一个左值同时是引用,就称为“左值引用”,如:函数

std::string s;
std::string& sref = s;  //sref为左值引用

非const左值引用不能使用右值对其赋值学习

std::string& r = std::string();  //错误!std::string()产生一个临时对象,为右值

假设能够的话,就会遇到一个问题:如何修改右值的值?由于引用是能够后续被赋值的。根据上面的定义,右值连可被获取的内存地址都没有,也就谈不上对其进行赋值。this

但const左值引用不同,由于常量不能被修改,也就不存在上面的问题:.net

const std::string& r = std::string();  //能够

咱们常用const左值引用做为函数的参数类型,能够减小没必要要的对象复制:指针

class MyString {
public:
    ...
    MyString &MyString(string& s);  //参数类型为左值引用
    ...
};

int main() 
{
    MyString s1("XXX");  //错误
    MyString s2(string("XXXX")); //同上,右值不能赋值给左值引用
}

MyString& MyString(string& a);改为MyString& MyString(const string& a);就不会有上面的编译错误。

带CV限定符(CV-qualified)的右值

C++标准中关于左值转右值的讨论,有这样一段话:

类型为T的左值(非函数、非数组类型)能够被转换为右值。若是T不是类(class)类型,转换后的右值的类型将为不带CV限定符的T类型,不然转换后的右值的类型为T。

什么是CV限定符?若是变量声明时类型前带有const或volatile,就说此变量类型具备CV限定符。

在C中,右值永远没有CV限定符,而C++中的类类型的右值能够有CV限定符,看下面代码:

#include <iostream>

class A {
public:
    void foo() const { std::cout << "A::foo() const\n"; }
    void foo() { std::cout << "A::foo()\n"; }
};

A bar() { return A(); }           //返回临时对象,为右值
const A cbar() { return A(); }    //返回带const的右值(带CV限定符)


int main()
{
    bar().foo();  // 非const对象调用A::foo()的非const版本
    cbar().foo(); // const对象调用A::foo()的const版本
}

也就是说,若是是类类型,从左值转为右值时,它的CV限定符会被保留。这里就不给出示例代码了。

右值引用(C++11)

右值引用及其相关的move语义是C++11新引入的最强大的特性之一。前文说到,左值(非const)能够被修改(赋值),但右值不能。但C++11引入的右值引用特性,打破了这个限制,容许咱们获取右值的引用,并修改之。让咱们先看点代码:
定义一个类Intvec及其赋值操做符重载函数以下:

class Intvec
{
public:
    ...
    Intvec& operator=(const Intvec& other)
    {
        log("copy assignment operator");
        Intvec tmp(other);  //构造一个临时对象,由于other为const,不能被修改
        std::swap(m_size, tmp.m_size);
        std::swap(m_data, tmp.m_data);  
        //跟临时对象交换值,临时对象晰构时会delete [] m_data
        return *this;
    }
private:
    size_t m_size;  
    int* m_data;     //存放int数组,构造时动态分配
};

代码要点:

  1. 代码使用了copy-swap策略,即先分配资源再更改自身状态,这样能够保证当资源分配失败的时候,自身可以维持原先状态,《高效C++》有条规则描述这个主题。因此先根据other拷贝构造一个临时对象tmp, 而后与tmp进行swap,m_data交换给了tmp以后,也会随着tmp的晰构而被释放。

  2. 之因此把other声明为const,有两个理由,其一是赋值操做不该该更改other,其二是能够传入一个右值。其实这样的声明随处可见。

假设现有类型为Intvec的对象v,用一个新对象给它赋值:

v = Intvec(33);

这句代码合法,它构造一个临时对象,为右值,传入到Intvec的赋值运算符重载函数中。这个代码是能够工做,并且一般状况下都比较高效。可是若是Intvec里包含某些m_handle成员,建立和释放m_handle比较昂贵,那么拷贝构造越少越好。这种状况,咱们设想一下,若是v能跟Intvec(33)临时对象直接进行内部数据交换,而不须要在重载函数里使用Intvec tmp(other);构造一个新对象出来swap,那该有多好!
如你所料,C++11引入的“右值引用”和“move语义”就能够实现这个目标,新的语法很简单,咱们重载一个新的赋值操做运算符函数:

Intvec& operator=(Intvec&& other)
{
    log("move assignment operator");
    std::swap(m_size, other.m_size);
    std::swap(m_data, other.m_data);
    return *this;
}

对于v = Intvec(33);这种写法就会调用此版本的重载函数(即传入一个右值)。
&&语法声明右值引用,表示一个指向右值的引用,经过这个引用,能够修改右值。

以上就是关于右值引用的一个简单的示例,实际上右值引用是一个复杂的主题,在实际应用中还有不少场景要考虑,更深刻的讲解见底部参考连接。

参考连接:

  1. 《Understanding lvalues and rvalues in C and C++》

  2. 《C++右值引用详解》

相关文章
相关标签/搜索