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

目录

一、左值右值

1.1 定义

1.2 举例

二、左值引用 右值引用

2.1 定义

2.2 引用的作用

2.3 需要注意的点


 

一、左值右值

1.1 定义

在C++中有几个晦涩的概念:左值、右值、泛左值、纯右值、将亡值

这5种值类别我总结了一张表格,如下:

首先需要区分两个概念。在C++中,表达式有两种属性:

  • 类型(type)
  • 值类别(value category)

例如有int a=1, b=2;  那么"a+b"这个表达式的【类型】是int,而【值类别】是右值,合起来就称为【int类型的右值表达式】。

 

左值右值描述的是一个表达式的值类别。

在C++中,一切表达式都必然属于左值、将亡值、纯右值中的一种。而右值=将亡值+纯右值,所以我们又可以说一切C++表达式都必然要么是左值要么是右值

泛左值=左值+将亡值; 右值=纯右值+将亡值; (将亡值既属于泛左值又属于右值)。

这些分类是由于它们具有不同性质而命名的,但其实我们只需要掌握左值和右值的概念就已经足够了。

从性质上来看:

  • 只有左值可以取地址、可以作为等号左边的操作数;不具有该性质的就必然是右值。
  • 左值和将亡值都具有多态性质;不具有该性质的就是纯右值了。

 

1.2 举例

用是否可以取地址来验证表达式是左值还是右值:

#include <utility>
#include <string>
using namespace std;

string &f1(string &a) {
    return a;
}
string &&f2() {
    string a;
    return move(a);
}
string f3() {
    return "";
}

int main() {
    string s("hello");
    printf("%p\n", &f1(s)); //f1(s)是左值,可以取地址
    // printf("%p\n", &f2()); //f2()是将亡值,不能取地址
    // printf("%p\n", &f3()); //f3()是纯右值,不能取地址

    int a = 1;
    printf("%p\n", &++a); //前置自增是左值,可以取地址
    // printf("%p\n", &a++); 后置自增是右值,不能取地址

    printf("%p\n", &"hello"); //字符串字面值是左值,可以取地址
    // printf("%p\n", &42); //数字字面值是右值,不能取地址
}

验证将亡值的多态性

#include <iostream>

struct A {
    virtual void f() {
        printf("classA\n");
    }
};

struct B : A {
    void f() override {
        printf("classB\n");
    }
};

int main() {
    static_cast<A&&>(B()).f();  //打印classB, static_cast表达式是将亡值,静态类型为A,动态类型为B,发生多态
    static_cast<A>(B()).f();  //打印classA, static_cast表达式是纯右值,静态类型为A,动态类型为A,未发生多态
}

 

二、左值引用 右值引用

2.1 定义

在C++中引用类型与表达式之间的绑定关系有语法上的规定,如下表:

  • T&只能绑定非常量左值
  • const T&是万能引用类型,可以绑定任意值类别的表达式
  • T&&只能绑定非常量右值
  • const T&&能绑定右值类型的表达式,无论是否常量

代码验证绑定规则:

#include <utility>
#include <string>
using namespace std;

int main() {
    string s1;  //非常量左值
    const string s2;  //常量左值

    string &r1 = s1;             //【非常量左值引用】可以绑定【非常量左值】
    // string &r2 = s2;          //【非常量左值引用】不能绑定【常量左值】
    const string &r3 = s1;       //【常量左值引用】可以绑定【非常量左值】
    const string &r4 = s2;       //【常量左值引用】可以绑定【常量左值】
    const string &r5 = move(s1); //【常量左值引用】可以绑定【非常量右值】
    const string &r6 = move(s2); //【常量左值引用】可以绑定【常量右值】
    string &&r7 = move(s1);      //【非常量右值引用】可以绑定【非常量右值】
    // string &&r8 = move(s2);   //【非常量右值引用】不能绑定【常量右值】
    const string &&r9 = move(s1); //【常量右值引用】可以绑定【非常量右值】
    const string &&r10 = move(s2); //【常量右值引用】可以绑定【常量右值】
}

回过头看一下C++的拷贝构造和移动构造

拷贝构造T(const T&)接受一个常量左值引用,可以绑定T类型的任意值类别表达式;

移动构造T(T&&)接受一个右值引用,只能绑定T类型的右值表达式。

这使得当没有定义移动构造函数时,右值表达式也能匹配上拷贝构造函数的形参。但同时也没法享受移动语义带来的高效率了。

 

2.2 引用的作用

左值本身就是具名变量,当它被绑定给左值引用时只是起了个别名,生命周期并不会变化。

而右值本身的生命周期会在表达式结束后立即被销毁。但当右值被绑定给引用时,该右值会“重获新生”,它的生命周期会延长,与该引用的生命周期相同。

代码举例:

#include <iostream>
using namespace std;

class A {
public:
    A(int a) : mVal(a) {
        printf("A(), mVal=%d\n", mVal);
    }
    ~A() {
        printf("~A(), mVal=%d\n", mVal);
    }

private:
    int mVal;
};

A &f() {
    A a = A(10);
    return a;  // a是局部变量,是左值。该值在离开此函数时即被销毁,绑定给A&并不会延长它的生命周期
}

int main() {
    A &ref = f(); // 此处的ref指向一个非法值
    A(0);  // 右值。生命周期只有该行。被创建后立即被析构
    A &&ref1 = A(1);  // 将右值绑定给右值引用ref1,其生命周期被延长至与ref1相同
    A &&ref2 = A(2);  // 将右值绑定给右值引用ref2,其生命周期被延长至与ref2相同
} // 离开函数作用域,依次销毁ref2、ref1

 

2.3 需要注意的点

※ 右值引用是个左值。(是不是很绕口?)

准确的来说,所有的引用,无论是左值引用还是右值引用,都是左值表达式。

这是因为右值引用延长了右值的生命周期,我们可以去取右值引用的地址,因此它是个左值。

代码举例:

#include <iostream>

int main() {
    int &&a = 3; //3是右值  a是右值引用
    printf("address of a: %p", &a); //我们不能直接对3取地址,但可以对右值引用a取地址。
}

※ move虽然将左值转换成了右值,但其生命周期并不会像右值一样立即结束。

代码举例:

#include <iostream>
using namespace std;

class A {
public:
    A(int a) : m(a) {
        printf("A(), m=%d\n", m);
    }
    A(const A &) = default;
    A(A &&) = default;
    ~A() {
        printf("~A(), m=%d\n", m);
    }

private:
    int m;
};

int main() {
    A a1(1);
    move(a1);  //move将a1从左值转换为右值,但它并不会在离开此行后被析构
    A a2(2);
}

 

 

 

评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值