引用类型

左值、右值、将亡值

每个C++表达式都由两部分组成:类型和值。所有表达式有具有非引用类型。表达式分为三种:纯右值、将亡值和左值

  • 一个泛左值(generalized left value, glvalue)是一个表达式,它的值决定了对象、位域或函数的标识

  • 一个纯右值(pure right value, prvalue)要么被用来求解表达式(此类纯右值没有结果对象(result object)),要么被用来初始化对象或位域(此类纯右值具有结果对象)。结果对象可能是一个变量、一个通过new创建的对象或一个 无名对象(C++17)

  • 一个将亡值(eXpiring value, xvalue)是一个泛左值,代表一个对象或者位域,其资源可以被重用

  • 一个左值是一个“不是将亡值的泛左值”。(其命名具有历史原因,因为左值通常出现在表达式的左边)

  • 一个右值是一个纯右值或将亡值

左值(lvalue)

左值的特征:

  • 包含泛左值的特征

  • 左值允许取地址

  • 左值位于赋值表达式的左边

  • 可以使用左值初始化左值引用

以下表达式是左值表达式:

  • 变量名、函数、模板参数对象 C++20 、数据成员、非标准类型(regardless of type,例如 std::cinstd::endl )。即使变量的类型是一个右值引用,其表达式依然是一个左值表达式

  • 函数调用或运算符重载表达式的返回值类型为左值引用。例如:std::getline(std::cin, str)std::cout << 1str1 = str2++it

  • 内建赋值表达式(例如 a = ba+=ba-=b …) 1

  • 解引用表达式(*p)

  • 数组表达式(a[n]、p[n])

  • 对象成员表达式(a.m)。除非m是成员枚举或非静态成员函数、或a是一个右值而m是一个非静态数据成员的非引用类型。

  • 指针成员表达式(a->m)。除非m是成员枚举或非静态成员函数。

  • 指向成员变量的指针(a.*mp、a->*mp)

  • 逗号表达式(a, b)此处b是一个左值

  • 三元条件表达式( a ? : c)。

  • 字符字面量。例如 “Hello, World!”

  • 转成左值引用的类型转换表达式。例如:static_cast<int&>(x)

  • 返回类型为左值引用的函数调用和运算符重载表达式

  • 转成右值引用的函数指针。例如:static_cast<void (&&)(int)>(x)

纯右值

纯右值的特征:

  • 包含右值的特征

  • 纯右值不能有 不完整的类型 (除了void类型)

  • 纯右值不能有抽象类或其数组

  • 非类、非数组类型的纯右值无法通过 cv限定 (函数调用或强制转换表达式可能会产生非类、cv限定的纯右值,但是cv-限定符会立即被删除)

  • 纯右值没有多态:其表示的对象的动态类型总是类型表达式

引用类型的分类

引用类型被分为以下几种类型:

引用类型的初始化

引用将在以下情景下进行初始化:

  • 使用初始化器对左值引用进行初始化

  • 使用初始化器对右值引用进行初始化

  • 一个参数类型为引用类型的函数

  • 若函数返回值为引用类型,则在``return`` 进行初始化

  • 非静态类成员使用初始化列表进行初始化

备注

初始化列表 请与 列表初始化 进行区分:

  • 初始化列表特指构造函数对类成员的构造方式

  • 列表初始化指使用大括号对数组等进行初始化的

引用的初始化遵循以下原则:

  • 若初始化器为列表初始化,则遵循列表初始化的规则

  • 若该引用为 左值引用

    设该初始化语句为 T& a = object: - 若 object 是一个左值表达式,其类型为 T 或继承自 T,且其 cv 限定没有 a 严格,则 a 直接被绑定到 object - 若 obejct 的类型既不是 T 也不派生自 T,但是其可以通过隐式类型转换到类型 T&,则通过调用类型转换函数进行转换(类型转换后的对象 cv 限定不得多于 a 的 cv 限定)

  • 若该引用为 volatile 限定,但是有 const 限定的左值引用 或者 右值引用

    设该初始化语句为 T& a = object 或者 T&&a = object - 若 object 是非位域右值或者函数左值,且其类型为 T 或派生自 T,且其 cv限定少于 a,则 object 将会绑定到 a 上(或者绑定到其基类对象上)(必要时物化临时对象)。

    struct A {};
    struct B : A {};
    extern B f();
    const A& rca2 = f(); // 将类 B 的对象绑定到类 A 的对象上
    A&& rra = f();       // 同上
    
    int i2 = 42;
    int&& rri = static_cast<int&&>(i2); // 直接绑定到 i2
    
    • 若 object 既不是 类型 T 也不派生自类型 T,但是 object 用于转换到类型 T 右值或函数左值的类型转换函数,且 cv 限定少于 a ,则 a 会绑定到类型转换的结果上(必要时物化临时对象)。

      struct A {};
      struct B : A {};
      struct X { operator B(); } x;
      const A& r = x; // 绑定 r 到类型转换表达式结果的基类对象上
      B&& rrb = x;    // 绑定 r 到类型转换表达式的结果上
      

