引用类型 ######################################## 左值、右值、将亡值 **************************************** 每个C++表达式都由两部分组成:类型和值。所有表达式有具有非引用类型。表达式分为三种:纯右值、将亡值和左值 - 一个泛左值(generalized left value, glvalue)是一个表达式,它的值决定了对象、位域或函数的标识 - 一个纯右值(pure right value, prvalue)要么被用来求解表达式(此类纯右值没有结果对象(result object)),要么被用来初始化对象或位域(此类纯右值具有结果对象)。结果对象可能是一个变量、一个通过new创建的对象或一个 \ `无名对象(C++17) `_ - 一个将亡值(eXpiring value, xvalue)是一个泛左值,代表一个对象或者位域,其资源可以被重用 - 一个左值是一个“不是将亡值的泛左值”。(其命名具有历史原因,因为左值通常出现在表达式的左边) - 一个右值是一个纯右值或将亡值 左值(lvalue) ======================================== 左值的特征: - 包含泛左值的特征 - 左值允许取地址 - 左值位于赋值表达式的左边 - 可以使用左值初始化左值引用 以下表达式是左值表达式: - 变量名、函数、模板参数对象 ``C++20`` 、数据成员、非标准类型(regardless of type,例如 ``std::cin`` 和 ``std::endl`` )。即使变量的类型是一个右值引用,其表达式依然是一个左值表达式 - 函数调用或运算符重载表达式的返回值类型为左值引用。例如:``std::getline(std::cin, str)`` 、 ``std::cout << 1`` 、 ``str1 = str2`` 、 ``++it`` - 内建赋值表达式(例如 ``a = b`` 、 ``a+=b`` 、 ``a-=b`` …) [#]_ - 解引用表达式(\*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(x)`` - 返回类型为左值引用的函数调用和运算符重载表达式 - 转成右值引用的函数指针。例如:``static_cast(x)`` 纯右值 ======================================== 纯右值的特征: - 包含右值的特征 - 纯右值不能有 \ `不完整的类型 `_ (除了void类型) - 纯右值不能有抽象类或其数组 - 非类、非数组类型的纯右值无法通过 \ `cv限定 `_ (函数调用或强制转换表达式可能会产生非类、cv限定的纯右值,但是cv-限定符会立即被删除) - 纯右值没有多态:其表示的对象的动态类型总是类型表达式 引用类型的分类 **************************************** 引用类型被分为以下几种类型: 引用类型的初始化 **************************************** 引用将在以下情景下进行初始化: - 使用初始化器对左值引用进行初始化 - 使用初始化器对右值引用进行初始化 - 一个参数类型为引用类型的函数 - 若函数返回值为引用类型,则在``return`` 进行初始化 - 非静态类成员使用初始化列表进行初始化 .. note:: ``初始化列表`` 请与 ``列表初始化`` 进行区分: - 初始化列表特指构造函数对类成员的构造方式 - 列表初始化指使用大括号对数组等进行初始化的 引用的初始化遵循以下原则: - 若初始化器为列表初始化,则遵循列表初始化的规则 - 若该引用为 ``左值引用`` : 设该初始化语句为 ``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 上(或者绑定到其基类对象上)(必要时物化临时对象)。 .. code-block:: cpp struct A {}; struct B : A {}; extern B f(); const A& rca2 = f(); // 将类 B 的对象绑定到类 A 的对象上 A&& rra = f(); // 同上 int i2 = 42; int&& rri = static_cast(i2); // 直接绑定到 i2 - 若 object 既不是 类型 T 也不派生自类型 T,但是 object 用于转换到类型 T 右值或函数左值的类型转换函数,且 cv 限定少于 a ,则 a 会绑定到类型转换的结果上(必要时物化临时对象)。 .. code-block:: cpp struct A {}; struct B : A {}; struct X { operator B(); } x; const A& r = x; // 绑定 r 到类型转换表达式结果的基类对象上 B&& rrb = x; // 绑定 r 到类型转换表达式的结果上 .. note:: - 一个临时值,要么绑定到右值引用上,要么绑定到更多 const 限定的左值引用上 - 在上述中“类型转换函数”既包括单参数造成的类型转换函数,也包括 operator 重载的类型转换函数,在第一种情况下,可能会导致临时对象的复制。例: .. code-block:: cpp const std::string& rs = "abc"; // rs 通过调用单参数构造函数进行初始化 通过为单参数构造函数添加 ``explicit`` 可以禁用此种类型绑定 引用类型的生命周期 **************************************** 当临时值绑定到引用时,临时值的生命周期将会延长至与引用类型相同。但以下情况除外: - 由 return 返回的临时值将会在 return 后立即被销毁,因此,return 只能获得悬空引用。 - 处于初始化列表中的临时对象将会在构造函数后立即销毁 - 若函数的参数是一个绑定了临时值的引用,则该引用在函数结束后立即被销毁,若该引用被 return, 则造成悬空引用。 - 用于 new 表达式的临时值生命周期将会在 new 表达式结束后结束,否则将会导致悬空引用。 - 若引用在初始化调用了构造函数(比如用 const char* 初始化 string& 时)或者调用了 operator 类型转换函数,则在类型转换完成后临时值将会被销毁。 - 使用小括号语法(而不是大括号语法)初始化一个结构体时,引用的临时对象将会在初始化完成后立即销毁。 .. code-block:: cpp struct A { int&& r; }; A a1{7}; // 生命周期延长 A a2(7); // 悬空引用 .. note:: 总的说来,无法通过传递一个引用来延长其生命周期,引用的引用也无法延长临时对象的生命周期。 移动语义 **************************************** 如果函数返回的是一个临时对象,则自动使用移动语义,否则只能使用 *std::move* 显式声明 函数的返回值: 如果函数的返回类型为左值引用,那么 return 返回的应当是指针的解引用,例如 \*this ,不要返回局部变量的左值引用,这样会出现悬空引用 函数的返回值除非特殊情况,否则不应当存在显式的右值引用 .. [#] \ `Value categories `_ .. [#] \ `Assignment operators `_ 何时使用引用 **************************************** :: 以下内容来自我的一些经验 在刚接触到引用的时候你可以会很兴奋,迫不及待地想要将引用用到代码的各个部分:多么美妙啊,使用右值引用来免除对象拷贝带来的开销,使用左值引用来避免难看的指针。于是你开始在各个地方使用它:创建对象别名、接受实参、返回数据。但是请一定要小心翼翼地使用引用,在函数中返回的引用可能是悬空的,目前,我只推荐在以下情况下使用: - 在创建函数中使用左值引用接受参数 - 在移动 构造/赋值 函数中使用右值引用接受参数 - 将返回 \*this 的函数的返回值更改为 const 限定的左值引用 - 在有所有权概念的类(比如线程)中使用 *std::move* 转移所有权 其他情况都不建议使用 - 不要使用 return std::move,这样带来的后果更可能是得到一个悬空引用 - 不要使用右值引用接受任何由强制类型转换得到的对象。请注意,一旦你使用强制类型得到了一个右值引用,这个引用很可能是绑定到一个将亡值上的,这时引用与原对象是毫无关系的 指针和引用的区别 **************************************** - 引用是没有地址的,对引用取地址得到的是原变量的地址 - 指针可以有多级,引用只有一级 - 指针有空指针,引用有悬空引用 - 指针失败可以使用空指针,引用失败只能抛异常 - 指针的大小为四个字节,引用的大小为四个字节 - 对指针 sizeof 得到的是指针的大小,对引用 sizeof 得到的对象对象的大小