C++ 内存模型 & 底层原理

一、程序内存五大分区

C++ 程序运行时,操作系统会为其划分独立的虚拟地址空间,主要分为栈区、堆区、全局/静态区、常量区、代码区,各区域的分配、生命周期、读写权限完全不同。

1. 栈区(Stack)

  • 存储内容:函数局部变量、函数形参、函数返回地址、临时变量。
  • 分配释放:由编译器自动管理,函数调用时自动分配内存,函数执行结束后立即自动回收。
  • 特性:空间连续、读写速度极快;栈空间容量固定且较小,默认通常几 MB。
  • 风险:局部数组/对象过大,会触发栈溢出(Stack Overflow)

2. 堆区(Heap)

  • 存储内容:使用 new / malloc 动态开辟的内存、动态创建的对象。
  • 分配释放:手动管理 ,程序员主动申请,必须主动调用 delete / free 释放。
  • 特性:空间不连续、容量大(可占用GB级内存)、分配速度慢。
  • 风险:只申请不释放会造成内存泄漏;释放后继续使用会产生野指针。

3. 全局/静态区(数据段 Data / BSS段)

  • 存储内容:全局变量、static 修饰的静态局部变量、静态成员变量。
  • 细分:
    • BSS段:未初始化的全局/静态变量,程序加载时统一置零;
    • 数据段:已初始化的全局/静态变量。
  • 生命周期:程序整个运行期间常驻内存,程序退出后由系统回收。
  • 特性:作用域全局/类内,不会随函数结束而销毁。

4. 常量区(只读数据段 .rodata)

  • 存储内容:字符串常量、全局 const 常量。
  • 权限:只读区域,运行阶段禁止修改,强行修改会触发程序崩溃。
  • 生命周期:和全局区一致,伴随程序全程运行。

5. 代码区(文本段 .text)

  • 存储内容:编译后生成的二进制机器指令、函数体代码。
  • 权限:只读、可共享(多个进程可共用同一份代码)。
  • 特性:程序启动时加载,运行期间不允许改写。

二、new / delete 底层实现原理

new/delete 是 C++ 动态内存管理核心,底层封装了 C 语言的 malloc/free,并额外增加了面向对象的构造、析构逻辑。

1. new 执行流程

  1. 底层调用 malloc堆区申请一块连续内存;
  2. 调用类的构造函数,完成对象初始化;
  3. 返回该内存的指针。

2. delete 执行流程

  1. 调用类的析构函数,释放对象内部资源;
  2. 底层调用 free,释放堆上的内存空间。

3. 数组形式 new\[\] / delete\[\]

  • new[]:申请连续数组内存,额外记录数组元素个数,依次调用每个元素的构造函数;
  • delete[]:根据记录的元素个数,逐个调用析构函数,再统一释放内存。
  • 注意:new\[\] 必须匹配 delete\[\],混用会导致析构函数调用不全、内存泄漏。

4. 与 malloc / free 核心区别

  1. malloc/free 只负责内存的申请与释放,不调用构造、析构函数
  2. new/delete 面向对象,天然结合构造、析构,是 C++ 推荐用法;
  3. malloc 申请失败返回 NULLnew 申请失败默认抛出异常。

三、内存对齐

1. 概念

CPU 读取内存时,默认按固定字节块读取。内存对齐是编译器自动调整成员变量偏移地址的规则,目的是提升硬件读写效率,牺牲少量空间换取运行速度。

2. 通用对齐规则

  1. 结构体/类的第一个成员变量,偏移地址为 0;
  2. 后续每个成员变量,偏移地址必须是自身占用字节数的整数倍;
  3. 整体结构体/类的总大小,必须是内部最大基本数据类型字节数的整数倍;
  4. 继承场景下,基类成员同样遵循对齐规则。

3. 常见数据类型字节大小(32/64位系统)

  • char:1 字节
  • short:2 字节
  • int / float:4 字节
  • long long / double / 指针:8 字节(64位系统)

4. 作用与补充

  • 核心作用:适配CPU硬件架构,避免非对齐访问带来的性能损耗;
  • 特殊场景:可通过编译器指令手动关闭/修改对齐规则,多用于网络编程、硬件开发。

四、类与对象的内存布局

类本身不占用内存,只有实例化的对象才会分配内存 ,对象大小由成员变量决定,成员函数不属于对象,统一存放在代码区。

