引用(Reference)是 C++ 中的一个重要特性,它为已存在的变量提供了一个别名。引用在很多方面比指针更安全、更易用,是现代 C++ 编程的重要组成部分。
引用的基本概念
什么是引用
引用可以看作是一个已存在变量的别名。对引用的任何操作,实际上都是对它所引用的原始变量的操作。
#include <iostream>
int main() {
// 普通变量
int x = 42;
// 引用声明:必须在声明时初始化
int& ref = x; // ref 是 x 的引用(别名)
std::cout << "引用的基本概念:" << std::endl;
std::cout << "x = " << x << std::endl;
std::cout << "ref = " << ref << std::endl;
std::cout << "&x = " << &x << std::endl;
std::cout << "&ref = " << &ref << std::endl; // 引用和原变量有相同的地址
// 通过引用修改值
ref = 100;
std::cout << "\n通过引用修改后:" << std::endl;
std::cout << "x = " << x << std::endl; // x 也变成了 100
std::cout << "ref = " << ref << std::endl; // ref 也是 100
// 通过原变量修改值
x = 200;
std::cout << "\n通过原变量修改后:" << std::endl;
std::cout << "x = " << x << std::endl; // x 是 200
std::cout << "ref = " << ref << std::endl; // ref 也是 200
return 0;
}
引用的特性
引用有一些严格的规则,这既是它的特点,也是其安全性的来源。
#include <iostream>
int main() {
int a = 10;
int b = 20;
// 1. 引用必须在声明时初始化
int& ref1 = a; // 正确
// int& ref2; // 错误!引用必须初始化
// 2. 引用一旦初始化就不能重新绑定到其他变量
int& ref2 = a;
ref2 = b; // 这不是重新绑定,而是将 b 的值赋给 a
std::cout << "引用的特性演示:" << std::endl;
std::cout << "a = " << a << ", b = " << b << std::endl; // a = 20, b = 20
// 3. 引用不是独立的对象,不占用独立的内存空间(或与原对象共享)
std::cout << "sizeof(a) = " << sizeof(a) << std::endl;
std::cout << "sizeof(ref1) = " << sizeof(ref1) << std::endl; // 大小与被引用的对象 a 相同
// 4. 不能创建引用的引用
// int&& ref_to_ref = ref1; // 这是右值引用,是C++11的新特性,不是引用的引用
// 5. 不能创建引用的数组
// int& ref_array[3]; // 错误!数组元素必须是对象,而引用不是
// 6. 不能创建指向引用的指针
// int&* ptr_to_ref; // 错误!引用没有地址,不能被指针指向
// 7. 但可以从引用创建指针(实际上是指向被引用对象的指针)
int* ptr = &ref1; // 实际上是获取 ref1 所引用的对象 a 的地址
std::cout << "*ptr = " << *ptr << std::endl; // 输出 20
return 0;
}
引用与指针的比较
引用和指针都能实现间接访问,但它们在语法、安全性和灵活性上存在显著差异。
语法和使用差异
#include <iostream>
void demonstratePointerVsReference() {
int x = 10;
int y = 20;
// 指针方式
int* ptr = &x; // 指针需要使用 '&' 取地址
std::cout << "指针方式:" << std::endl;
std::cout << "通过指针访问: *ptr = " << *ptr << std::endl; // 访问值需要使用 '*' 解引用
*ptr = 30; // 通过指针修改值
std::cout << "修改后 x = " << x << std::endl;
ptr = &y; // 指针可以重新指向其他变量
std::cout << "重新指向后: *ptr = " << *ptr << std::endl;
// 引用方式
int& ref = x; // 引用直接绑定
std::cout << "\n引用方式:" << std::endl;
std::cout << "通过引用访问: ref = " << ref << std::endl; // 像普通变量一样直接使用
ref = 40; // 直接修改值
std::cout << "修改后 x = " << x << std::endl;
// ref = y; // 这不是重新绑定,而是赋值操作,会把 y 的值赋给 x
// 空值比较
int* null_ptr = nullptr; // 指针可以为空
// int& null_ref = nullptr; // 错误!引用不能为空,必须绑定到有效的对象
std::cout << "\n安全性比较:" << std::endl;
if (null_ptr == nullptr) {
std::cout << "指针可能为空,使用前需要检查" << std::endl;
}
// 引用总是有效的,通常不需要检查
std::cout << "引用总是有效: ref = " << ref << std::endl;
}
int main() {
demonstratePointerVsReference();
return 0;
}
性能比较
在底层,编译器通常会将引用实现为指针,因此它们的性能几乎没有差别。代码的可读性和安全性是选择引用而非指针的主要原因。
#include <iostream>
#include <chrono>
#include <vector>
// 使用指针的函数
void processWithPointer(const std::vector<int>* vec) {
if (vec) { // 需要检查空指针
for (size_t i = 0; i < vec->size(); ++i) {
// 使用 -> 操作符
volatile int temp = (*vec)[i]; // 防止编译器优化掉循环
}
}
}
// 使用引用的函数
void processWithReference(const std::vector<int>& vec) {
// 不需要检查,引用总是有效的(假设传入的是有效对象)
for (size_t i = 0; i < vec.size(); ++i) {
// 直接使用 . 操作符
volatile int temp = vec[i]; // 防止编译器优化掉循环
}
}
int main() {
std::vector<int> data(1000000);
for (size_t i = 0; i < data.size(); ++i) {
data[i] = static_cast<int>(i);
}
const int iterations = 100;
// 测试指针性能
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < iterations; ++i) {
processWithPointer(&data);
}
auto end = std::chrono::high_resolution_clock::now();
auto pointer_time = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
// 测试引用性能
start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < iterations; ++i) {
processWithReference(data);
}
end = std::chrono::high_resolution_clock::now();
auto reference_time = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
std::cout << "性能比较 (" << iterations << " 次迭代):" << std::endl;
std::cout << "指针方式: " << pointer_time.count() << " 微秒" << std::endl;
std::cout << "引用方式: " << reference_time.count() << " 微秒" << std::endl;
return 0;
}
引用作为函数参数
这是引用最重要的用途,它可以避免不必要的对象拷贝,并允许函数修改调用方的变量。
传值、传指针、传引用比较
#include <iostream>
class LargeObject {
public:
int data[1000]; // 大对象
LargeObject(int value = 0) {
for (int i = 0; i < 1000; ++i) {
data[i] = value + i;
}
std::cout << "LargeObject 构造函数调用" << std::endl;
}
LargeObject(const LargeObject& other) {
for (int i = 0; i < 1000; ++i) {
data[i] = other.data[i];
}
std::cout << "LargeObject 拷贝构造函数调用" << std::endl;
}
~LargeObject() {
std::cout << "LargeObject 析构函数调用" << std::endl;
}
};
// 1. 传值方式(会发生昂贵的拷贝)
void processByValue(LargeObject obj) {
std::cout << "processByValue: 第一个元素 = " << obj.data[0] << std::endl;
obj.data[0] = 999; // 只修改副本,不影响原对象
}
// 2. 传指针方式(高效,但语法繁琐且需空指针检查)
void processByPointer(LargeObject* obj) {
if (obj) {
std::cout << "processByPointer: 第一个元素 = " << obj->data[0] << std::endl;
obj->data[0] = 888; // 修改原对象
}
}
// 3. 传引用方式(高效,语法简洁)
void processByReference(LargeObject& obj) {
std::cout << "processByReference: 第一个元素 = " << obj.data[0] << std::endl;
obj.data[0] = 777; // 修改原对象
}
// 4. 传常量引用(只读访问,高效且安全,是最佳实践)
void processByConstReference(const LargeObject& obj) {
std::cout << "processByConstReference: 第一个元素 = " << obj.data[0] << std::endl;
// obj.data[0] = 666; // 错误!不能修改 const 引用
}
int main() {
std::cout << "=== 创建对象 ===" << std::endl;
LargeObject obj(100);
std::cout << "\n=== 传值调用 ===" << std::endl;
processByValue(obj); // 会调用拷贝构造函数
std::cout << "原对象第一个元素: " << obj.data[0] << std::endl; // 仍然是 100
std::cout << "\n=== 传指针调用 ===" << std::endl;
processByPointer(&obj); // 不会拷贝
std::cout << "原对象第一个元素: " << obj.data[0] << std::endl; // 变为 888
std::cout << "\n=== 传引用调用 ===" << std::endl;
processByReference(obj); // 不会拷贝
std::cout << "原对象第一个元素: " << obj.data[0] << std::endl; // 变为 777
std::cout << "\n=== 传常量引用调用 ===" << std::endl;
processByConstReference(obj); // 不会拷贝,且保证不修改
std::cout << "\n=== 程序结束 ===" << std::endl;
return 0;
}
引用参数的实际应用
引用在实现“返回”多个值、修改传入对象状态等场景中非常实用。
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
#include <cctype>
// 交换函数,直接修改传入的变量
void swap(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
// 通过引用参数“返回”查找结果
void findMinMax(const std::vector<int>& vec, int& min_val, int& max_val) {
if (vec.empty()) return;
min_val = max_val = vec[0];
for (size_t i = 1; i < vec.size(); ++i) {
if (vec[i] < min_val) min_val = vec[i];
if (vec[i] > max_val) max_val = vec[i];
}
}
// 字符串处理函数,直接修改传入的字符串
void processString(std::string& str) {
// 转换为大写
std::transform(str.begin(), str.end(), str.begin(), ::toupper);
// 移除空格 (Erase-Remove Idiom)
str.erase(std::remove(str.begin(), str.end(), ' '), str.end());
}
// 数组排序函数
void bubbleSort(std::vector<int>& arr) {
size_t n = arr.size();
for (size_t i = 0; i < n - 1; ++i) {
for (size_t j = 0; j < n - i - 1; ++j) {
if (arr[j] > arr[j + 1]) {
swap(arr[j], arr[j + 1]); // 使用引用交换
}
}
}
}
int main() {
// 测试交换函数
std::cout << "=== 测试交换函数 ===" << std::endl;
int x = 10, y = 20;
std::cout << "交换前: x = " << x << ", y = " << y << std::endl;
swap(x, y);
std::cout << "交换后: x = " << x << ", y = " << y << std::endl;
// 测试查找最值函数
std::cout << "\n=== 测试查找最值函数 ===" << std::endl;
std::vector<int> numbers = {5, 2, 8, 1, 9, 3};
int min_val, max_val;
findMinMax(numbers, min_val, max_val);
std::cout << "最小值: " << min_val << ", 最大值: " << max_val << std::endl;
// 测试字符串处理函数
std::cout << "\n=== 测试字符串处理函数 ===" << std::endl;
std::string text = "Hello World C++";
std::cout << "处理前: " << text << std::endl;
processString(text);
std::cout << "处理后: " << text << std::endl;
// 测试排序函数
std::cout << "\n=== 测试排序函数 ===" << std::endl;
std::cout << "排序前: ";
for (int num : numbers) std::cout << num << " ";
std::cout << std::endl;
bubbleSort(numbers);
std::cout << "排序后: ";
for (int num : numbers) std::cout << num << " ";
std::cout << std::endl;
return 0;
}
引用作为返回值
函数可以返回一个引用,这使得函数调用可以出现在赋值语句的左边。
返回引用的基本用法
#include <iostream>
#include <vector>
class IntArray {
private:
std::vector<int> m_data;
public:
IntArray(size_t size) : m_data(size) {}
// 返回一个引用,允许对数组成员进行读写
int& operator[](size_t index) {
return m_data[index];
}
// 为 const 对象提供一个只读版本
const int& operator[](size_t index) const {
return m_data[index];
}
};
int main() {
IntArray arr(5);
// 调用返回引用的 operator[] 来赋值
arr[0] = 10;
arr[1] = 20;
// 调用返回引用的 operator[] 来读取
std::cout << "arr[0] = " << arr[0] << std::endl;
std::cout << "arr[1] = " << arr[1] << std::endl;
const IntArray const_arr = arr;
// const_arr[0] = 99; // 错误!调用的是 const 版本,返回 const int&,不能修改
std::cout << "const_arr[0] = " << const_arr[0] << std::endl;
return 0;
}
返回引用的风险
最严重的错误是返回一个局部变量的引用。当函数执行完毕后,局部变量被销毁,返回的引用就成了一个“悬挂引用”,指向无效内存。
#include <iostream>
// !!! 严重错误:不要这样做 !!!
int& createAndGetValue() {
int local_value = 42;
return local_value; // 返回局部变量的引用
} // local_value 在这里被销毁
// 正确的做法:可以返回一个静态变量的引用
int& getStaticValue() {
static int static_value = 100;
return static_value;
}
int main() {
// 访问悬挂引用是未定义行为
// int& dangling_ref = createAndGetValue();
// std::cout << dangling_ref << std::endl; // 可能崩溃,可能输出垃圾值
// 访问静态变量的引用是安全的
int& static_ref = getStaticValue();
std::cout << "静态引用: " << static_ref << std::endl; // 输出 100
static_ref = 200;
std::cout << "修改后静态引用: " << getStaticValue() << std::endl; // 输出 200
return 0;
}
总结: 只有当返回的对象在函数调用结束后仍然存在时(例如,它是静态变量、全局变量,或者是通过引用传入的参数的一部分),返回引用才是安全的。