目录
[一、 内存布局 (Memory Layout)](#一、 内存布局 (Memory Layout))
[1. C++ 程序的内存分区](#1. C++ 程序的内存分区)
[2. 堆 (Heap) 和 栈 (Stack) 的区别](#2. 堆 (Heap) 和 栈 (Stack) 的区别)
[二、 内存分配 (Memory Allocation)](#二、 内存分配 (Memory Allocation))
[1. new / delete 和 malloc / free 的区别](#1. new / delete 和 malloc / free 的区别)
[2. delete 和 delete[] 的区别](#2. delete 和 delete[] 的区别)
[3. 什么是内存泄漏 (Memory Leak)?如何检测?](#3. 什么是内存泄漏 (Memory Leak)?如何检测?)
[4. 什么是内存对齐 (Memory Alignment)?为什么要对齐?](#4. 什么是内存对齐 (Memory Alignment)?为什么要对齐?)
[三、 编译链接 (Compilation & Linking)](#三、 编译链接 (Compilation & Linking))
[1. 从源码到可执行文件的四个步骤](#1. 从源码到可执行文件的四个步骤)
[2. 静态链接和动态链接的区别](#2. 静态链接和动态链接的区别)
一、 内存布局 (Memory Layout)
C++ 程序在运行时的内存空间通常被划分为以下几个主要区域(从高地址到低地址通常为栈向堆方向):
1. C++ 程序的内存分区
-
栈区 (Stack):
-
由编译器自动分配和释放。
-
存放函数的参数值、局部变量、返回值地址等。
-
-
堆区 (Heap):
-
由程序员手动分配(
new/malloc)和释放(delete/free)。 -
若程序员不释放,程序结束时可能由 OS 回收。
-
-
全局/静态区 (Global/Static):
-
.data段:存放已初始化的全局变量和静态变量。 -
.bss段:存放未初始化的全局变量和静态变量(程序启动前由内核清零)。
-
-
常量区 (Literal/Constant):
-
通常对应
.rodata段。 -
存放常量字符串、
const修饰的全局变量。此区域只读,修改会导致非法访问(Segmentation Fault)。
-
-
代码区 (Code/Text):
- 存放函数体的二进制机器指令。
2. 堆 (Heap) 和 栈 (Stack) 的区别
这是面试中最高频的考点之一,建议从以下四个维度对比:
| 特性 | 栈 (Stack) | 堆 (Heap) |
|---|---|---|
| 申请方式 | 系统自动分配 。例如声明 int a;。 |
程序员手动申请 。例如 new int(10);。 |
| 分配效率 | 极高。仅需移动栈指针(寄存器操作),且有专门的指令支持。 | 较低。C 库需遍历空闲链表寻找合适大小的内存块,可能涉及系统调用。 |
| 空间大小 | 较小。通常为几 MB(如 Linux 默认 8MB,Windows 默认 1MB)。 | 很大。受限于虚拟内存空间,32位系统理论可近 4GB(实际约 2-3GB)。 |
| 碎片问题 | 无碎片。严格遵循 LIFO(后进先出),内存是连续的。 | 容易产生碎片。频繁的分配释放会导致内存空间不连续。 |
二、 内存分配 (Memory Allocation)
1. new / delete 和 malloc / free 的区别
| 特性 | new / delete | malloc / free |
|---|---|---|
| 本质 | C++ 运算符 (Operator),可重载。 | C 标准库函数。 |
| 构造/析构 | 自动调用 。new 先分配内存再调用构造函数;delete 先调析构再释放内存。 |
不调用。仅负责分配和释放纯粹的内存字节。 |
| 类型安全 | 类型安全。返回具体类型指针,无需强转。 | 不安全 。返回 void*,需要强制类型转换。 |
| 大小计算 | 编译器自动计算类型大小。 | 需要手动传递字节数(如 sizeof(T))。 |
2. delete 和 delete[] 的区别
-
区别:
-
delete:用于释放单个对象。 -
delete[]:用于释放数组。它会根据数组前的"cookie"(记录数组大小的信息)知道要调用多少次析构函数。
-
-
如果在数组上错用
delete(如int* p = new int[10]; delete p;):-
对于内置类型 (int, char):通常不会出大问题,内存会被释放(因为 allocator 知道块的大小),但这是 Undefined Behavior (UB)。
-
对于自定义类 (Class) :只会调用第一个对象的析构函数 ,剩余 9 个对象的析构函数不会被调用。如果对象内部管理着资源(如打开的文件、动态申请的内存),将导致严重的资源泄漏。
-
3. 什么是内存泄漏 (Memory Leak)?如何检测?
-
定义:程序动态申请了堆内存,但使用完后没有释放,导致这部分内存既不能被程序再次使用,也不能被操作系统回收(直到进程结束)。
-
检测手段:
-
Linux : 使用 Valgrind (
valgrind --leak-check=full ./app)。 -
Windows (VS) : 使用 CRT Debug Library (
_CrtDumpMemoryLeaks()) 或 Visual Studio 自带的诊断工具。 -
代码层面 : 使用智能指针 (
std::unique_ptr,std::shared_ptr) 实现 RAII,从根源杜绝泄漏。
-
4. 什么是内存对齐 (Memory Alignment)?为什么要对齐?
-
定义 :数据在内存中的起始地址必须是其类型大小(或特定数值)的整数倍。例如,4 字节的
int通常存储在地址能被 4 整除的地方。 -
原因:
-
性能 (Performance):CPU 读取内存通常按块读取(如 4 或 8 字节)。如果数据未对齐(跨越了两个块),CPU 需要做两次内存访问并进行拼接,效率减半。
-
平台兼容性 (Portability):某些硬件架构(如某些 ARM 或 RISC 架构)如果访问未对齐的内存,会直接触发硬件异常(Bus Error / Crash)。
-
三、 编译链接 (Compilation & Linking)
1. 从源码到可执行文件的四个步骤
-
预处理 (Preprocessing) (
g++ -E):-
处理
#include(展开头文件)、#define(宏替换)、#ifdef(条件编译)。 -
删除注释。
-
生成
.i文件。
-
-
编译 (Compilation) (
g++ -S):-
语法分析、词法分析、语义分析。
-
代码优化。
-
将 C++ 代码翻译成汇编代码。
-
生成
.s文件。
-
-
汇编 (Assembly) (
g++ -c):-
将汇编代码翻译成机器能识别的机器码 (Machine Code)。
-
生成
.o(Linux) 或.obj(Windows) 目标文件。
-
-
链接 (Linking) (
g++ -o):-
合并多个
.o文件和库文件。 -
符号解析:找到函数和变量的定义。
-
地址重定位:确定所有符号的最终内存地址。
-
生成可执行文件 (
.exe/.out)。
-
2. 静态链接和动态链接的区别
| 特性 | 静态链接 (Static Linking) | 动态链接 (Dynamic Linking) |
|---|---|---|
| 文件类型 | .a (Linux), .lib (Windows) |
.so (Linux), .dll (Windows) |
| 链接时机 | 编译链接阶段,代码被复制进可执行文件。 | 程序运行阶段,代码仅被引用,不复制。 |
| 可执行文件大小 | 大(包含所有依赖库代码)。 | 小(仅包含引用信息)。 |
| 依赖性 | 低。发布时不需带库文件,独立运行。 | 高。运行环境必须有对应版本的动态库。 |
| 升级维护 | 难。库更新需重新编译发布整个程序。 | 易。只需替换动态库文件即可。 |
后续建议
这些是理论基础,为了加深印象,您可以尝试以下操作:
-
写一个会导致内存泄漏的各种场景的小 Demo,然后用 Valgrind 跑一下,看它输出的报告是什么样的。
-
查看汇编代码 :写一个简单的
main.cpp,使用g++ -E main.cpp > main.i和g++ -S main.cpp,亲自查看预处理和编译后的文件内容。