C/C++传统程序内存分布(代码实测)

C/C++程序传统内存分布(代码实测)

引言

C/C++ 程序的内存分布是后端、嵌入式、C++ 面试的核心高频考点 。很多人只会背概念,但分不清 databssrodata、栈、堆的区别,也搞不懂 staticconst 对内存布局的影响

本文结合可运行实测代码 + 完整理论总结,带你彻底吃透 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*)&param << "\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

六、实验结果分析

运行后可清晰观察到地址高低顺序,同时精准验证虚表存储位置:

  1. .text 地址最低(函数代码、指令逻辑)

  2. .rodata 段存放全局只读虚表 :虚表测试函数分别测试栈、堆对象的虚函数表、虚基表地址,可发现所有对象的虚表地址完全一致,和 rodata 常量地址区间重合,彻底证实虚表属于只读全局数据

  3. 其次是 data、bss 静态数据区,地址略高于 rodata 段

  4. 堆区地址远高于静态区,向高地址延伸,动态分配空间

  5. 栈区地址最高,向低地址生长;对象的虚指针(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++ 程序传统内存分布的常见核心知识点 + 实测代码 + 面试考点,希望能帮助大家彻底搞懂各内存分区的存储规则、关键字特性及地址增长逻辑

相关推荐
掉鱼的猫1 小时前
不只是 Token 流:Solon AI 4.1 的语义化聊天事件
java·openai
牛油果子哥q1 小时前
C++对接LLM完整工程:异步HTTP请求、JSON解析、超时容错、重连兜底
c++·http·json
她的男孩1 小时前
同一个接口,为什么销售只看到自己的订单?我拆了 681 行数据权限拦截器的源码
java·后端·架构
卓怡学长1 小时前
w157基于springboot北部湾地区助农平台
java·spring boot·spring·maven·intellij-idea
小白羊丨1 小时前
问数 Agent 整体架构、意图识别、历史上下文与 NL2SQL
java·面试·架构
Linux-lucky1 小时前
33-Linux学习之旅之MySQL用户管理
java·linux·运维·学习·mysql·ubuntu
SimonKing1 小时前
将cURL 命令直接变成 Java 代码?用JQuick-Curl搞起来
java·后端·程序员
会周易的程序员1 小时前
aiDgeController软PLC控制通讯协议文档
c++·物联网·网关·iot·ipc·进程间通讯
凤山老林2 小时前
企业级文件服务中台:Spring Boot 集成 MinIO 实现分片存储、生命周期管理与多租户隔离
java·spring boot·后端·minio·文件服务