C++ const以及相关关键字
const
是 C++ 中一个非常重要的关键字,它的核心思想是**“只读”(read-only)**。通过const
,我们可以告诉编译器某个数据不应该被修改。这不仅可以防止我们意外地修改数据,还能让编译器进行优化,并使代码的意图更加清晰。
1. const 变量:编译期常量与运行期常量
const
变量必须在声明时进行初始化,之后其值就不能再被改变。根据初始化的时机,我们可以将其分为两种。
a) 编译期常量 (Compile-time Constant)
编译期常量的值在编译阶段就已经确定了。编译器可以直接将其替换为具体的值,有点类似宏替换,但更安全(有类型检查)。
特点:
- 用一个常量值(字面量)或另一个编译期常量来初始化。
- 可以用于数组大小、模板参数、
case
标签等需要编译期确定值的场景。
示例:
const int MAX_USERS = 100; // 100是字面量,编译期确定
char user_names[MAX_USERS]; // OK, 数组大小必须是编译期常量const int MAX_LEVEL = MAX_USERS / 10; // MAX_USERS是编译期常量,所以MAX_LEVEL也是
在现代 C++ (C++11及以后) 中,更推荐使用 constexpr
来明确表示一个值是编译期常量。
b) 运行期常量 (Run-time Constant)
运行期常量的值在程序运行时才被确定,但一旦初始化后,其值就不能再改变。
特点:
- 用一个变量、函数返回值或其他在运行时才能确定的值来初始化。
- 其“常量”属性体现在初始化之后不可修改。
示例:
#include <iostream>int getUserInput() {int val;std::cout << "请输入一个数字: ";std::cin >> val;return val;
}int main() {const int USER_ID = getUserInput(); // USER_ID的值在运行时由用户输入确定std::cout << "你的ID是: " << USER_ID << std::endl;// USER_ID = 100; // 错误!编译不通过,因为USER_ID是const,初始化后不能再赋值return 0;
}
在这个例子中,USER_ID
的值直到程序运行到getUserInput()
时才确定,但一旦被赋值,它就成为了一个只读的常量。
2. const 指针
这是const
用法中最容易混淆的部分。判断的关键是const
修饰的是谁。一个简单的记忆法则是**“从右向左读”**,*
读作 pointer to
(指向…的指针)。
a) const int *p
(指向常量的指针)
- 读法:
p
is a pointer to aconst int
(p
是一个指向常量int
的指针)。 - 也写作:
int const *p
(效果完全相同)。 - 含义: 指针
p
所指向的值不能通过p
来修改,但指针p
本身可以被修改,即可以指向其他地址。 - 记忆: “锁值,不锁向” (The value is locked, the direction is not)。
示例:
int a = 10, b = 20;
const int *p = &a;// *p = 15; // 错误!不能通过p修改它所指向的值
p = &b; // 正确!p可以指向别处
常见用途: 作为函数参数,防止函数内部修改指针所指向的数据。
void printArray(const int* arr, int size);
b) int * const p
(常量指针)
- 读法:
p
is aconst
pointer to anint
(p
是一个指向int
的常量指针)。 - 含义: 指针
p
本身的值(即它存储的地址)是常量,不能被修改。但它所指向的值可以通过p
来修改。 - 关键点: 因为指针本身是常量,所以必须在声明时初始化。
- 记忆: “锁向,不锁值” (The direction is locked, the value is not)。
示例:
int a = 10, b = 20;
int * const p = &a; // 必须在声明时初始化*p = 15; // 正确!可以修改p所指向的值
// p = &b; // 错误!p本身是常量,不能再指向别处
c) const int * const p
(指向常量的常量指针)
- 读法:
p
is aconst
pointer to aconst int
(p
是一个指向常量int
的常量指针)。 - 含义: 指针
p
本身和它所指向的值都不能被修改。 - 记忆: “值和向都锁” (Both value and direction are locked)。
示例:
int a = 10, b = 20;
const int * const p = &a; // 必须在声明时初始化// *p = 15; // 错误!不能修改值
// p = &b; // 错误!不能修改指向
3. const 成员函数
const
成员函数是类设计中的一个重要概念,它承诺该函数不会修改对象的数据成员。
-
语法: 在函数声明和定义的参数列表后面加上
const
关键字。
ReturnType functionName(params) const;
-
内部机制: 在
const
成员函数内部,this
指针的类型是const ClassName*
。而非const
成员函数的this
指针类型是ClassName*
。由于this
指向一个常量对象,所以你不能通过它来修改任何成员变量。
示例:
class Rectangle {
private:int width, height;
public:Rectangle(int w, int h) : width(w), height(h) {}// const 成员函数,它承诺不修改 width 或 heightint getArea() const {// width = 10; // 错误!不能在const成员函数中修改成员变量return width * height;}// 非 const 成员函数,它可以修改成员变量void setWidth(int w) {width = w;}
};
4. const 对象
const
对象是指在声明时使用 const
关键字修饰的对象。一旦一个对象被声明为const
,它的状态在构造完成后就不能再被修改。
- 规则:
const
对象只能调用const
成员函数。 - 原因: 调用非
const
成员函数意味着有可能会修改对象的状态,这违背了对象本身的const
属性。编译器会阻止这种行为。
示例:
const Rectangle r(10, 5); // r是一个const对象int area = r.getArea(); // 正确!getArea() 是一个 const 成员函数
// r.setWidth(20); // 错误!setWidth() 不是 const 成员函数,不能被const对象调用
5. mutable 关键字
mutable
是 const
的一个“例外”。它允许在 const
成员函数中修改被 mutable
修饰的成员变量。
- 用途: 当一个成员变量不属于对象的“逻辑状态”,而是一些内部状态(如缓存、互斥锁、调试计数器等)时,可以使用
mutable
。即使在逻辑上是“只读”的操作(如get
方法),也可能需要更新这些内部状态。
示例:
假设我们想为一个计算密集型的 getLength()
方法添加缓存。
#include <cmath>class Line {
private:double x1, y1, x2, y2;mutable double cachedLength; // 缓存的长度mutable bool isCacheValid; // 缓存是否有效public:Line(double x1, double y1, double x2, double y2): x1(x1), y1(y1), x2(x2), y2(y2), isCacheValid(false) {}double getLength() const {if (!isCacheValid) {// 这段代码在const成员函数中,但可以修改mutable成员cachedLength = sqrt(pow(x2 - x1, 2) + pow(y2 - y1, 2));isCacheValid = true;std::cout << " (计算并缓存) ";}return cachedLength;}
};int main() {const Line line(0, 0, 3, 4); // 一个const对象std::cout << line.getLength() << std::endl; // 输出: (计算并缓存) 5std::cout << line.getLength() << std::endl; // 输出: 5 (直接从缓存读取)
}
getLength()
从外部看是一个只读操作,所以声明为 const
是合理的。但内部为了效率需要修改缓存,mutable
完美地解决了这个矛盾。
6. const_cast:去除 const 属性
const_cast
是 C++ 四种类型转换操作符之一,它的唯一作用就是添加或移除变量的 const
或 volatile
属性。这通常被认为是一个危险的操作。
-
语法:
const_cast<new_type>(expression)
-
核心警告: 如果你对一个最初就被声明为
const
的变量使用const_cast
去掉const
属性,并尝试修改它,其结果是未定义行为 (Undefined Behavior)。这意味着程序可能会崩溃,也可能看起来正常工作,或者产生无法预料的结果。为什么是未定义行为? 因为编译器可能会对原始
const
变量进行优化,比如把它放在只读内存段(如.rodata
),或者在编译时就用其值替换所有引用。强行写入只读内存会导致段错误(segmentation fault)。
合规但需谨慎的用例:
最常见的(也是少数合理的)用例是与一些旧的、设计不佳的C风格API交互。这些API可能接受一个 char*
参数,但函数内部并不会修改它。
// 假设这是一个我们无法修改的旧API
// 它承诺不会修改str,但参数类型不是 const char*
void legacy_c_function(char* str) {printf("String: %s\n", str);
}int main() {const char* my_message = "Hello, world!";// 直接调用会报错:cannot convert 'const char*' to 'char*'// legacy_c_function(my_message); // 我们确信函数是安全的,所以使用const_castlegacy_c_function(const_cast<char*>(my_message)); // OK
}
危险的错误用例(导致未定义行为):
int main() {const int magic_number = 42;// p 指向一个const intconst int* p = &magic_number;// 使用 const_cast 去掉 const 属性int* p_non_const = const_cast<int*>(p);// 尝试修改一个最初就是 const 的变量*p_non_const = 99; // !!! 未定义行为 !!!// 程序可能在这里崩溃,也可能不会std::cout << magic_number << std::endl; // 输出可能是42,也可能是99,或者其他
}
总结: 除非你百分之百确定你在做什么(比如调用一个const
不正确的旧API),否则应避免使用 const_cast
。滥用它会破坏const
提供的类型安全保证。