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" 这两个字符串常量,都存放在常量区。指针 strstr2 本身在栈上(如果是局部变量),但它们指向的是常量区的地址。

这里有个坑,我必须要提醒你:

我曾经在项目里看到过这样的代码:

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 常量区的内存布局

咱们来看看常量区在整个进程内存空间中的位置。我画了一张图,帮你直观理解:

进程内存空间布局 栈区(Stack) 局部变量、函数参数、返回地址 堆区(Heap) 动态分配的内存(malloc/new) 数据段(Data Segment) 全局变量、静态变量(已初始化) 常量区(.rodata) 字符串常量、全局const变量、静态const变量 高地址 低地址

从这张图可以看出,常量区在数据段的下方(更低地址)。它和代码段(.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::stringstd::string_view 来处理字符串。它们帮你管理内存,省心很多。只有在性能敏感或者底层操作时,才直接操作 const char*

5.7 常量区的大小限制

常量区的大小是有限制的,虽然通常比栈大得多。我记得有一次在嵌入式项目里,一个同事在代码里写了一个巨大的常量数组:

const int big_table[] = {
    // 几万个元素...
};

结果链接的时候报错了,说 .rodata 段溢出。嗯,常量区虽然大,但不是无限的。在嵌入式系统或者有内存限制的环境下,要注意常量区的大小。

一般来说,桌面系统和服务器上,常量区的大小限制在几百 MB 到几 GB 之间,具体取决于操作系统和链接器配置。但在嵌入式系统上,可能只有几十 KB。

好了,常量区这块就聊到这儿。记住一句话:常量区的东西,只读不写。写就崩。

专注资料整理