1. 空类

cpp 复制代码
class Empty{};
  • sizeof(Empty) = 1
  • 原理:编译器会给空类分配 1 字节占位符,保证每个对象拥有唯一内存地址,区分不同实例。

2. 普通类(无虚函数、无静态成员)

  • 对象大小 = 所有非静态成员变量占用的总大小(遵循内存对齐规则);
  • 普通成员函数、静态成员变量、静态成员函数,均不占用对象空间。

3. 包含静态成员的类

  • 静态成员变量属于类本身,存放在全局/静态区,所有对象共享同一份;
  • 因此静态成员不增加单个对象的大小

4. 包含虚函数的类(多态底层核心)

当类中声明 virtual 虚函数时,编译器会做额外处理:

  1. 编译器为该类生成一张虚函数表(vtable) ,本质是一个函数指针数组,存储所有虚函数的地址,虚表存放在常量区
  2. 每个对象内部会新增一个虚指针(vptr),指向当前类的虚函数表;
  3. 对象大小 = 原有成员变量大小 + 虚指针大小(64位系统占 8 字节,32位占 4 字节)。

五、虚函数与多态底层原理

动态多态的本质,就是依靠虚指针 + 虚函数表在运行时动态绑定函数地址。

1. 虚表(vtable)特性

  • 每个拥有虚函数的类,独有一张虚表,同类所有对象共享这张表;
  • 继承关系下,子类会继承父类虚表,若子类重写(override)虚函数,会覆盖虚表中对应位置的函数地址;未重写则保留父类函数地址。

2. 动态绑定执行流程

  1. 使用父类指针/引用指向子类对象;
  2. 调用虚函数时,编译器不会在编译期确定函数地址;
  3. 运行时:通过对象内部的虚指针 vptr → 找到对应虚表 vtable;
  4. 在虚表中读取目标函数的真实地址,完成调用,实现运行时多态

3. 关键限制与规则

  1. 构造函数不能声明为虚函数:对象创建阶段虚指针还未初始化,无法访问虚表;
  2. 静态成员函数不能声明为虚函数:静态函数不属于对象,没有this指针,无法通过vptr查找虚表;
  3. 析构函数建议声明为虚函数:当父类指针释放子类对象时,保证子类析构函数优先执行,避免内存泄漏。

4. 继承场景下的虚表变化

  • 单继承:子类虚表整合父类虚函数,重写则覆盖地址;
  • 多继承:子类会存在多张虚表,对应每一个父类;
  • 菱形继承+虚继承:引入虚基类表,解决数据冗余与二义性问题,内存布局会进一步复杂化。

六、补充:C++ 标准内存模型(并发内存模型)

C++11 及以后定义了抽象内存模型,规范多线程场景下变量的读写、指令重排规则,保证并发安全:

  1. 规定线程之间共享数据的可见性、原子操作规则;
  2. 限制编译器、CPU 的指令重排序,避免多线程下逻辑错乱;
  3. 配套关键字:volatile(保证内存可见性)、std::atomic(原子类型)、内存序(memory_order);
  4. 应用场景:多线程、并发编程、锁机制底层依赖。
相关推荐
zincsweet1 小时前
Linux 命名管道(FIFO)详解:原理分析、源码封装与通信流程图解
linux·服务器·c++·流程图
兰令水1 小时前
2026.5.30休息一天
java
公众号-老炮说Java1 小时前
Spring AI Alibaba 硬核实战:Token 原理 → RAG → 多智能体,一篇通
java·人工智能·后端·spring
Kurisu5751 小时前
深度解析:Java 对象的内存布局与指针压缩原理
java·开发语言
garmin Chen1 小时前
Elasticsearch(2):JavaRestClient操作Elasticsearch全流程实战指南
java·大数据·elasticsearch·搜索引擎
zoyation1 小时前
Spring Boot多数据源
java·spring boot·后端
i220818 Faiz Ul1 小时前
在线预约导游|基于SSM+vue的在线预约导游系统(源码+数据库+文档)
java·前端·数据库·vue.js·spring boot·毕设·在线预约导游系统
旺仔老馒头.2 小时前
【C++】类和对象(三)
开发语言·c++·程序人生·类和对象
Zklys2 小时前
Cmake的学习笔记step1
c++·笔记·学习