C++ 顶层 const 和底层 const

一、基本概念

在 C++ 中,const 限定符有两种不同的用法,分别称为顶层 const底层 const

  • 顶层 const:修饰的是对象本身,表示对象本身是常量,不能修改。例如:const int x = 5;x 就是顶层 const。
  • 底层 const:表示指针或引用所指向的对象是常量,不能通过该指针或引用修改对象。例如:const int *p;const 修饰的是 int(被指向的对象),这是底层 const。

简单记法:当 const 出现在指针声明中并修饰 * 左侧的类型(const int* / int const*)时是底层 const;当 const 修饰整个对象(如 int* const 中修饰指针本身)时属于顶层 const


二、参数传递

2.1 基本原则

用实参初始化形参时,会忽略掉顶层 const。也就是说,当形参有顶层 const 时,传给它常量对象或者非常量对象都是可以的。

void f(int)void f(const int) 在函数类型上等价,不能用顶层 const 来重载。

底层 const 必须匹配或允许转换,例如 const T* 可以接受 T*const T* 实参,但 T* 不能接受 const T*)。

2.2 各种参数传递情况分析

① 按值传递 - 普通参数

#include <iostream>
using namespace std;

// int cube(int i):实参可以为 int 类型,也可以为 const int 类型
// 在函数体中可以修改 i 的值,函数不会修改实参的值
int cube(int i) {
    i = i + 1;  // 可以修改,不影响实参
    return i * i * i;
}

// ② int cube(const int i):实参可以为 int 类型,也可以为 const int 类型
// 在函数体中不可以修改 i 的值,函数不会修改实参的值
int cube2(const int i) {
    // i = 0;  // 错误:不能修改 const 参数
    return i * i * i;
}

调用:无论实参是否 constintconst int),都可以传递给上面两个函数。因为顶层 const 会被忽略用于匹配参数类型。

按值参数上的顶层 const 主要是给读代码的人看的,表示函数不会修改拷贝。

② 指针参数 - 不同 const 修饰

  • int *p; — 指针可变,指向的内容可变。
  • const int *p; — 指针可变,但不能通过 p 修改被指向的 int(底层 const)。
  • int * const p; — 指针本身是常量(顶层 const),指针不能再指向别处,但可以修改 *p
  • const int * const p; — 指针本身不可变,指向的内容也不可变。
// int p_cube(int* pi):实参为 int 类型的指针,不能为 const int 类型
// 可以通过修改 *pi 的值修改实参指向的对象的值,修改 pi 对实参没有影响
int p_cube(int* pi) {
    *pi = 10;  // 修改传入参数实际地址指向的值
    return *pi * (*pi) * (*pi);
}

// int p_cube2(const int* pi):底层 const
// 实参可以为 const int 类型,也可以为 int 类型
// 不可以通过修改 *pi 的值修改实参指向的对象的值,修改 pi 对实参没有影响
int p_cube2(const int* pi) {
    // *pi = 10;  // 错误:不能通过 const 指针修改值
    return *pi * (*pi) * (*pi);
}

// int p_cube3(int* const pi):顶层 const
// 实参为 int 类型,不能为 const int 类型
// 可以通过修改 *pi 的值修改实参指向的对象值,不可以给 pi 赋值
int p_cube3(int* const pi) {
    *pi = 20;  // 可以:可以修改指针指向的值
    // pi = nullptr;  // 错误:不能修改指针本身(顶层 const)
    return *pi * (*pi) * (*pi);
}

2.3 完整测试示例

#include <iostream>
using namespace std;

// 函数声明
int cube(int i);
int cube2(const int i);
int p_cube(int* pi);
int p_cube2(const int* pi);
int p_cube3(int* const pi);

int main() {
    int num1 = 10;      // int 类型
    const int num2 = 11; // const int 类型

    // 测试按值传递
    cout << "按值传递测试:" << endl;
    // 实参是 int 类型和 const int 类型都是可以的
    cout << "cube(num1): " << cube(num1) << endl;
    cout << "cube(num2): " << cube(num2) << endl;
    cout << "cube2(num1): " << cube2(num1) << endl;
    cout << "cube2(num2): " << cube2(num2) << endl;

    cout << "\n指针参数测试:" << endl;
    // 测试指针参数
    int temp1 = 5;
    const int temp2 = 6;
    
    // p_cube:实参只能为 int 类型
    cout << "p_cube(&temp1): " << p_cube(&temp1) << endl;
    // cout << p_cube(&temp2) << endl;  // 错误:不能传递 const int* 给 int*
    
    // p_cube2:实参可以是 int 类型,也可以是 const int 类型
    cout << "p_cube2(&temp1): " << p_cube2(&temp1) << endl;
    cout << "p_cube2(&temp2): " << p_cube2(&temp2) << endl;
    
    // p_cube3:实参只能为 int 类型
    cout << "p_cube3(&temp1): " << p_cube3(&temp1) << endl;
    // cout << p_cube3(&temp2) << endl;  // 错误:不能传递 const int* 给 int* const

    return 0;
}

