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;
}

img

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

但是,如果我们想要创建一个指向常量变量的引用呢?普通的左值引用(指向非常量值)是行不通的。

左值引用常量

通过在声明左值引用时使用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;
}

img

使用可修改的左值初始化对 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;
}

img

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

最佳实践
除非需要修改被引用的对象,否则对 lvalue references to non-const 优先选择此 lvalue references to const

对常量的左值引用使用右值初始化

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

#include <iostream>

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

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

    return 0;
}

img

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

相关内容:
我们在 2.5 课中介绍了临时对象——局部作用域简介。

对常量的左值引用使用不同类型的值初始化

指向 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;
}

img

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

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

这两种情况下,引用类型和临时匹配类型。

关键见解:
如果尝试将常量左值引用绑定到不同类型的值,编译器将创建一个与引用类型相同的临时对象,使用该值初始化该临时对象,然后将引用绑定到该临时对象。

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

虽然允许这样做可能看起来很奇怪,但我们将在第12.6 课中看到这样做很有用的例子——通过常量左值引用传递。

警告
我们通常假设引用与其所绑定的对象相同——但当引用绑定到对象的临时副本或对象转换后产生的临时对象时,这种假设就不成立了。之后对原始对象所做的任何修改,引用都无法感知(因为它引用的是不同的对象),反之亦然。
这里有一个简单的例子来说明这一点:

#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 是一个 short , you 是一个 const int&。因为you只能绑定到 int 对象,所以当 you 初始化为bombos 时,编译器会隐式地将其转换bombsint,从而创建一个临时int对象(值为 1 )。最终,you 绑定到了这个临时对象,而不是 bombs

bombs递减时,you 不受影响,因为它引用的是不同的对象。所以尽管我们预期if (you)它的值为false,但实际上它的值为true。

如果你能停止毁灭世界,那就太好了。

绑定到临时对象的 const 引用会延长临时对象的生命周期

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

鉴于上述语句,考虑一下如果在初始化表达式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

img

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

关键见解:
左值引用只能绑定到可修改的左值。
指向 const 的左值引用可以绑定到可修改的左值、不可修改的左值和右值。这使得它们成为一种更加灵活的引用类型。

适合高级读者
只有当 const 引用直接绑定到临时对象时,生命周期延长才有效。从函数返回的临时对象(即使是由 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;
}

img

当定义对 const 变量的 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 引用通常很少被使用。

posted @ 2025-12-09 16:04  游翔  阅读(27)  评论(0)    收藏  举报