docs/lang/reference.md
声明具名变量为引用,即既存对象或函数的别名.
引用可以看成是 C++ 封装的非空指针,可以用来传递它所指向的对象,在声明时必须指向对象.
引用不是对象,因此不存在引用的数组、无法获取引用的指针,也不存在引用的引用.
??? note "引用类型不属于对象类型"
如果想让引用能完成一般的复制、赋值等操作,比如作为容器元素,则需要 reference_wrapper,通常维护一个非空指针实现.
引用主要分为两种,左值引用和右值引用.
??? note "左值和右值" 对左值和右值的讲解,请参考 值类别 页面.
通常我们会接触到的引用为左值引用,即绑定到左值的引用,同时 const 限定的左值引用可以绑定右值.以下是来自 参考手册 的一段示例代码.
#include <iostream>
#include <string>
int main() {
std::string s = "Ex";
std::string& r1 = s;
const std::string& r2 = s;
r1 += "ample"; // 修改 r1,即修改了 s
// r2 += "!"; // 错误:不能通过到 const 的引用修改
std::cout << r2 << '\n'; // 打印 r2,访问了s,输出 "Example"
}
左值引用最常用的地方是函数参数,用于避免不需要的拷贝.
#include <iostream>
#include <string>
// 参数中的 s 是引用,在调用函数时不会发生拷贝
char& char_number(std::string& s, std::size_t n) {
s += s; // 's' 与 main() 的 'str'
// 是同一对象,此处还说明左值也是可以放在等号右侧的
return s.at(n); // string::at() 返回 char 的引用
}
int main() {
std::string str = "Test";
char_number(str, 1) = 'a'; // 函数返回是左值,可被赋值
std::cout << str << '\n'; // 此处输出 "TastTest"
}
右值引用是绑定到右值的引用,用于移动对象,也可以用于 延长临时对象生存期.
#include <iostream>
#include <string>
using namespace std;
int main() {
string s1 = "Test";
// string&& r1 = s1; // 错误:不能绑定到左值,需要 std::move 或者 static_cast
const string& r2 = s1 + s1; // 可行:到常量的左值引用延长生存期
// r2 += "Test"; // 错误:不能通过到常量的引用修改
cout << r2 << '\n';
string&& r3 = s1 + s1; // 可行:右值引用延长生存期
r3 += "Test";
cout << r3 << '\n';
const string& r4 = r3; // 右值引用可以转换到 const 限定的左值
cout << r4 << '\n';
string& r5 = r3; // 右值引用可以转换到左值
cout << r5 << '\n';
}
当引用指代的对象已经销毁,引用就会变成悬垂引用,访问悬垂引用这是一种未定义行为,可能会导致程序崩溃.
以下为常见的悬垂引用的例子:
引用局部变量
#include <iostream>
int& foo() {
int a = 1;
return a;
}
int main() {
int& b = foo();
std::cout << b << std::endl; // 未定义行为
}
解分配导致的悬垂引用
#include <iostream>
int main() {
int* ptr = new int(10);
int& ref = *ptr;
delete ptr;
std::cout << ref << std::endl; // 未定义行为
}
内存重分配导致的悬垂引用
#include <iostream>
int main() {
std::string str = "hello";
const char& ref = str.front();
str.append("world"); // 可能会重新分配内存,导致 ref 指向的内存被释放
std::cout << ref << std::endl; // 未定义行为
}
类似 std::vector,std::unordered_map 等容器的插入操作,均有可能导致内存重新分配.
使用引用时,应时刻关注引用指向的对象的生命周期,避免造成悬垂引用.
通常静态检查工具和良好的代码习惯能让我们避免悬垂引用的问题.
常见的 非轻量对象 有:
vector,array,map 等string而对 轻量对象 使用引用不能带来任何好处,引用类型作为参数的空间占用大小,甚至可能会比类型本身还大.
这可能会带来些的性能负担,同时可能会阻止编译器优化.
以下属于 轻量对象
int,float 等使用 std::move 转移 对象的所有权.这通常见于局部变量之间,或参数与局部变量之间:
#include <iostream>
#include <string>
#include <vector>
using namespace std;
string world(string str) { return std::move(str) += " world!"; }
int main() {
// 1
cout << world("hello") << '\n';
vector<string> vec0;
// 2
{
string&& size = to_string(vec0.size());
size += ", " + to_string(size.size());
vec0.emplace_back(std::move(size));
}
cout << vec0.front();
}
但不是所有时候都需要这么做,比如 函数返回值优化.
从语义上,临时量可能会带来的额外的复制或移动,尽管多数情况下编译器能通过 复制消除 进行优化,但引用能强制编译器不进行这些多余操作,避免不确定性.