const 是 C++ 类型系统中最精密的安全机制之一。表面上它只是“不可修改”,但在不同上下文中语义分化极大,且与指针、引用、成员函数、模板、重载决议深度交织。本文提炼 const 的高阶用法,并给出可直接用于编程与面试的二次结论


一、顶层 const 与底层 const

顶层 const 修饰对象本身不可变;底层 const 修饰所指或所引对象不可变。

int a = 10;
const int b = 20;         // 顶层 const
int *const p1 = &a;       // 顶层 const(指针本身不可变)
const int *p2 = &a;       // 底层 const(所指对象只读)
const int *const p3 = &a; // 左侧底层 const,右侧顶层 const

二次结论 1

  • 赋值或初始化时,顶层 const 可被忽略(拷贝时不保留顶层 const)。

  • 底层 const 必须严格匹配,不能将底层 const 的指针/引用转换为非 const

int i = 0;
const int ci = 1;
int *p = &ci;          // 错误:底层 const 不匹配
const int *pc = &i;    // 正确:添加底层 const 允许

二、const 与函数参数:常引用的三重优势

void f(const std::string &s);

二次结论 2
常引用参数同时实现:

  1. 避免拷贝(相比值传递);

  2. 保护实参不被修改(相比非 const 引用);

  3. 可接受右值与左值(非 const 引用不能绑定右值)。

因此,除明确需要修改实参或移动语义的场景外,大对象参数应优先选用 const T&


三、const 成员函数与重载

const 成员函数的本质:this 指针类型从 T* 变为 const T*

class Buffer {
public:
    char& operator[](size_t i) { return data[i]; }        // ①
    const char& operator[](size_t i) const { return data[i]; } // ②
};

二次结论 3

  • 编译器根据对象的 const 性质选择重载版本:const 对象调用 const 成员函数,非 const 对象优先调用非 const 版本。

  • 如果只定义了非 const 版本,const 对象将无法调用该函数,导致编译错误。

  • 同一功能的 const 与非 const 版本建议成对出现,非 const 版本可调用 const 版本消除重复。

典型实现技巧(const_cast 消除重复):

const char& operator[](size_t i) const { return data[i]; }
char& operator[](size_t i) {
    return const_cast<char&>(
        static_cast<const Buffer&>(*this)[i]
    );
}

二次结论 4
将非 const 版本委托给 const 版本是安全的;反向则会导致未定义行为(修改原本的 const 对象)。


四、mutable:突破 const 的逻辑边界

mutable 允许在 const 成员函数中修改被修饰的成员。

class Cache {
    mutable std::map<int, int> cache;
public:
    int query(int key) const {
        if (auto it = cache.find(key); it != cache.end())
            return it->second;
        // 计算并缓存
        cache[key] = compute(key); // 合法
        return cache[key];
    }
};

二次结论 5

  • mutable 应用于“逻辑上不影响对象外部状态”的成员(如缓存、互斥锁、引用计数)。

  • const 成员函数保证的是逻辑常量性,而非物理(比特)常量性。


五、const_cast 的正确使用场景与危险边界

const_cast 用于添加或移除 const/volatile 限定。仅当原始对象本身不是 const 时,移除 const 才安全。

void print(char *s); // 旧接口
const char *msg = "hello";
print(const_cast<char*>(msg)); // 未定义行为!msg 指向常量区

二次结论 6

  • const_cast 最常见的合理用途是消除 const/非 const 成员函数的代码重复(见上节)。

  • 对真正的 const 对象(如 const int a = 10; 或字符串字面量)使用 const_cast 并修改,是未定义行为


六、const 与引用绑定:临时量生命周期延长

常引用绑定右值时,临时对象的生命周期会延长至引用本身的生命周期。

std::string get();
const std::string &s = get(); // 临时对象生命周期延长

二次结论 7

  • 只有 const T&(或 T&&)能延长临时对象的生命周期,非 const 引用不能。

  • 该特性常用于避免不必要的拷贝,并在函数返回值的接收中提高效率。


七、const 与 volatile 的联合使用

const 表示“程序不应修改”,volatile 表示“硬件可能修改”。二者可以同时出现。

const volatile int *status_reg = ...;

二次结论 8

  • const volatile 修饰的对象只能读不能写(程序侧),但每次读取都会从实际地址加载(不优化缓存)。

  • 常见于内存映射 I/O、信号处理共享变量。


八、const 与 constexpr

const 是“只读”,constexpr 是“编译期常量”,二者正交但可组合。

const int a = f();    // 运行时只读
constexpr int b = 42; // 编译期常量
constexpr const int c = 42; // 编译期常量且只读

二次结论 9

  • 所有 constexpr 变量都隐式是 const,但 const 不一定是编译期常量。

  • 对于编译期已知的值,使用 constexpr 可以保证其在编译期参与常量表达式计算。


九、const 与模板类型推导

在模板类型推导中,顶层 const 被忽略,底层 const 保留。

template<typename T> void f(T t);
const int ci = 0;
f(ci); // T 推导为 int(顶层 const 丢失)
f(&ci); // T 推导为 const int*(底层 const 保留)

二次结论 10

  • 值传递时实参的 const 消失(因为副本不需要保持原对象的只读性)。

  • 引用传递可保留顶层 constvoid g(T& t)g(ci) 推导 T 为 const int

  • 当需要保留 const 特性时,优先使用 T&& 或 const T& 参数。


十、const 与重载决议优先级

const 影响重载决议的顺序。对于成员函数:

void f();        // 非 const
void f() const;  // const
  • 非 const 对象优先匹配非 const 版本,否则匹配 const 版本。

  • const 对象只能匹配 const 版本。

对于非成员函数,const 限定的指针/引用参数同样参与匹配。

二次结论 11
设计接口时,若对象状态不同需要不同行为,const 重载是最自然的表达;若行为一致,应将非 const 版本委托给 const 版本。


十一、综合防错清单

  1. 值传递函数不需要为参数加顶层 const(因为拷贝本身不会影响实参),在声明中写 void f(const int x) 无意义且会干扰重载决议。

  2. 函数返回 const 值类型通常无意义const int f() 会禁止移动,且接收方通常拷贝一份可修改对象。

  3. 常量成员变量必须在初始化列表中初始化const int x; 不能在构造函数体内赋值。

  4. 静态成员变量可以是非整型的 const,但需要在类外定义(除非 constexpr 且 inline)。

  5. const_iterator 与 const 迭代器不同const vector<int>::iterator 是迭代器本身不可变,而 vector<int>::const_iterator 是迭代器指向的元素不可变。

更多推荐