引用(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;
}

总结: 只有当返回的对象在函数调用结束后仍然存在时(例如,它是静态变量、全局变量,或者是通过引用传入的参数的一部分),返回引用才是安全的。