C/C++程序传统内存分布(代码实测)
引言
C/C++ 程序的内存分布是后端、嵌入式、C++ 面试的核心高频考点 。很多人只会背概念,但分不清 data、bss、rodata、栈、堆的区别,也搞不懂 static、const 对内存布局的影响
本文结合可运行实测代码 + 完整理论总结,带你彻底吃透 Linux(x86/x64) 环境下 C/C++程序完整内存布局,看完即可搞定所有内存相关面试题
运行环境 :Linux x86_64 / GCC / G++
编译要求 :使用
-O0关闭编译器优化,否则变量可能会被优化,无法正确观测内存地址
一、整体内存架构总览
Linux 环境下,C/C++ 程序运行时内存整体分为两大模块:静态存储区 + 动态运行区,地址由低到高排布,整体顺序如下:
.text 代码段 -> .rodata 只读数据段 -> .data 初始化数据段 -> .bss 未初始化数据段 -> 堆区 -> 共享区(mmap) -> 栈区
1.1 分区核心特性
-
静态存储区(编译确定大小):程序编译链接后地址固定,运行时不扩容、不移动
-
动态存储区(运行动态变化):边界运行时可变,堆向高地址增长,栈向低地址增长
二、八大内存分区详细解析
2.1 .text 代码段(只读)
存储程序所有二进制汇编指令,属于只读内存,防止指令被篡改。
存储内容 :所有普通函数、成员函数的机器指令、程序执行代码(函数地址也存放在代码段)
核心特点:只读、地址最低、运行全程固定
2.2 .rodata 只读数据段(只读)
专门存放程序中所有只读数据,保护常量不被修改。
存储内容:
-
字符串字面量(如
"hello") -
全局
const变量 -
静态常量
static const(全局/局部) -
虚函数表、虚基表等只读结构
核心特点:只读,修改会触发段错误
2.3 .data 已初始化数据段(可读写)
存放已初始化非零的全局变量、静态变量
存储内容 :全局非零初始化变量、static 非零初始化变量
核心特点:可读写、占用磁盘空间、编译阶段确定地址
2.4 .bss 零初始化段(可读写)
存放未初始化 或 初始值为0的全局、静态变量
核心特性(面试重点):
-
不占用可执行文件磁盘空间,仅在程序加载时由内核统一清零
(就是说在可执行程序中仅记录了一个变量应该占用多少字节,记录下来这个数字,但没有实际分配空间),直至被加载到内存后,才会真正为这些变量分配空间(更准确来说,加载时分配虚拟地址空间,第一次访问时才分配物理内存页) -
变量后续赋值修改后,仍然留在 bss 段,不会迁移到 data 段
2.5 堆区(Heap)
由程序员手动申请、手动释放的动态内存区域。
核心特性:
-
地址向高地址增长
-
通过
new / malloc分配,delete / free释放 -
空间大、灵活度高,容易产生内存碎片、内存泄漏
2.6 共享区(mmap 映射区)
位于堆和栈之间的弹性内存区域,由系统 mmap 机制管理。
主要作用:加载动态库、文件内存映射、进程共享内存,运行时动态扩容
2.7 栈区(Stack)
由编译器自动分配、自动回收,无需手动管理
存储内容:函数参数、局部变量、局部普通 const 变量、函数返回地址、栈帧信息
核心特性:
-
地址向低地址增长
-
Linux 默认栈大小固定为 8MB
-
递归过深、超大局部数组会导致栈溢出
-
函数执行结束,栈空间立即自动回收
三、关键字内存规则(面试必背)
3.1 static 关键字规则
-
所有
static变量(全局/局部)全部位于静态存储区,不占栈空间 -
static 普通变量:非零初始化 -> .data ;未初始化/零初始化 -> .bss -
static const 常量:固定存入 .rodata 只读段
3.2 const 关键字规则
-
全局 const、static const、constexpr -> .rodata 只读段
-
局部普通 const(无 static)-> 默认在栈区(部分编译器优化会放入 rodata)
-
核心区别 :
const控制「只读属性」,static控制「存储周期 & 内存区域」,二者互不干扰
3.3 函数与指针内存规则
-
所有函数代码本体,固定在 .text 代码段
-
函数指针变量本身:全局存 data/bss,局部存栈区
-
虚函数表 vtable、虚基表 vbtable -> .rodata
-
虚指针 vptr、vbptr:跟随对象(对象在栈则指针在栈,对象在堆则指针在堆)
四、完整可运行实测代码
通过打印各变量内存地址,直观验证上述所有内存分布规则:
cpp
#include <iostream>
#include <cstdlib>
// ========== 全局变量(静态存储区) ==========
int g_init_var = 100; // .data(已初始化全局变量)
int g_uninit_var; // .bss(未初始化全局变量)
const int g_const_var = 200; // .rodata(全局只读常量)
static int sg_init = 300; // .data(静态全局已初始化)
static int sg_uninit; // .bss(静态全局未初始化)
static const int sg_const = 400; // .rodata(静态全局只读)
// ========== 虚函数、虚基类测试类(验证vtable/vbtable) ==========
// 普通虚函数类(验证虚函数表 vtable)
class BaseVtable {
public:
virtual void func1() {}
virtual void func2() {}
};
// 多重继承虚基类(验证虚基表 vbtable)
class GrandFather {};
class Father : virtual public GrandFather {};
class Son : virtual public Father {};
// ========== 通用内存布局测试函数 ==========
void test_memory_layout(int param)
{
// 栈区变量
int stack_var = 10; // 普通局部变量 - 栈
const int stack_const = 20; // 局部普通const - 栈
// 静态局部变量(静态存储区)
static int s_local_init = 500; // .data(静态局部已初始化)
static int s_local_uninit; // .bss(静态局部未初始化)
static const int s_local_const = 600; // .rodata(静态局部只读)
// 堆区变量
int* heap_var = new int(30);
// 字符串字面量 - rodata
const char* str = "Hello, Memory!";
// 打印各区域内存地址
std::cout << "====== C程序常规内存地址实测 ======\n\n";
std::cout << "[.text 代码段]\n";
std::cout << " 函数地址 : " << (void*)test_memory_layout << "\n\n";
std::cout << "[.rodata 只读数据段]\n";
std::cout << " 字符串常量 : " << (void*)str << "\n";
std::cout << " 全局const变量 : " << (void*)&g_const_var << "\n";
std::cout << " 静态全局const : " << (void*)&sg_const << "\n";
std::cout << " 静态局部const : " << (void*)&s_local_const << "\n\n";
std::cout << "[.data 初始化数据段]\n";
std::cout << " 全局已初始化 : " << (void*)&g_init_var << "\n";
std::cout << " 静态全局初始化 : " << (void*)&sg_init << "\n";
std::cout << " 静态局部初始化 : " << (void*)&s_local_init << "\n\n";
std::cout << "[.bss 未初始化数据段]\n";
std::cout << " 全局未初始化 : " << (void*)&g_uninit_var << "\n";
std::cout << " 静态全局未初始化: " << (void*)&sg_uninit << "\n";
std::cout << " 静态局部未初始化: " << (void*)&s_local_uninit << "\n\n";
std::cout << "[stack 栈区]\n";
std::cout << " 函数参数 : " << (void*)¶m << "\n";
std::cout << " 局部栈变量 : " << (void*)&stack_var << "\n";
std::cout << " 栈上const变量 : " << (void*)&stack_const << "\n\n";
std::cout << "[heap 堆区]\n";
std::cout << " 堆内存地址 : " << (void*)heap_var << "\n";
// 释放堆内存
delete heap_var;
}
// ========== 独立虚表专项测试函数(Vtable / Vbtable 专属验证)注意:G++实现虚菱形继承不依赖虚基表,且虚基表指针以及虚函数表指针的内存位置没有C++标准支持,所以下面实验稍不严谨 ==========
void test_virtual_table_layout()
{
std::cout << "====== 虚函数表 & 虚基表 专项内存实测 ======\n\n";
// 1. 普通虚函数对象(栈上)
BaseVtable stack_base_obj;
// 2. 虚继承对象(栈上)
Son stack_son_obj;
// 3. 普通虚函数对象(堆上)
BaseVtable* heap_base_obj = new BaseVtable();
// 4. 虚继承对象(堆上)
Son* heap_son_obj = new Son();
// 取出虚表、虚基表地址
void* stack_vtable = *(void**)&stack_base_obj;
void* stack_vbtable = *(void**)&stack_son_obj;
void* heap_vtable = *(void**)heap_base_obj;
void* heap_vbtable = *(void**)heap_son_obj;
// 对象自身地址(vptr 所在位置)
std::cout << "[栈上对象 - 虚指针与虚表地址对比]\n";
std::cout << " 栈上对象本身地址(vptr存储位置) : " << (void*)&stack_base_obj << "\n";
std::cout << " 对应虚函数表VTable真实地址 : " << stack_vtable << "\n";
std::cout << " 虚继承对象vptr存储位置 : " << (void*)&stack_son_obj << "\n";
std::cout << " 对应虚基表VBTABLE真实地址 : " << stack_vbtable << "\n\n";
std::cout << "[堆上对象 - 虚指针与虚表地址对比]\n";
std::cout << " 堆上对象本身地址(vptr存储位置) : " << (void*)heap_base_obj << "\n";
std::cout << " 对应虚函数表VTable真实地址 : " << heap_vtable << "\n";
std::cout << " 虚继承对象vptr存储位置 : " << (void*)heap_son_obj << "\n";
std::cout << " 对应虚基表VBTABLE真实地址 : " << heap_vbtable << "\n\n";
// 关键结论打印
std::cout << "[实验结论]\n";
std::cout << " 1. 虚表/虚基表地址固定,属于 .rodata 全局只读段\n";
std::cout << " 2. 无论对象在栈/堆,vtable/vbtable 地址完全一致\n";
std::cout << " 3. vptr 存于对象自身内存(栈/堆),仅指向 rodata 虚表\n\n";
delete heap_base_obj;
delete heap_son_obj;
}
int main()
{
// 分开测试,逻辑解耦
test_memory_layout(1);
std::cout << "----------------------------------------\n";
test_virtual_table_layout();
std::cout << "按回车键结束程序...";
std::cin.get();
return 0;
}
运行结果:

