引用声明

< cpp‎ | language

声明具名变量为引用,即既存对象或函数的别名。

语法

引用变量声明是声明符拥有下列形式的简单声明

& attr(可选) 声明符 (1)
&& attr(可选) 声明符 (2) (C++11 起)
1) 左值引用声明符:声明 S& D;D 声明为到 声明说明符序列 所确定的类型 S左值引用
2) 右值引用声明符:声明 S&& D;D 声明为到 声明说明符序列 所确定的类型 S右值引用
声明符 - 除引用声明符之外的任何其他声明符(不存在引用的引用)
attr(C++11) - 可选的属性列表

引用必须被初始化为指代一个有效的对象或函数:见引用初始化

不存在 void 的引用,也不存在引用的引用。

引用类型无法在顶层被 cv 限定;声明中没有为此而设的语法,而若将限定性添加到 typedef 名、 decltype 说明符或类型模板形参,则忽略它。

引用不是对象;它们不必占用存储,尽管若需要分配存储以实现所需语义(例如,引用类型的非静态数据成员通常会增加类的大小,量为存储内存地址所需),则编译器会这么做。

因为引用不是对象,故不存在引用的数组,不存在指向引用的指针,不存在引用的引用:

int& a[3]; // 错误
int&* p;   // 错误
int& &r;   // 错误
引用坍缩

容许通过模板或 typedef 中的类型操作构成引用的引用,这种情况下适用引用坍缩(reference collapsing)规则:右值引用的右值引用坍缩成右值引用,所有其他组合均坍缩成左值引用:

typedef int&  lref;
typedef int&& rref;
int n;
lref&  r1 = n; // r1 的类型是 int&
lref&& r2 = n; // r2 的类型是 int&
rref&  r3 = n; // r3 的类型是 int&
rref&& r4 = 1; // r4 的类型是 int&&

(这条规则,和将 T&& 用于函数模板时的模板实参推导的特殊规则一起,组成了使得 std::forward 可行的规则。)

(C++11 起)

左值引用

左值引用可用于建立既存对象的别名(可选地拥有不同的 cv 限定):

#include <iostream>
#include <string>
 
int main() {
    std::string s = "Ex";
    std::string& r1 = s;
    const std::string& r2 = s;
 
    r1 += "ample";           // 修改 s
//  r2 += "!";               // 错误:不能通过到 const 的引用修改
    std::cout << r2 << '\n'; // 打印 s,现在保有 "Example"
}


它们亦可用于在函数调用中实现按引用传递语义:

#include <iostream>
#include <string>
 
void double_string(std::string& s) {
    s += s; // 's' 与 main() 的 'str' 是同一对象
}
 
int main() {
    std::string str = "Test";
    double_string(str);
    std::cout << str << '\n';
}


当函数的返回值是左值引用时,函数调用表达式成为左值表达式

#include <iostream>
#include <string>
 
char& char_number(std::string& s, std::size_t n) {
    return s.at(n); // string::at() 返回 char 的引用
}
 
int main() {
    std::string str = "Test";
    char_number(str, 1) = 'a'; // 函数调用是左值,可被赋值
    std::cout << str << '\n';
}


右值引用

右值引用可用于为临时对象延长生存期(注意,左值引用亦能延长临时对象生存期,但不能通过左值引用修改它们):

#include <iostream>
#include <string>
 
int main()
{
    std::string s1 = "Test";
//  std::string&& r1 = s1;           // 错误:不能绑定到左值
 
    const std::string& r2 = s1 + s1; // okay:到 const 的左值引用延长生存期
//  r2 += "Test";                    // 错误:不能通过到 const 的引用修改
 
    std::string&& r3 = s1 + s1;      // okay:右值引用延长生存期
    r3 += "Test";                    // okay:能通过到非 const 的引用修改
    std::cout << r3 << '\n';
}


更重要的是,当函数同时具有右值引用和左值引用的重载时,右值引用重载绑定到右值(包含纯右值和亡值),而左值引用重载绑定到左值:

