const关键字的核心概念、典型场景和易错陷阱
一、const本质:类型系统的守护者
1. 与#define的本质差异
维度 | #define | const |
---|---|---|
编译阶段 | 预处理替换 | 编译器类型检查 |
作用域 | 无作用域(全局污染) | 遵循块作用域 |
调试可见性 | 符号消失 | 保留符号信息 |
类型安全 | 无类型 | 强类型约束 |
内存分配 | 不占内存(文本替换) | 占用内存(可取地址) |
致命陷阱示例:
cpp
#define MAX_SIZE 1024
const int max_size = 1024;
char buffer1[MAX_SIZE*2]; // 正确,宏在编译前展开
char buffer2[max_size*2]; // C++11前错误,const变量不是编译期常量
2. constexpr的救赎(C++11)
cpp
constexpr int max_size = 1024; // 真正的编译期常量
char buffer[max_size*2]; // 合法
二、类中的const攻防战
1. 成员变量:初始化列表的独裁
cpp
class Matrix {
public:
Matrix(int w) : width(w) {} // 必须通过初始化列表
private:
const int width; // const成员变量
mutable int cache; // 可变成员(即使const对象也可修改)
};
极端案例:
cpp
class Immortal {
public:
Immortal() {} // 错误!未初始化const成员year
private:
const int year = 2023; // C++11允许类内初始化
};
2. 成员函数:const的双重含义
cpp
class DataPool {
public:
void modify() const {
// 错误!不能修改非mutable成员
// count++;
}
void nonConstFunc() {
// 非const函数可修改成员
}
};
重载的黑暗法则:
cpp
class Logger {
public:
void log() const { /* 读操作 */ }
void log() { /* 写操作 */ }
};
const Logger cl;
cl.log(); // 调用const版本
Logger l;
l.log(); // 调用非const版本
三、指针与const的纠缠
1. 声明顺序的死亡游戏
cpp
int a = 10;
const int* p1 = &a; // 指向常量的指针(底层const)
int const* p2 = &a; // 等同p1
int* const p3 = &a; // 常量指针(顶层const)
const int* const p4 = &a; // 双const
指针常量 vs 常量指针:
- 左定值(const在*左边):指向的值不可变
- 右定向(const在*右边):指针本身不可变
2. 类型转换的修罗场
cpp
const int* pci = &a;
int* pi = const_cast<int*>(pci); // 去const化(危险!)
*pci = 20; // 未定义行为(原始对象非常量时可能成功)
安全转换法则:
- 只有原始对象本身是非const的,才能用const_cast去掉const属性
四、函数签名中的const暗战
1. 参数传递:效率与安全的博弈
cpp
void process(const BigObject& obj); // 避免拷贝+防止修改
void dangerous(const int* ptr); // 可能被const_cast突破防御
2. 返回值修饰:所有权的宣誓
cpp
const std::string& getConfig(); // 返回只读引用
const int* getRawData() const; // 承诺不修改数据
死亡陷阱:
cpp
const int& func() {
int local = 42;
return local; // 返回局部变量的引用!
}
五、高级战场:模板与const
1. 类型推导的混沌法则
cpp
template<typename T>
void deduce(T param) {}
const int ci = 10;
deduce(ci); // T推导为int(const被剥离)
deduce(&ci); // T推导为const int*
2. const与完美转发
cpp
template<typename T>
void relay(T&& arg) {
process(std::forward<T>(arg));
}
relay(ci); // 转发后保持const属性
六、面试核弹级问题
-
如何让const成员函数修改成员变量?
- 使用mutable修饰成员变量
- const_cast去除this指针的const属性(危险操作)
-
const成员函数调用非const函数是否合法?
cppclass Test { public: void foo() { } void bar() const { foo(); // 错误!const函数不能调用非const成员函数 } };
-
为什么函数重载时const可以作为区分?
- 编译器将const成员函数视为
void func(const T* this)
- 非const版本为
void func(T* this)
- 编译器将const成员函数视为
总结:const的哲学
- 契约精神:对编译器承诺数据不可变
- 防御性编程:限制意外修改,提升代码健壮性
- 类型系统武器:与引用、模板等特性配合构建安全屏障
掌握const的每个细节,相当于拿到了C++类型系统的核按钮------既能保证代码安全,又能精准控制程序的每一块内存。