引用声明
声明具名变量为引用,即既存对象或函数的别名。
语法
引用变量声明是声明符拥有下列形式的简单声明
& attr(可选) 声明符
|
(1) | ||||||||
&& attr(可选) 声明符
|
(2) | (C++11 起) | |||||||
D
声明为到 声明说明符序列 所确定的类型 S
的左值引用。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&& (这条规则,和将 |
(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 的左值引用能延长临时对象的生存期(参阅引用初始化中的规则和例外情况)。
若被指代对象被销毁(例如通过显式的析构函数调用),但存储尚未被解分配,则到生存期外的对象的引用仍能以有限的方式使用,且当在同一存储中重新创建对象时也可以变为有效(细节见在生存期之外进行访问)。