5. 常量区:字符串常量、const变量存储位置
常量区,这个名字听起来就挺直白的——存常量的地方。但说实话,我刚入行那会儿,对这块的理解也踩过坑。我记得有一次排查一个诡异的崩溃问题,折腾了两天才发现,问题就出在常量区的内存分配上。嗯,今天咱们就把这块彻底聊透。
5.1 常量区到底存了啥?
常量区,也叫只读数据段(.rodata)。它存放的是那些在程序运行期间不会改变的数据。说白了,就是编译器帮你保证“这东西你别想改”。
具体来说,常量区主要存放这几类东西:
- 字符串常量:比如
"hello world"这种直接写在代码里的字符串 - 全局 const 变量:用
const修饰的全局变量 - 静态 const 变量:用
static const修饰的变量 - 某些编译期确定的常量表达式
核心要点:常量区的数据是只读的。任何试图修改它的行为,都会导致程序崩溃——段错误(Segmentation Fault)。
5.2 字符串常量的存储位置
先看一个最常见的例子:
const char* str = "hello";
char* str2 = "world";
这两行代码,"hello" 和 "world" 这两个字符串常量,都存放在常量区。指针 str 和 str2 本身在栈上(如果是局部变量),但它们指向的是常量区的地址。
这里有个坑,我必须要提醒你:
我曾经在项目里看到过这样的代码:
char* p = "hello";
p[0] = 'H'; // 试图修改字符串常量
这段代码在有些编译器上能“侥幸”通过,但在现代编译器和操作系统上,几乎必崩。因为 "hello" 在常量区,是只读的。正确的做法是用数组:
char p[] = "hello";
p[0] = 'H'; // 没问题,这是在栈上拷贝了一份
为什么会这样?因为 char p[] = "hello" 会在栈上分配一个数组,然后把常量区的字符串拷贝过来。而 char* p = "hello" 只是让指针指向常量区,并没有拷贝。
5.3 const 变量的存储位置
const 变量的存储位置,其实比你想的要复杂一些。它取决于 const 变量是局部的还是全局的,以及编译器怎么优化。
| 变量类型 | 存储位置 | 说明 |
|---|---|---|
| 全局 const 变量 | 常量区(.rodata) | 编译期确定,只读 |
| 局部 const 变量 | 栈上 | 运行时确定,但编译器会检查不允许修改 |
| 静态 const 变量 | 常量区(.rodata) | 和全局 const 类似 |
| const 成员变量 | 取决于对象存储位置 | 对象在哪,它就在哪 |
我个人习惯把 const 变量分为两类:
- 编译期常量:值在编译时就确定了,比如
const int MAX = 100;(全局)。这种通常放在常量区。 - 运行期常量:值在运行时才确定,比如函数参数
void foo(const int n)。这种在栈上,只是编译器禁止你修改它。
一个小技巧:如果你想让一个 const 变量一定放在常量区,可以用 constexpr 关键字。C++11 引入的 constexpr 能保证在编译期求值,并且通常会被放到常量区。
5.4 常量区的内存布局
咱们来看看常量区在整个进程内存空间中的位置。我画了一张图,帮你直观理解:
从这张图可以看出,常量区在数据段的下方(更低地址)。它和代码段(.text)一样,都是只读的。操作系统会把它们映射到物理内存的只读页上。
5.5 一个容易混淆的问题
你可能会问:const int a = 10; 如果定义在函数内部,它到底在哪?
嗯,这个问题我当年也纠结过。答案是:在栈上。因为局部 const 变量的值可能是在运行时才确定的,比如:
void foo(int x) {
const int n = x + 1; // 运行时确定
// n 在栈上
}
但如果是:
void bar() {
const int n = 10; // 编译期确定
// 现代编译器可能会优化,直接替换为常量,不分配存储空间
}
对于编译期就能确定的局部 const 变量,编译器通常不会给它分配内存,而是直接在使用的地方替换成常量值。这就是所谓的“常量折叠”优化。
总结一下:
- 字符串常量 → 常量区
- 全局 const 变量 → 常量区
- 静态 const 变量 → 常量区
- 局部 const 变量 → 栈上(或直接被优化掉)
5.6 避坑指南
我在项目中遇到过几次跟常量区相关的 bug,这里分享两个最常见的:
坑一:字符串常量比较
const char* s1 = "hello";
const char* s2 = "hello";
if (s1 == s2) {
// 这可能是 true,也可能是 false!
// 取决于编译器是否做了字符串池化
}
有些编译器会把相同的字符串常量合并到同一个地址(字符串池化),所以 s1 == s2 可能为 true。但这不是 C++ 标准保证的。正确的做法是用 strcmp 或者 std::string。
坑二:返回局部字符串常量的指针
const char* getStr() {
return "hello"; // 没问题,返回的是常量区地址
}
const char* getStr2() {
char buf[] = "hello";
return buf; // 大问题!返回的是栈上地址,函数返回后失效
}
第一个函数没问题,因为字符串常量在常量区,程序整个生命周期都存在。第二个函数就危险了,返回的是栈上数组的地址,函数返回后这块内存就被回收了。
我的建议:在 C++ 中,尽量用 std::string 和 std::string_view 来处理字符串。它们帮你管理内存,省心很多。只有在性能敏感或者底层操作时,才直接操作 const char*。
5.7 常量区的大小限制
常量区的大小是有限制的,虽然通常比栈大得多。我记得有一次在嵌入式项目里,一个同事在代码里写了一个巨大的常量数组:
const int big_table[] = {
// 几万个元素...
};
结果链接的时候报错了,说 .rodata 段溢出。嗯,常量区虽然大,但不是无限的。在嵌入式系统或者有内存限制的环境下,要注意常量区的大小。
一般来说,桌面系统和服务器上,常量区的大小限制在几百 MB 到几 GB 之间,具体取决于操作系统和链接器配置。但在嵌入式系统上,可能只有几十 KB。
好了,常量区这块就聊到这儿。记住一句话:常量区的东西,只读不写。写就崩。