三、const 引用传递的性能优势

3.1 自定义类型的参数传递

对于自定义类型(如类对象),按值传递参数会触发对象的拷贝构造函数,产生额外的内存开销和性能损耗。而使用 const 引用作为参数,既能避免拷贝,又能保证对象不被修改。

#include <iostream>
using namespace std;

class LargeObject {
public:
    int data[1000]; // 假设是一个大对象
    LargeObject() { 
        // 初始化逻辑
        for (int i = 0; i < 1000; ++i) {
            data[i] = i;
        }
        cout << "LargeObject 构造函数被调用" << endl;
    }
    
    // 拷贝构造函数
    LargeObject(const LargeObject& other) {
        for (int i = 0; i < 1000; ++i) {
            data[i] = other.data[i];
        }
        cout << "LargeObject 拷贝构造函数被调用" << endl;
    }
};

// 按值传递:会拷贝整个 LargeObject,效率低
void processByValue(LargeObject obj) {
    cout << "processByValue: 对象大小 = " << sizeof(obj) << " 字节" << endl;
}

// const 引用传递:无拷贝,且不能修改 obj
void processByConstRef(const LargeObject& obj) {
    cout << "processByConstRef: 对象大小 = " << sizeof(obj) << " 字节" << endl;
    // obj.data[0] = 100;  // 错误:不能修改 const 引用对象
}

int main() {
    LargeObject lo;
    
    cout << "按值传递测试:" << endl;
    processByValue(lo); // 拷贝整个对象,效率低
    
    cout << "\nconst 引用传递测试:" << endl;
    processByConstRef(lo); // 无拷贝,效率高且安全
    
    return 0;
}

3.2 const 引用的灵活性

普通引用(非 const)只能绑定到可修改的左值(如变量),而 const 引用的灵活性更强:

  • T& —— 只能绑定到左值(可修改的变量)。
  • const T& —— 可以绑定到左值、常量、甚至临时对象(右值)。这是非常常用的传参方式(尤其适合大对象,避免拷贝且保证不修改)。
#include <iostream>
#include <string>
using namespace std;

void print(const string& s) {
    cout << s << endl;
}

string getString() {
    return "临时字符串";
}

int main() {
    // 1. 绑定到常量
    const int& ref1 = 100;  // 合法:const 引用可以绑定到字面量
    
    // 2. 绑定到临时对象(表达式结果)
    int a = 5, b = 3;
    const int& ref2 = a + b;  // 合法:a + b 的结果是临时对象,可被 const 引用绑定
    cout << "ref2 = " << ref2 << endl;
    
    // 3. 函数参数接收临时对象
    print("hello");  // 合法:字符串字面量是临时对象
    print(getString());  // 合法:函数返回的临时对象
    
    // 对比:普通引用不能绑定到常量或临时对象
    // int& ref3 = 100;        // 错误
    // int& ref4 = a + b;      // 错误
    // void badPrint(string& s); badPrint("hello");  // 错误
    
    return 0;
}

注意:非 const 引用(T&不能绑定到临时对象或字面量(例如 int& r = 5; 是错误的)。


四、const 成员函数 与 mutable

4.1 const 成员函数

void foo() const 声明成员函数,表示该函数不会修改对象的可观察状态(也就是不会修改非 mutable 成员),并且可以在 const 对象上调用。例如:

struct S {
    int x;
    int getX() const { return x; }   // 可以在 const S 对象上调用
};

4.2 mutable

如果某成员被声明为 mutable,则即便在 const 成员函数中也可以被修改(常用于缓存/惰性计算的内部状态):

struct Cache {
    mutable bool cached = false;
    mutable int value = 0;
    int get() const {
        if (!cached) { value = /* compute */ 42; cached = true; }
        return value;
    }
};

五、总结

const 引用的核心意义在于在保证效率(避免拷贝)的同时,通过限制修改操作来提高代码的安全性和可读性,并扩展了引用的适用场景(如绑定常量、临时对象)。它是 C++ 中一种"零成本抽象"的典型体现——既提供了安全性,又不带来额外的性能开销。

使用

  1. 函数参数传递:对于内置类型,按值传递即可;对于自定义类型,优先使用 const 引用传递
  2. 指针参数:根据是否需要修改指针本身或指向的内容,选择合适的 const 修饰
  3. 避免不必要的拷贝:对于大对象,总是使用 const 引用传递
  4. 提高代码安全性:使用 const 可以防止意外的修改,让代码意图更清晰
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值