前面我们学了指针——C++ 中最强大也最容易出错的特性。这一节我们来学两个让代码更安全、更易读的工具:引用const 限定符

1. 引用(Reference)

1.1 什么是引用?

引用是变量的「别名」——给一个已存在的变量取另一个名字。对引用的操作,就是对原变量的操作。

int a = 10;
int& b = a;  // b 是 a 的引用(别名)

b = 20;      // 修改 b,就是修改 a
cout << a;   // 输出 20

关键点

  • 引用必须在声明时初始化
  • 引用一旦绑定到某个变量,就不能再绑定到其他变量
  • 引用不是新变量,它不占用额外的存储空间(概念上)

1.2 引用 vs 指针

引用和指针都能间接访问变量,但有本质区别:

int a = 10;
int* p = &a;   // 指针:需要取地址,需要解引用
int& r = a;    // 引用:直接用,语法更简洁

*p = 20;       // 指针需要 * 来解引用
r = 30;        // 引用直接用,就像原变量一样
特性 引用 指针
初始化 必须在声明时初始化 可以不初始化(但不推荐)
可否为空 不能为 null 可以为 null
可否改变指向 不能 可以
语法 和原变量一样 需要 *&
安全性 更安全 容易出错

简单规则:能用引用就不用指针。

1.3 引用作为函数参数

引用最常见的用途是作为函数参数,实现「传引用」:

// 传值:函数内修改不影响外部
void swapBad(int a, int b) {
    int temp = a;
    a = b;
    b = temp;
    // 外部的 a 和 b 不会变!
}

// 传引用:函数内修改影响外部
void swapGood(int& a, int& b) {
    int temp = a;
    a = b;
    b = temp;
    // 外部的 a 和 b 交换了!
}

int main() {
    int x = 10, y = 20;
    swapGood(x, y);
    cout << x << " " << y;  // 输出:20 10
}

1.4 引用作为函数返回值

函数可以返回引用,但要注意:不要返回局部变量的引用

// ❌ 错误:返回局部变量的引用
int& badFunction() {
    int local = 42;
    return local;  // local 在函数结束后被销毁,引用变成悬空引用
}

// ✅ 正确:返回全局变量或静态变量的引用
int globalVar = 100;
int& getGlobal() {
    return globalVar;  // 全局变量不会被销毁
}

// ✅ 正确:返回参数的引用
int& getElement(vector<int>& arr, int index) {
    return arr[index];  // 数组元素在调用者的作用域中
}

返回引用的函数调用可以出现在赋值号左边:

vector<int> arr = {1, 2, 3, 4, 5};
getElement(arr, 2) = 99;  // arr[2] 变成 99
cout << arr[2];           // 输出 99

1.5 const 引用

const 修饰引用,表示「只读」:

void printValue(const int& x) {
    // x = 100;  // ❌ 编译错误:不能修改 const 引用
    cout << x;   // ✅ 只能读取
}

int main() {
    int a = 42;
    printValue(a);  // 正常传入

    printValue(100);  // 也可以传入字面量!
    // 普通引用不能绑定到字面量,const 引用可以
}

const 引用的好处

  • 防止函数意外修改参数
  • 避免拷贝,提高效率
  • 可以绑定到字面量和临时对象

2. const 限定符

2.1 const 变量

const 表示「常量」,一旦初始化就不能修改:

const int MAX_SIZE = 100;
// MAX_SIZE = 200;  // ❌ 编译错误

const string NAME = "Alice";
// NAME = "Bob";    // ❌ 编译错误

const 变量必须在声明时初始化,因为之后无法赋值。

2.2 const 与指针

const 和指针的组合是初学者最容易混淆的地方。记住一个口诀:const 在 * 左边,指向的内容不能改;const 在 * 右边,指针本身不能改

int a = 10, b = 20;

// 1. 指向 const 的指针(内容只读)
const int* p1 = &a;
// *p1 = 30;    // ❌ 不能通过 p1 修改 a
p1 = &b;       // ✅ p1 可以指向其他变量
a = 30;        // ✅ 直接修改 a 是可以的

// 2. const 指针(指针本身只读)
int* const p2 = &a;
*p2 = 30;      // ✅ 可以通过 p2 修改 a
// p2 = &b;    // ❌ p2 不能指向其他变量

// 3. 指向 const 的 const 指针(都不能改)
const int* const p3 = &a;
// *p3 = 30;   // ❌
// p3 = &b;    // ❌

记忆方法

  • const int*:const 修饰 int,所以 int 不能改 → 内容只读
  • int* const:const 修饰 int*,所以指针不能改 → 指针只读

2.3 const 成员函数

在类中,const 放在函数声明末尾,表示这个函数不会修改对象的状态:

