声明具名变量为引用,即既存对象或函数的别名。
引用变量声明是声明符拥有下列形式的简单声明
& 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; // 错误
引用坍缩 (C++11 起)
容许通过模板或 typedef 中的类型操作构成引用的引用,这种情况下适用引用坍缩(reference coolapsing)规则:
右值引用的右值引用坍缩成右值引用,所有其他组合均坍缩成左值引用:
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 可行的规则。)
左值引用
左值引用可用于建立既存对象的别名(可选地拥有不同的 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';
}
右值引用 (C++11 起)
右值引用可用于为临时对象延长生存期(注意,左值引用亦能延长临时对象生存期,但不能通过左值引用修改它们):
#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)
}
这允许在适当时机自动选择移动构造函数、移动赋值运算符和其他具移动能力的函数(例如 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 转发实参。转发引用是下列之一:
函数模板的函数形参,其被声明为同一函数模板的类型模板形参的无 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 是转发引用
};
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。
悬垂引用
尽管引用一旦初始化,就始终指代一个有效的对象或函数,但有可能创建一个程序,被指代对象的生存期结束,
但引用仍保持可访问(悬垂(dangling))。访问这种引用是未定义行为。 一个常见例子是返回自动变量的引用的函数:
std::string& f()
{
std::string s = "Example";
return s; // 退出 s 的作用域: // 调用其析构函数并解分配其存储
}
std::string& r = f(); // 悬垂引用
std::cout << r; // 未定义行为:从悬垂引用读取
std::string s = f(); // 未定义行为:从悬垂引用复制初始化
注意,右值引用和到 const 的左值引用能延长临时对象的生存期(参阅引用初始化中的规则和例外情况)。
若被指代对象被销毁(例如通过显式的析构函数调用),但存储尚未被解分配,则到生存期外的对象的引用仍能以有限的方式使用,
且当在同一存储中重新创建对象时也可以变为有效(细节见在生存期之外进行访问)。