12.4 — 对 const 的左值引用

在上一课(12.3 -- 左值引用)中,我们讨论了左值引用如何只能绑定到可修改的左值。这意味着以下情况是非法的

int main()
{
    const int x { 5 }; // x is a non-modifiable (const) lvalue
    int& ref { x }; // error: ref can not bind to non-modifiable lvalue

    return 0;
}

这是不允许的,因为它将允许我们通过非常量引用(ref)修改常量变量(x)。

但是,如果我们想创建一个引用指向一个常量变量呢?普通的左值引用(指向非常量值)将无法做到。

const 的左值引用

通过在声明左值引用时使用 const 关键字,我们告诉左值引用将其引用的对象视为 const。这种引用被称为**const 值的左值引用**(有时也称为**const 引用**或**常量引用**)。

const 的左值引用可以绑定到不可修改的左值

int main()
{
    const int x { 5 };    // x is a non-modifiable lvalue
    const int& ref { x }; // okay: ref is a an lvalue reference to a const value

    return 0;
}

因为 const 的左值引用将其引用的对象视为 const,所以它们可以用于访问但不能修改被引用的值

#include <iostream>

int main()
{
    const int x { 5 };    // x is a non-modifiable lvalue
    const int& ref { x }; // okay: ref is a an lvalue reference to a const value

    std::cout << ref << '\n'; // okay: we can access the const object
    ref = 6;                  // error: we can not modify an object through a const reference
    
    return 0;
}

用可修改的左值初始化 const 的左值引用

const 的左值引用也可以绑定到可修改的左值。在这种情况下,当通过引用访问时,被引用的对象被视为 const(即使底层对象不是 const)

#include <iostream>

int main()
{
    int x { 5 };          // x is a modifiable lvalue
    const int& ref { x }; // okay: we can bind a const reference to a modifiable lvalue

    std::cout << ref << '\n'; // okay: we can access the object through our const reference
    ref = 7;                  // error: we can not modify an object through a const reference

    x = 6;                // okay: x is a modifiable lvalue, we can still modify it through the original identifier

    return 0;
}

在上面的程序中,我们将 const 引用 ref 绑定到可修改的左值 x。然后我们可以使用 ref 访问 x,但由于 ref 是 const,我们不能通过 ref 修改 x 的值。但是,我们仍然可以直接修改 x 的值(使用标识符 x)。

最佳实践

除非您需要修改被引用的对象,否则请优先使用const 的左值引用,而不是非常量的左值引用

用右值初始化 const 的左值引用

也许令人惊讶的是,const 的左值引用也可以绑定到右值

#include <iostream>

int main()
{
    const int& ref { 5 }; // okay: 5 is an rvalue

    std::cout << ref << '\n'; // prints 5

    return 0;
}

发生这种情况时,会创建一个临时对象并用右值对其进行初始化,然后将 const 引用绑定到该临时对象。

相关内容

我们在第 2.5 -- 局部作用域介绍 课中介绍了临时对象。

用不同类型的值初始化 const 的左值引用

const 的左值引用甚至可以绑定到不同类型的值,只要这些值可以隐式转换为引用类型

#include <iostream>

int main()
{
    // case 1
    const double& r1 { 5 };  // temporary double initialized with value 5, r1 binds to temporary

    std::cout << r1 << '\n'; // prints 5

    // case 2
    char c { 'a' };
    const int& r2 { c };     // temporary int initialized with value 'a', r2 binds to temporary

    std::cout << r2 << '\n'; // prints 97 (since r2 is a reference to int)

    return 0;
}

在情况 1 中,创建一个 double 类型的临时对象并用 int 值 5 初始化。然后 const double& r1 绑定到该临时 double 对象。

在情况 2 中,创建一个 int 类型的临时对象并用 char 值 a 初始化。然后 const int& r2 绑定到该临时 int 对象。

在两种情况下,引用的类型和临时对象的类型都匹配。

关键见解

如果您尝试将 const 左值引用绑定到不同类型的值,编译器将创建一个与引用类型相同的临时对象,使用该值对其进行初始化,然后将引用绑定到该临时对象。

另请注意,当我们打印 r2 时,它打印为 int 而不是 char。这是因为 r2 是对 int 对象(已创建的临时 int)的引用,而不是对 char c 的引用。

尽管这看起来可能很奇怪,但我们将在 12.6 -- 通过 const 左值引用传递 课中看到这种用法的例子。

警告