备注

  • 一个临时值,要么绑定到右值引用上,要么绑定到更多 const 限定的左值引用上

  • 在上述中“类型转换函数”既包括单参数造成的类型转换函数,也包括 operator 重载的类型转换函数,在第一种情况下,可能会导致临时对象的复制。例:

    const std::string& rs = "abc"; // rs 通过调用单参数构造函数进行初始化
    

    通过为单参数构造函数添加 explicit 可以禁用此种类型绑定

引用类型的生命周期

当临时值绑定到引用时,临时值的生命周期将会延长至与引用类型相同。但以下情况除外:

  • 由 return 返回的临时值将会在 return 后立即被销毁,因此,return 只能获得悬空引用。

  • 处于初始化列表中的临时对象将会在构造函数后立即销毁

  • 若函数的参数是一个绑定了临时值的引用,则该引用在函数结束后立即被销毁,若该引用被 return, 则造成悬空引用。

  • 用于 new 表达式的临时值生命周期将会在 new 表达式结束后结束,否则将会导致悬空引用。

  • 若引用在初始化调用了构造函数(比如用 const char* 初始化 string& 时)或者调用了 operator 类型转换函数,则在类型转换完成后临时值将会被销毁。

  • 使用小括号语法(而不是大括号语法)初始化一个结构体时,引用的临时对象将会在初始化完成后立即销毁。

    struct A {
       int&& r;
    };
    A a1{7}; // 生命周期延长
    A a2(7); // 悬空引用
    

备注

总的说来,无法通过传递一个引用来延长其生命周期,引用的引用也无法延长临时对象的生命周期。

移动语义

如果函数返回的是一个临时对象,则自动使用移动语义,否则只能使用 std::move 显式声明

函数的返回值:

如果函数的返回类型为左值引用,那么 return 返回的应当是指针的解引用,例如 *this ,不要返回局部变量的左值引用,这样会出现悬空引用 函数的返回值除非特殊情况,否则不应当存在显式的右值引用

1

Value categories

2

Assignment operators

何时使用引用

以下内容来自我的一些经验

在刚接触到引用的时候你可以会很兴奋,迫不及待地想要将引用用到代码的各个部分:多么美妙啊,使用右值引用来免除对象拷贝带来的开销,使用左值引用来避免难看的指针。于是你开始在各个地方使用它:创建对象别名、接受实参、返回数据。但是请一定要小心翼翼地使用引用,在函数中返回的引用可能是悬空的,目前,我只推荐在以下情况下使用:

  • 在创建函数中使用左值引用接受参数

  • 在移动 构造/赋值 函数中使用右值引用接受参数

  • 将返回 *this 的函数的返回值更改为 const 限定的左值引用

  • 在有所有权概念的类(比如线程)中使用 std::move 转移所有权

其他情况都不建议使用

  • 不要使用 return std::move,这样带来的后果更可能是得到一个悬空引用

  • 不要使用右值引用接受任何由强制类型转换得到的对象。请注意,一旦你使用强制类型得到了一个右值引用,这个引用很可能是绑定到一个将亡值上的,这时引用与原对象是毫无关系的

指针和引用的区别

  • 引用是没有地址的,对引用取地址得到的是原变量的地址

  • 指针可以有多级,引用只有一级

  • 指针有空指针,引用有悬空引用

  • 指针失败可以使用空指针,引用失败只能抛异常

  • 指针的大小为四个字节,引用的大小为四个字节

  • 对指针 sizeof 得到的是指针的大小,对引用 sizeof 得到的对象对象的大小