#include <iostream>
#include <utility>
 
void f(int& x) {
    std::cout << "lvalue reference overload f(" << x << ")\n";
}
 
void f(const int& x) {
    std::cout << "lvalue reference to const overload f(" << x << ")\n";
}
 
void f(int&& x) {
    std::cout << "rvalue reference overload f(" << x << ")\n";
}
 
int main() {
    int i = 1;
    const int ci = 2;
    f(i);  // 调用 f(int&)
    f(ci); // 调用 f(const int&)
    f(3);  // 调用 f(int&&)
           // 若不提供 f(int&&) 重载则会调用 f(const int&)
    f(std::move(i)); // 调用 f(int&&)
 
    // 右值引用变量在用于表达式时是左值
    int&& x = 1;
    f(x);            // 调用 f(int& x)
    f(std::move(x)); // 调用 f(int&& x)
}


这允许在适当时机自动选择移动构造函数移动赋值运算符和其他具移动能力的函数(例如 std::vector::push_back())。

因为右值引用能绑定到亡值,故它们能指代非临时对象:

int i2 = 42;
int&& rri = std::move(i2); // 直接绑定到 i2

这使得可以将作用域中不再需要的对象移动出去:

std::vector<int> v{1,2,3,4,5};
std::vector<int> v2(std::move(v)); // 绑定右值引用到 v
assert(v.empty());
(C++11 起)

转发引用

转发引用是一种特殊的引用,它保持函数实参的值类别,使得能利用 std::forward 转发实参。转发引用是下列之一:

1) 函数模板的函数形参,其被声明为同一函数模板的类型模板形参的无 cv 限定的右值引用:
template<class T>
int f(T&& x) {                    // x 是转发引用
    return g(std::forward<T>(x)); // 从而能被转发
}
int main() {
    int i;
    f(i); // 实参是左值,调用 f<int&>(int&), std::forward<int&>(x) 是左值
    f(0); // 实参是右值,调用 f<int>(int&&), std::forward<int>(x) 是右值
}
 
template<class T>
int g(const T&& x); // x 不是转发引用:const T 不是无 cv 限定的
 
template<class T> struct A {
    template<class U>
    A(T&& x, U&& y, int* p); // x 不是转发引用:T 不是构造函数的类型模板形参
                             // 但 y 是转发引用
};
2) auto&&,但当其从花括号包围的初始化器列表推导时则不是:
auto&& vec = foo();       // foo() 可以是左值或右值,vec 是转发引用
auto i = std::begin(vec); // 也可以
(*i)++;                   // 也可以
g(std::forward<decltype(vec)>(vec)); // 转发,保持值类别
 
for (auto&& x: f()) {
  // x 是转发引用;这是使用范围 for 循环的最安全方式
}
 
auto&& z = {1, 2, 3}; // *不是*转发引用(初始化器列表的特殊情形)

参阅模板实参推导std::forward

(C++11 起)

悬垂引用

尽管引用一旦初始化,就始终指代一个有效的对象或函数,但有可能创建一个程序,被指代对象的生存期结束,但引用仍保持可访问(悬垂(dangling))。访问这种引用是未定义行为。 一个常见例子是返回自动变量的引用的函数:

std::string& f()
{
    std::string s = "Example";
    return s; // 退出 s 的作用域:
              // 调用其析构函数并解分配其存储
}
 
std::string& r = f(); // 悬垂引用
std::cout << r;       // 未定义行为:从悬垂引用读取
std::string s = f();  // 未定义行为:从悬垂引用复制初始化

注意,右值引用和到 const 的左值引用能延长临时对象的生存期(参阅引用初始化中的规则和例外情况)。

若被指代对象被销毁(例如通过显式的析构函数调用),但存储尚未被解分配,则到生存期外的对象的引用仍能以有限的方式使用,且当在同一存储中重新创建对象时也可以变为有效(细节见在生存期之外进行访问)。