class Circle {
private:
    double radius;

public:
    Circle(double r) : radius(r) {}

    // const 成员函数:不会修改对象
    double getArea() const {
        // radius = 10;  // ❌ 不能在 const 函数中修改成员变量
        return 3.14159 * radius * radius;
    }

    // 非 const 成员函数:可以修改对象
    void setRadius(double r) {
        radius = r;
    }
};

void printArea(const Circle& c) {
    // c 只能调用 const 成员函数
    cout << c.getArea() << endl;
    // c.setRadius(5);  // ❌ 不能调用非 const 函数
}

规则:如果一个函数不修改对象状态,就把它声明为 const。这样它就能被 const 引用和 const 指针调用。

2.4 常量表达式(constexpr)

C++11 引入了 constexpr,比 const 更严格:

const int x = 10;           // x 是常量
constexpr int y = 20;       // y 是编译期常量

const int z = getSomeValue();  // ✅ 运行时确定的常量也可以
// constexpr int w = getSomeValue();  // ❌ 必须编译期能确定

constexpr int square(int n) {
    return n * n;
}

constexpr int result = square(5);  // 编译期计算,result = 25

constexpr 告诉编译器:这个值在编译期就能确定,可以做更多优化。

2.5 mutable 关键字

有时候你需要在 const 成员函数中修改某些特定的成员变量(比如缓存、计数器)。mutable 就是为此设计的:

class DataFetcher {
private:
    mutable int accessCount = 0;  // 即使在 const 函数中也能修改
    string data;

public:
    string getData() const {
        accessCount++;  // ✅ mutable 变量可以在 const 函数中修改
        return data;
    }

    int getAccessCount() const {
        return accessCount;
    }
};

3. 引用与 const 的实际应用

3.1 函数参数的最佳实践

// ✅ 最佳实践:按需选择参数传递方式

// 基本类型(int、double 等):传值
void processInt(int x) { /* ... */ }

// 不需要修改的大对象:const 引用
void printVector(const vector<int>& vec) {
    for (int x : vec) cout << x << " ";
}

// 需要修改的参数:引用
void sortVector(vector<int>& vec) {
    sort(vec.begin(), vec.end());
}

// 可能为空的参数:指针
void processMaybeNull(int* ptr) {
    if (ptr != nullptr) {
        // 处理数据
    }
}

3.2 返回值的最佳实践

class Matrix {
private:
    vector<vector<int>> data;

public:
    // 返回 const 引用:只读访问
    const vector<int>& getRow(int i) const {
        return data[i];
    }

    // 返回非 const 引用:可修改访问
    vector<int>& getRow(int i) {
        return data[i];
    }

    // 返回值:小对象或需要返回新对象时
    int getElement(int i, int j) const {
        return data[i][j];
    }
};

3.3 范围 for 循环中的引用

vector<int> arr = {1, 2, 3, 4, 5};

// 只读遍历:const 引用
for (const int& x : arr) {
    cout << x << " ";  // 高效,不拷贝
}

// 修改遍历:非 const 引用
for (int& x : arr) {
    x *= 2;  // 原地修改每个元素
}

// 不用引用:会拷贝每个元素(对于大对象效率低)
for (int x : arr) {
    cout << x << " ";  // 基本类型没问题,string 等大对象会有额外拷贝
}

4. 常见陷阱

4.1 悬空引用

int& getDangling() {
    int local = 42;
    return local;  // ⚠️ 返回局部变量的引用
}

int main() {
    int& ref = getDangling();
    cout << ref;  // 未定义行为!可能输出垃圾值
}

4.2 const 丢失

const int a = 10;
const int* p = &a;

// int* bad = p;      // ❌ 编译错误:不能丢弃 const
int* bad = const_cast<int*>(p);  // ⚠️ 强制转换,但修改 a 是未定义行为

4.3 引用的引用

int a = 10;
int& r1 = a;
int& r2 = r1;  // r2 是 a 的引用,不是 r1 的引用
r2 = 20;
cout << a;     // 输出 20

5. 小结

本节我们学习了引用和 const 限定符:

  • 引用是变量的别名,必须初始化,不能重新绑定
  • const 引用可以绑定到字面量,防止意外修改
  • const 指针有两种:指向内容的 const 和指针本身的 const
  • const 成员函数表示不修改对象状态
  • constexpr 是编译期常量,比 const 更严格

最佳实践

  • 函数参数优先用 const 引用(只读)或 引用(需修改)
  • 不修改对象的成员函数都声明为 const
  • 能用引用就不用指针
  • constexpr 代替 const 定义编译期常量

下一节我们将学习类与对象基础——C++ 面向对象编程的起点。

更多推荐