C++: 左值、左值引用、右值、右值引用


本文部分内容来自C++ primer,记录一下。

左值和右值

左值是可以放在赋值号左边可以被赋值的值;左值必须要在内存中有实体;
右值当在赋值号右边取出值赋给其他变量的值;右值可以在内存也可以在CPU寄存器。
一个对象被用作右值时,使用的是它的内容(值),被当作左值时,使用的是它的地址。
左值右值翻译:
L-value中的L指的是Location,表示可寻址。Avalue (computer science)that has an address.
R-value中的R指的是Read,表示可读。in computer science, a value that does not have an address in a computer language.

int a=3;
const int b=5;
a=b+2; //a是左值,b+2是右值
b=a+2; //错!b是只读的左值但无写入权,不能出现在赋值符号左边
(a=4) +=2; //a=4是左值表达式,2是右值,+=为赋值操作符
34=a+2; //错!34是字面量不能做左值

引用

引用的本质还是靠指针来实现的。引用相当于变量的别名。
引用(reference)分为左值引用和右值引用,通常我们说引用,指的是左值引用。

1.左值引用

引用为对象起了另外一个名字,引用类型引用另外一种类型,通过将声明符写成&d的形式来定义引用类型,其中d是声明的变量名:

int ival = 1024;
int &refVal = ival;
int &refVal2;//Declaration of reference variable 'refVal2' requires an initializer

一般在初始化变量时,初始值会被拷贝到新建的对象中,然而定义引用时,程序把引用和它的初始值绑定(bind)在一起,而不是将初始值拷贝给引用,一旦初始化完成,引用将和它的初始值对象一致绑定在一起,因为无法令引用重新绑定到另外一个对象,所以引用必须初始化,类似于const定义的常量。
注意:引用即别名,引用并非对象,相反的,它只是为一个已经存在的对象所起的另外一个名字。
定义一个引用后,对其进行的所有操作都是在与之绑定的对象上进行的:

refVal = 2;
int ii = refVal;

为引用赋值,实际上是把值赋给了与引用绑定的对象上,获取引用的值,实际上是获取了引用绑定的对象的值,以引用为初始值,实际上以引用绑定的对象为初始值:

int &refVal3 = refVal;//绑定到ival
int i = refVal;//ival的值初始化

引用的定义

int i=2, &ref=i;
int &refVal = 10 // Non-const lvalue reference to type 'int' cannot bind to a temporary of type 'int'

注意:非const引用类型的初始值必须是对象。
常量左值引用可以被赋右值。 因为它们是常量,不能通过引用被修改,因此修改一个右值没问题。这使得C++中接受常量引用作为函数形参成为可能,这避免了一些不必要的临时对象的拷贝和构造。

2.右值引用

一个右值表达式表示的是对象的值

int i=42;
int &r=i;   //正确,r引用i
int &&rr=i   //错误,不能将一个右值引用绑定到一个左值上
int &r2=i*42;  //错误,i*42是一个右值
const int &r3=i*42;  //正确,我们可以将一个const的引用绑定到一个右值上
int &&r2=i*42; //正确,将rr2绑定到乘法结果上
左值持久,右值短暂

左值有持久的状态,而右值要么是字面值常量,要么是表达式求值过程中创建的临时对象。
由于右值引用只能绑定到临时对象,我们得知:
1.所引用的对象将要被销毁
2,.该对象没有其他用户

这两个特征意味着:使用右值引用的代码可以自由地接管所引用的对象的资源。

变量是左值

变量可以看作只有一个运算对象而没有运算符的表达式,虽然我们很少这样看待变量。类似于其他任何表达式,变量表达式也有左值/右值属性。变量表达式都是左值,带来的结果就是,我们不能将一个右值引用绑定到一个右值引用类型的变量上。

int &&rr1 =1;  //正确,字面值常量是右值
int &&r2 =rr1;   //错误,表达式rr1是左值,因为我们可以给rr1赋值 rr1=2

虽然不能将一个右值引用直接绑定到一个左值上,但我们可以显式地将一个左值转换为对应的右值引用类型。我们可以通过调用一个名为move的新标准库函数来获得绑定到左值上的右值引用,此函数定义在头文件utility中。

int&& r1 = 1;
int&& r2 = std::move(r1); //OK

move调用告诉编译器:我们有一个左值,但我们希望像右值一样处理它。我们必须认识到,调用move就意味着承诺:除了对rr1赋值或者销毁之外,我们将不再使用它。在调用move之后,我们不能对移后源对象的值做任何假设。
注意:
1.我们可以销毁一个移后源对象,也可以赋予它新值,但是不能使用一个移后源对象的值。
2.对于move的使用应该是std:move而不是move。这样做可以避免潜在的名字冲突。

3.左值右值转换

通常来说,语言构造一个对象的值要求右值作为它的参数。例如,二元加运算符 ‘+’ 要求两个右值作为它的参数并且返回一个右值:

