引用声明
声明具名变量为引用,即既存对象或函数的别名。
目录 |
[编辑] 语法
引用变量声明是任何声明器拥有下列形式的简单声明
& attr(可选) declarator
|
(1) | ||||||||
&& attr(可选) declarator
|
(2) | (C++11 起) | |||||||
D
为到 decl-specifier-seq 所确定类型 S
的左值引用。D
为到 decl-specifier-seq 所确定类型 S
的右值引用。declarator | - | 任何其他引用声明器之外的声明器(无到引用的引用) |
attr(C++11) | - | 可选的属性列表 |
引用要求初始化为指代合法对象或函数:见引用初始化。
无到 void 的引用,且无到引用的引用。
引用类型不能在顶层为 cv 限定;声明中无为此而设的语法,且若通过 typedef 、 decltype 或模板参数引入限定,则忽略限定。
引用不是对象;它们不必占用存储,尽管若需要分配存储以实现所需语义(例如,引用类型的非静态数据成员通常会增加类的大小,量为存储内存地址所需),则编译器会这么做。
因为引用不是对象,故无引用的数组,无指向引用的指针,无到引用的引用:
int& a[3]; // 错误 int&* p; // 错误 int& &r; // 错误
引用折叠容许通过模板或 typedef 中的类型操作组成到引用的引用,该情况下应用引用折叠规则:到右值引用的右值引用折叠成右值引用,所有其他组合组成左值引用: 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); // calls f(int& x) f(std::move(x)); // calls f(int&& x) }
这允许移动构造函数、移动赋值运算符和其他具移动函数(例如 vector::push_back() )在适合时自动被选择。 |
(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<T>(x) 是左值 f(0); // 参数是右值,调用 f<int>(int&&), std::forward<T>(x) 是右值 } template<class T> int g(const T&& y); // y 不是转发引用: const T 不恰是 T template<class T> struct A { template<class U> A(T&& x, U&& y, int* p); // x 不是转发引用而 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 起) |
[编辑] 悬垂引用
尽管引用一旦初始化,就始终引用合法对象或函数,但可以创建在被指代对象的生存期结束,但引用保持可访问(悬垂)的程序。访问这种引用是未定义行为。 一个例子是返回到自动变量的引用的函数:
std::string& f() { std::string s = "Example"; return s; // 退出 s 的作用域: // 调用其析构函数并解分配其存储 } std::string& r = f(); // 悬垂引用 std::cout << r; // 未定义行为:从悬垂引用读取 std::string s = f(); // 未定义行为:从悬垂引用复制初始化
注意右值引用和到 const 的左值引用能延长临时对象的生存期(参阅临时量生存期的规则和例外)。
若被指代对象被销毁(例如通过显式的析构函数调用),但存储未被解分配,则到生存期外对象的引用仍能以限定的方式使用,且可以变得合法,若在同一存储重新创建对象(细节见生存期外访问)。