【C++】类型限定符const

Const的用法

const 的本质:类型系统的一部分

const 不是“运行时保护”,而是编译期的类型约束:约束“通过该名字/表达式能否修改某个对象”。
const 可以参与重载(尤其在成员函数与引用/指针参数中) 
const 不提供并发同步语义(不等价于线程安全),仅代表“逻辑上的只读”,而不代表“物理上的线程安全”。

const 修饰对象:必须初始化、不可再赋值

const int a = 10;   // 必须初始化
// a = 20;          // 错误:不可修改

要点:
定义时必须初始化(除非是某些特殊存储期/延迟初始化场景,但常规写法即“必须初始化”)。
这是“对象不可赋值”的约束:并不意味着对象所在内存不可写(那是更底层的系统层语义)。

const 修饰指针/引用:顶层 const 与底层 const

这部分建议用“const 修饰的是谁”来稳定理解:
看 * 的左右:
* 左边:约束“指向的对象”(底层 const)
* 右边:约束“指针本身”(顶层 const
int a = 10;
int* const p1 = &a;        // 顶层const:p1 不可重新指向
const int* p2 = &a;        // 底层const:*p2 不可改,但 p2 可改
const int* const p3 = &a;  // 底层 + 顶层
const int& r = a;          // 引用的 const 本质是底层const:不能通过 r 改 a

顶层/底层 const 的赋值规则
顶层 const:拷贝时一般可忽略(因为拷贝的是值/指针本身)。
底层 const:是“被指向对象不可改”的约束,不能丢。
int x = 1;
const int cx = 2;
const int* pc = &cx; // 指向 const int
int* p = &x;
// p = pc;           // 错误:不能把“指向const”的指针赋给“指向非const”的指针(会丢底层const)
pc = p;              // 可以:给更严格的类型

const_cast:只能改变底层 const(语义上)
const_cast 是 C++ 四种强制类型转换符之一,专门用于移除或添加变量的 const(只读)或 volatile(易变)属性。
如果原对象本来就是 const(真正以 const 定义的对象),再通过 const_cast 修改它是未定义行为。
典型正确用途:调用历史接口(需要 T*)但你手上只有 const T*,且你明确知道该接口不会写数据。

const 作为函数参数:按值/按引用/按指针的意义差异

按值传参:const 通常没有语义收益
原因:按值会拷贝出临时副本,在函数内修改也影响不到实参。
因此按值参数加 const 更多是风格表达,工程中通常不强制。
void f(const int x); // 与 void f(int x) 基本等价(仅函数体内 x 不可改)

按引用/指针传参:const 是“真实约束”
void g(const int& x);  // 承诺不修改实参
void h(const int* p);  // 承诺不通过 p 修改 *p

重载与 const:顶层 const 不能区分重载
仅靠“是否顶层 const”的差异,无法形成有效重载(因为参数类型在重载匹配时等价)。
例如:
void f(int x);
void f(const int x); // 重定义:与上面等价
但以下可以形成重载(因为底层 const 不同):
void f(int& x);
void f(const int& x); // OK:可区分

const 与类:成员变量、成员函数、mutable

const 成员变量:必须在构造函数初始化列表中初始化
struct A {
    const int v;
    A(int x) : v(x) {}  // 必须在初始化列表
};

const 成员变量不能在构造函数体内赋值(那是“先默认构造再赋值”的语义)。

const 成员函数:隐式的 this 是 const
struct A {
    int x = 0;
    int get() const { return x; }  // this 的类型是 const A*
    void set(int v) { x = v; }
};

规则:
const 对象只能调用 const 成员函数(因为只有这些函数承诺不修改对象状态)。
非 const 对象既可调用 const 成员函数,也可调用非 const 成员函数。

mutable:允许在 const 成员函数里修改特定字段
struct Cache {
    mutable bool ready = false;
    mutable int value = 0;
    int get() const {
        if (!ready) { value = 42; ready = true; }
        return value;
    }
};

工程语义:
mutable 适合“逻辑常量”(对外可观测状态不变)但需要内部缓存/统计的场景。
需要注意:mutable 不等于线程安全;并发场景仍需同步原语或原子类型。

const 与返回值:何时需要返回 const

返回值类型通常不必加 const(返回的是副本,加 const 反而影响移动/赋值等使用体验)。
返回引用/指针时,用 const 表达只读接口很有价值:
const std::string& name() const; // 返回只读引用
对运算符重载(如 operator[])会经常提供 const/非const 两个版本,以适配对象的 const 性:
T& operator[](size_t);
const T& operator[](size_t) const;

const 与链接性:文件内可见

const修饰变量是也与static有一样的隐藏作用。只能在该文件中使用,其他文件不可以引用声明使用。 因此在头文件中声明const变量是没问题的,因为即使被多个文件包含,链接性都是内部的,不会出现符号冲突。
命名空间作用域的 const 变量在很多编译模型下具有内部链接(类似 static 的“每个翻译单元各自一份”效果),因此放在头文件里通常不会造成链接冲突。
但工程中更推荐显式写法以避免歧义:
C++17:用 inline constexpr 在头文件定义“单一定义”的常量
或者:头文件 extern 声明,源文件定义(传统方式)
推荐范式(头文件常量):
// header.h
inline constexpr int kMaxPlayers = 64;

constexpr / consteval / constinit

现代 C++ 里,“常量”不再只是一种语法糖,而是一套可控的编译期计算体系:
constexpr:允许在编译期求值(如果上下文需要);同时也可以在运行期求值
consteval(C++20):必须编译期求值,否则编译失败
constinit(C++20):用于静态存储期变量,要求其初始化发生在静态初始化阶段(避免晚初始化导致的顺序问题),但不意味着它是常量表达式
实践中我倾向于:
需要“强制编译期”的逻辑(例如生成查表结构、编译期哈希)使用 consteval
需要“静态对象尽早初始化”的场景使用 constinit
其余优先 constexpr,让编译器自行选择最优路径

静态初始化与动态初始化
静态初始化阶段 (Static Initialization)
这个阶段发生在程序加载时,甚至在 main 函数执行之前。它的特点是速度极快,因为不涉及复杂的函数调用。
它包含两种形式:
零初始化 (Zero-initialization):所有静态或全局变量首先被清零(填入 0 或 nullptr)。
常量初始化 (Constant-initialization):如果变量是用常量表达式(如 123 或 constexpr 函数)初始化的,编译器会在编译时计算好结果,直接硬编码在二进制文件的 .data 段中。
特点:安全、可靠、无顺序风险。
动态初始化阶段 (Dynamic Initialization)
如果一个全局变量需要通过运行期才能确定的逻辑(比如调用函数、读取文件、实例化复杂的类)来初始化,它就进入了动态初始化阶段。
int a = 10;                // 静态初始化(常量)
int b = get_cpu_count();   // 动态初始化(需要运行函数)
std::string s = "Hello";   // 动态初始化(需要调用构造函数)
致命弱点:顺序不确定
在一个编译单元(即一个 .cpp 文件)内,变量按定义顺序初始化。但是,不同编译单元(不同 .cpp 文件)之间的动态初始化顺序是未定义的。



评论

此博客中的热门博文

【Hybrid 引擎】从0开始的引擎开发——01 项目准备

【C++】左值和右值,左值引用和右值引用