一、程序内存五大分区
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 执行流程
- 底层调用
malloc在堆区申请一块连续内存; - 调用类的构造函数,完成对象初始化;
- 返回该内存的指针。
2. delete 执行流程
- 调用类的析构函数,释放对象内部资源;
- 底层调用
free,释放堆上的内存空间。
3. 数组形式 new\[\] / delete\[\]
new[]:申请连续数组内存,额外记录数组元素个数,依次调用每个元素的构造函数;delete[]:根据记录的元素个数,逐个调用析构函数,再统一释放内存。- 注意:new\[\] 必须匹配 delete\[\],混用会导致析构函数调用不全、内存泄漏。
4. 与 malloc / free 核心区别
malloc/free只负责内存的申请与释放,不调用构造、析构函数;new/delete面向对象,天然结合构造、析构,是 C++ 推荐用法;malloc申请失败返回NULL,new申请失败默认抛出异常。
三、内存对齐
1. 概念
CPU 读取内存时,默认按固定字节块读取。内存对齐是编译器自动调整成员变量偏移地址的规则,目的是提升硬件读写效率,牺牲少量空间换取运行速度。
2. 通用对齐规则
- 结构体/类的第一个成员变量,偏移地址为 0;
- 后续每个成员变量,偏移地址必须是自身占用字节数的整数倍;
- 整体结构体/类的总大小,必须是内部最大基本数据类型字节数的整数倍;
- 继承场景下,基类成员同样遵循对齐规则。
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 虚函数时,编译器会做额外处理:
- 编译器为该类生成一张虚函数表(vtable) ,本质是一个函数指针数组,存储所有虚函数的地址,虚表存放在常量区;
- 每个对象内部会新增一个虚指针(vptr),指向当前类的虚函数表;
- 对象大小 = 原有成员变量大小 + 虚指针大小(64位系统占 8 字节,32位占 4 字节)。
五、虚函数与多态底层原理
动态多态的本质,就是依靠虚指针 + 虚函数表在运行时动态绑定函数地址。
1. 虚表(vtable)特性
- 每个拥有虚函数的类,独有一张虚表,同类所有对象共享这张表;
- 继承关系下,子类会继承父类虚表,若子类重写(override)虚函数,会覆盖虚表中对应位置的函数地址;未重写则保留父类函数地址。
2. 动态绑定执行流程
- 使用父类指针/引用指向子类对象;
- 调用虚函数时,编译器不会在编译期确定函数地址;
- 运行时:通过对象内部的虚指针 vptr → 找到对应虚表 vtable;
- 在虚表中读取目标函数的真实地址,完成调用,实现运行时多态。
3. 关键限制与规则
- 构造函数不能声明为虚函数:对象创建阶段虚指针还未初始化,无法访问虚表;
- 静态成员函数不能声明为虚函数:静态函数不属于对象,没有this指针,无法通过vptr查找虚表;
- 析构函数建议声明为虚函数:当父类指针释放子类对象时,保证子类析构函数优先执行,避免内存泄漏。
4. 继承场景下的虚表变化
- 单继承:子类虚表整合父类虚函数,重写则覆盖地址;
- 多继承:子类会存在多张虚表,对应每一个父类;
- 菱形继承+虚继承:引入虚基类表,解决数据冗余与二义性问题,内存布局会进一步复杂化。
六、补充:C++ 标准内存模型(并发内存模型)
C++11 及以后定义了抽象内存模型,规范多线程场景下变量的读写、指令重排规则,保证并发安全:
- 规定线程之间共享数据的可见性、原子操作规则;
- 限制编译器、CPU 的指令重排序,避免多线程下逻辑错乱;
- 配套关键字:
volatile(保证内存可见性)、std::atomic(原子类型)、内存序(memory_order); - 应用场景:多线程、并发编程、锁机制底层依赖。