【C++】类型限定符const
Const的用法
const 的本质:类型系统的一部分
const 不是“运行时保护”,而是编译期的类型约束:约束“通过该名字/表达式能否修改某个对象”。
const 可以参与重载(尤其在成员函数与引用/指针参数中)
const 不提供并发同步语义(不等价于线程安全),仅代表“逻辑上的只读”,而不代表“物理上的线程安全”。
const 修饰对象:必须初始化、不可再赋值
要点:
定义时必须初始化(除非是某些特殊存储期/延迟初始化场景,但常规写法即“必须初始化”)。
这是“对象不可赋值”的约束:并不意味着对象所在内存不可写(那是更底层的系统层语义)。
const 修饰指针/引用:顶层 const 与底层 const
看 * 的左右:
* 左边:约束“指向的对象”(底层 const)
* 右边:约束“指针本身”(顶层 const
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 是 C++ 四种强制类型转换符之一,专门用于移除或添加变量的 const(只读)或 volatile(易变)属性。
如果原对象本来就是 const(真正以 const 定义的对象),再通过 const_cast 修改它是未定义行为。
典型正确用途:调用历史接口(需要 T*)但你手上只有 const T*,且你明确知道该接口不会写数据。
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”的差异,无法形成有效重载(因为参数类型在重载匹配时等价)。
例如:
void f(int x);
void f(const int x); // 重定义:与上面等价
但以下可以形成重载(因为底层 const 不同):
void f(int& x);
void f(const int& x); // OK:可区分
const 与类:成员变量、成员函数、mutable
struct A {
const int v;
A(int x) : v(x) {} // 必须在初始化列表
};
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 成员函数。
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 std::string& name() const; // 返回只读引用
对运算符重载(如 operator[])会经常提供 const/非const 两个版本,以适配对象的 const 性:
T& operator[](size_t);
const T& operator[](size_t) const;
const 与链接性:文件内可见
但工程中更推荐显式写法以避免歧义:
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 文件)之间的动态初始化顺序是未定义的。
评论
发表评论