我们通常假定引用与其绑定的对象相同——但是当引用绑定到对象的临时副本或由对象转换产生的临时对象时,这个假设就会被打破。随后对原始对象进行的任何修改都不会被引用看到(因为它引用的是不同的对象),反之亦然。

这是一个显示此情况的愚蠢示例

#include <iostream>

int main()
{
    short bombs { 1 };         // I can has bomb! (note: type is short)
    
    const int& you { bombs };  // You can has bomb too (note: type is int&)
    --bombs;                   // Bomb all gone

    if (you)                   // You still has?
    {
        std::cout << "Bombs away!  Goodbye, cruel world.\n"; // Para bailar la bomba
    }

    return 0;
}

在上面的示例中,bombs 是一个 shortyou 是一个 const int&。因为 you 只能绑定到 int 对象,所以当 youbombs 初始化时,编译器会将 bombs 隐式转换为 int,这会导致创建一个临时 int 对象(值为 1)。you 最终绑定到这个临时对象而不是 bombs

bombs 递减时,you 不受影响,因为它引用的是不同的对象。所以虽然我们期望 if (you) 评估为 false,但它实际上评估为 true

如果您能停止炸毁世界,那就太好了。

绑定到临时对象的常量引用延长了临时对象的生命周期

临时对象通常在创建它们的表达式结束时被销毁。

给定语句 const int& ref { 5 };,考虑如果创建用于保存右值 5 的临时对象在初始化 ref 的表达式结束时被销毁,会发生什么。引用 ref 将处于悬空状态(引用一个已被销毁的对象),当我们尝试访问 ref 时,我们将得到未定义行为。

为了避免在这种情况下出现悬空引用,C++ 有一个特殊规则:当 const 左值引用**直接**绑定到临时对象时,临时对象的生命周期将延长以匹配引用的生命周期。

#include <iostream>

int main()
{
    const int& ref { 5 }; // The temporary object holding value 5 has its lifetime extended to match ref

    std::cout << ref << '\n'; // Therefore, we can safely use it here

    return 0;
} // Both ref and the temporary object die here

在上面的示例中,当 ref 用右值 5 初始化时,会创建一个临时对象,并且 ref 绑定到该临时对象。临时对象的生命周期与 ref 的生命周期匹配。因此,我们可以在下一条语句中安全地打印 ref 的值。然后 ref 和临时对象都超出作用域并在块结束时被销毁。

关键见解

左值引用只能绑定到可修改的左值。

const 的左值引用可以绑定到可修改的左值、不可修改的左值和右值。这使它们成为一种更灵活的引用类型。

致进阶读者

生命周期延长仅在常量引用直接绑定到临时对象时才有效。从函数返回的临时对象(即使是常量引用返回的)不符合生命周期延长的条件。

我们在 12.12 -- 通过引用返回和通过地址返回 课中展示了这方面的一个示例。

对于类类型右值,将引用绑定到成员将延长整个对象的生命周期。

那么 C++ 为什么允许常量引用绑定到右值呢?我们将在下一课中回答这个问题!

Constexpr 左值引用 可选

当应用于引用时,constexpr 允许在常量表达式中使用引用。Constexpr 引用有一个特殊的限制:它们只能绑定到具有静态持续时间的对象(全局变量或静态局部变量)。这是因为编译器知道静态对象在内存中实例化在哪里,所以它可以将该地址视为编译时常量。

constexpr 引用不能绑定到(非静态)局部变量。这是因为局部变量的地址直到它们定义的函数实际调用时才知道。

int g_x { 5 };

int main()
{
    [[maybe_unused]] constexpr int& ref1 { g_x }; // ok, can bind to global

    static int s_x { 6 };
    [[maybe_unused]] constexpr int& ref2 { s_x }; // ok, can bind to static local

    int x { 6 };
    [[maybe_unused]] constexpr int& ref3 { x }; // compile error: can't bind to non-static object

    return 0;
}

当定义常量变量的 constexpr 引用时,我们需要同时应用 constexpr(应用于引用)和 const(应用于被引用的类型)。

int main()
{
    static const int s_x { 6 }; // a const int
    [[maybe_unused]] constexpr const int& ref2 { s_x }; // needs both constexpr and const

    return 0;
}

鉴于这些限制,constexpr 引用通常很少使用。

guest
您的电子邮箱地址将不会被显示
发现错误?请在上方留言!
与勘误相关的评论在处理后将被删除,以帮助减少混乱。感谢您帮助使网站对每个人都更好!
来自 https://gravatar.com/ 的头像与您提供的电子邮箱地址相关联。
有回复时通知我:  
208 条评论
最新
最早 最多投票
内联反馈
查看所有评论