int a = 1;     //a是一个左值
int b = 2;     //b是一个左值
int c = a + b; //+需要右值,左值a和b都转换成右值,并且返回一个右值
a + 1 = 2;  //错误!表达式必须是左值,而a+1为右值

左值可以转换成右值(当做右值用),而右值不能转换成左值(即右值不能当做左值用)。
但是右值可以产生左值,例如一元运算符*(解引用)可以以一个右值为参数产生左值作为结果。

int arr[] = {1, 2};
int* p = &arr[0];
*(p + 1) = 10;    //对的:p+1是一个右值,但是*(p+1)是一个左值

相反的,一元取地址符 & 拿一个左值作为参数并且生成一个右值:

int var = 10;
int* bad_addr = &(var + 1); //错误:‘&’运算符要求一个左值
int* addr = &var;           //正确:var是左值
&var = 40;                  //错误:赋值运算符的左操作数要求一个左值
加深理解
例1
int foo() { return 2; }

int main()
{
    foo() = 2;
    return 0;
}

运行:

test.c: In function 'main':
test.c:8:5: error: lvalue required as left operand of assignment

原因是等号左侧要求一个左值,而函数返回一个右值

例2
int& foo() { return 2; }

int main()
{
    foo() = 2;
    return 0;
}

运行:

testcpp.cpp: Infunction 'int& foo()':
testcpp.cpp:5:12: error: invalid initialization of non-const reference of type 'int&' from an rvalue of type 'int'

赋值运算符的左部分是一个来自右值的左值引用,无法赋值

不是所有的对函数调用结果赋值都是无效的。比如,C++的引用(reference)让这成为可能:

int globalvar = 20;

int& foo()
{
    return globalvar;
}

int main()
{
    foo() = 10;
    return 0;
}

这里foo返回一个引用,这是一个左值,所以它可以被赋值。实际上,C++从函数中返回左值的能力对于实现一些重载运算符时很重要的。一个普遍的例子是在类中为实现某种查找访问而重载中括号运算符 []。std::map可以这样做。
mymap[10] 赋值是合法的因为非const的重载运算符 std::map::operator[] 返回一个可以被赋值的引用。

std::map<int, float> mymap;
mymap[10]=5.6;
例3

例3可以帮助理解右值引用帮助减少赋值的重复构造操作。
作为一个例子,考虑下面一个简单的动态 “整数vector” 实现。

class Intvec
{
public:
    explicit Intvec(size_t num = 0)
        : m_size(num), m_data(new int[m_size])
    {
        log("constructor");
    }
    ~Intvec()
    {
        log("destructor");
        if (m_data) {
            delete[] m_data;
            m_data = 0;
        }
    }

    Intvec(const Intvec& other)
        : m_size(other.m_size), m_data(new int[m_size])
    {
        log("copy constructor");
        for (size_t i = 0; i < m_size; ++i)
            m_data[i] = other.m_data[i];
    }

    Intvec& operator=(const Intvec& other)
    {
        log("copy assignment operator");
        Intvec tmp(other);
        std::swap(m_size, tmp.m_size);
        std::swap(m_data, tmp.m_data);
        return *this;
    }
private:
    void log(const char* msg)
    {
        cout << "[" << this << "] " << msg << "\n";
    }

    size_t m_size;
    int* m_data;
};
[000000C5888FFC90] constructor //Intvec v1(20)
[000000C5888FFC70] constructor //Intvec v2 默认构造
assigning lvalue...
[000000C5888FFC70] copy assignment operator //Intvec& operator=(const Intvec& other)
[000000C5888FFC80] copy constructor //Intvec(const Intvec& other)
[000000C5888FFC80] destructor //尾号80 调用析构函数
ended assigning lvalue...
[000000C5888FFC70] destructor
[000000C5888FFC90] destructor

虽然这里我只是赋一个刚刚构造的vector,但是这只是真是证明一个非常普遍的例子,一些临时的右值被构造然后被赋值给 v2(比如,这可能发生在函数中返回一个vector)。输出:

[0000007CAFDEF840] constructor
assigning rvalue...
[0000007CAFDEF820] constructor // 右值 构造
[0000007CAFDEF840] copy assignment operator
[0000007CAFDEF830] copy constructor // 再次构造
[0000007CAFDEF830] destructor
[0000007CAFDEF820] destructor
ended assigning rvalue...
[0000007CAFDEF840] destructor

可以发现,多了一对额外的构造/析构调用。不幸的是,这是个额外工作,没有任何用,因为在拷贝赋值运算符的内部,另一个临时拷贝的对象在被创建和析构。
C++11给我们右值引用可以实现“移动语义”,我们来添加另一个 operator= 到 Intvec :

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

&& 语法是新的右值引用。掏空右值对象的值。输出为:

[0000004164BAFE30] constructor
assigning rvalue...
[0000004164BAFE20] constructor
[0000004164BAFE30] move assignment operator
[0000004164BAFE20] destructor
ended assigning rvalue...
[0000004164BAFE30] destructor

左值、左值引用、右值、右值引用
C++:浅谈右值引用
特别感谢:理解C和C++中的左值和右值

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值