五、编译运行指令
必须关闭优化,否则部分变量会被编译器优化删除,无法观测:
bash
# 编译:关闭优化
g++ -O0 main.cpp -o mem_test
# 运行
./mem_test
六、实验结果分析
运行后可清晰观察到地址高低顺序,同时精准验证虚表存储位置:
-
.text 地址最低(函数代码、指令逻辑)
-
.rodata 段存放全局只读虚表 :虚表测试函数分别测试栈、堆对象的虚函数表、虚基表地址,可发现所有对象的虚表地址完全一致,和 rodata 常量地址区间重合,彻底证实虚表属于只读全局数据
-
其次是 data、bss 静态数据区,地址略高于 rodata 段
-
堆区地址远高于静态区,向高地址延伸,动态分配空间
-
栈区地址最高,向低地址生长;对象的虚指针(vptr)存储在栈/堆对象内存中,仅指向 rodata 段的虚表,不存储虚表本身
七、常见面试总结(高频)
1、bss 段和 data 段的区别?
data 存放非零初始化全局/静态变量,占磁盘空间;bss 存放未初始化/零初始化变量,不占磁盘空间,运行时内核清零。
2、局部 const 变量在哪里?
普通局部 const 在栈区;加 static 修饰后存入 rodata 只读段。
3、堆和栈的核心区别?
栈自动分配释放、空间小、向下增长;堆手动管理、空间大、向上增长,易泄漏碎片。
4、static 变量为什么生命周期全局?
static 变量存入静态存储区,程序启动分配、程序结束才释放,脱离函数栈帧生命周期。
5、虚表、虚指针内存归属高频题?
虚函数表(vtable)、虚基表(vbtable) :全局只读元数据,编译生成,存储在 .rodata 只读段 ;虚指针(vptr/vbptr):属于对象成员变量,跟随对象存储,对象在栈则vptr在栈、对象在堆则vptr在堆,运行时动态指向 rodata 中的虚表。
结尾
3、堆和栈的核心区别?
栈自动分配释放、空间小、向下增长;堆手动管理、空间大、向上增长,易泄漏碎片。
4、static 变量为什么生命周期全局?
static 变量存入静态存储区,程序启动分配、程序结束才释放,脱离函数栈帧生命周期。
5、虚表、虚指针内存归属高频题?
虚函数表(vtable)、虚基表(vbtable) :全局只读元数据,编译生成,存储在 .rodata 只读段 ;虚指针(vptr/vbptr):属于对象成员变量,跟随对象存储,对象在栈则vptr在栈、对象在堆则vptr在堆,运行时动态指向 rodata 中的虚表。
结尾
本文覆盖了 C/C++ 程序传统内存分布的常见核心知识点 + 实测代码 + 面试考点,希望能帮助大家彻底搞懂各内存分区的存储规则、关键字特性及地址增长逻辑