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. 应用场景:多线程、并发编程、锁机制底层依赖。
相关推荐
nanxun8863 小时前
记一次诡异的 Docker 容器"串包"故障排查
java
用户1563068103516 小时前
Day01 | Java 基础(Java SE)
java
行者全栈架构师7 小时前
Maven dependency:tree 的 8 个高级用法
java·后端
行者全栈架构师11 小时前
IDEA 中 Maven 项目的 15 个红色报错快速解决方法
java·后端
令人头秃的代码0_011 小时前
mac(m5)平台编译openjdk
java
唐青枫1 天前
Java JDBC 实战指南:从 Connection 到事务和连接池
java
一个做软件开发的牛马1 天前
MyBatis-Plus 从零实战:完整搭建可运行 Demo,BaseMapper 零 SQL、Wrapper 条件构造、分页插件与代码生成器详解
java·后端
用户3721574261351 天前
Java 处理 PDF 图片:提取 PDF 中的图片,并压缩 PDF 图片体积
java
用户3721574261351 天前
Java 打印 Word 文档:从基础打印到高级设置
java
用户3521802454752 天前
当 Prompt 学会"热更新":Spring Boot × Nacos3 AI 实战
java·spring boot·ai编程