C/C++ 内存分布深度剖析:从代码区到堆栈,new/delete 与 malloc/free 详解
深入剖析 C/C++ 程序内存分布,涵盖代码段、数据段、堆、栈等区域。对比了 C 语言 malloc/calloc/realloc/free 与 C++ new/delete 操作符的区别,详细讲解了 operator new/delete 底层实现、定位 new 表达式以及内存分配策略(如空闲链表、分箱)。旨在帮助开发者理解内存管理机制,避免泄漏与野指针,提升程序性能与稳定性。

深入剖析 C/C++ 程序内存分布,涵盖代码段、数据段、堆、栈等区域。对比了 C 语言 malloc/calloc/realloc/free 与 C++ new/delete 操作符的区别,详细讲解了 operator new/delete 底层实现、定位 new 表达式以及内存分配策略(如空闲链表、分箱)。旨在帮助开发者理解内存管理机制,避免泄漏与野指针,提升程序性能与稳定性。

在 C/C++ 程序的世界中,内存管理是开发者必须掌握的核心技能之一。从代码区的静态指令到堆栈的动态分配,从 C 语言的 malloc/free 到 C++ 的 new/delete,内存的布局与管理方式直接影响程序的性能、稳定性与安全性。理解这些机制不仅能帮助开发者编写高效的代码,还能避免内存泄漏、野指针等常见问题。
本文将系统剖析 C/C++ 内存分布的各个区域,对比 C 语言与 C++ 在动态内存管理上的异同,深入探讨 new/delete 的实现原理及其与 malloc/free 的本质区别。同时,还会介绍 operator new、定位 new 表达式等高级特性,为读者呈现一套完整的内存管理知识体系。
程序的内存布局显示了其数据在执行过程中如何存储在内存中。它帮助开发者高效理解和管理内存。

栈又叫堆栈 – 非静态局部变量/函数参数/返回值等等,栈是向下增长的。
存储内容: ├── 局部变量(非 static) ├── 函数参数 ├── 函数返回地址 ├── 函数调用的上下文信息(寄存器状态等) └── 局部常量(const 局部变量)
特点: ├── 编译器自动管理(自动分配和释放) ├── 从高地址向低地址增长 ↓ ├── 函数结束时自动释放 └── 空间有限(通常 1-8MB)
void func(int param) {
// param 在栈上
int local = 10; // 局部变量在栈上
const int LOCAL_CONST = 5; // 局部常量也在栈上
char buffer[100]; // 局部数组在栈上
}
// 函数结束,所有局部变量自动释放
内存映射段是高效的 I/O 映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享内存,做进程间通信。
定义:通过 mmap() 系统调用映射的内存区域
存储内容: ├── 共享库 ├── 内存映射文件 ├── 共享内存 ├── 匿名映射(大块内存分配) └── 动态链接库加载区域
特点: ├── 动态创建,不在可执行文件中预定义 ├── 可以与文件关联,也可以是匿名的 ├── 可以设置共享或私有 ├── 大块内存分配时 malloc 可能使用 mmap └── 生命周期由程序员控制
// mmap 映射文件
int fd = open("file.txt", O_RDWR);
void* p = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
// 共享内存
int shmid = shmget(key, size, IPC_CREAT | 0666);
void* p = shmat(shmid, NULL, 0);
// 大块内存分配(malloc 内部可能用 mmap)
void* big = malloc(128 * 1024); // >128KB 可能用 mmap
堆用于程序运行时动态内存分配,堆是可以上增长的。
存储内容: ├── malloc / calloc / realloc 分配的内存 ├── new / new[] 分配的内存 └── 动态创建的对象
特点: ├── 程序员手动管理(分配和释放) ├── 从低地址向高地址增长 ↑ ├── 生命周期由程序员控制 └── 不释放会导致内存泄漏
int* p = (int*)malloc(sizeof(int) * 10); // 堆上分配
int* arr = new int[100]; // C++ 堆上分配
free(p); // 必须手动释放
delete[] arr; // 必须手动释放
数据段 – 存储全局数据和静态数据。
存储内容: ├── 已初始化的全局变量 └── 已初始化的静态变量(static)
特点:可读写,程序启动时加载
int global_var = 100; // 已初始化全局变量
static int static_var = 50; // 已初始化静态变量
代码段 – 可执行的代码/只读常量。
存储内容: ├── 程序的可执行机器指令(编译后的代码) ├── 字符串常量(如 "Hello World") └── 全局常量(const 全局变量)
特点:只读、可被多个进程共享
const int MAX_SIZE = 100; // 全局常量,存储在代码段
char* str = "Hello"; // "Hello"字符串本身在代码段
接下来我们来看下面一段代码和相关问题:
int globalVar = 1;
static int staticGlobalVar = 1;
void Test() {
static int staticVar = 1;
int localVar = 1;
int num1[10] = {1, 2, 3, 4};
char char2[] = "abcd";
const char* pChar3 = "abcd";
int* ptr1 = (int*)malloc(sizeof(int) * 4);
int* ptr2 = (int*)calloc(4, sizeof(int));
int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4);
free(ptr1);
free(ptr3);
}
选择题: 选项:A.栈 B.堆 C.数据段 (静态区) D.代码段 (常量区) globalVar 在哪里?_____ staticGlobalVar 在哪里?_____ staticVar 在哪里?_____ localVar 在哪里?_____ num1 在哪里?_____
char2 在哪里?_____ *char2 在哪里?_____ pChar3 在哪里?_____ *pChar3 在哪里?_____ ptr1 在哪里?_____ *ptr1 在哪里?_____
答案: C C C A A A A A D A B
主要讲一下后六个空:
abcd\0为代码存在代码段中,char2 在栈上开了 5 个字节的数组并把abcd\0拷贝过来,char2* 指向数组的首元素地址,即 char2 和 char2* 都在栈上;pChar3 为局部变量存在栈上,*pChar3 指向abcd\0字符串,故在代码段上;ptr1 也是栈上的局部变量,*ptr1 指向堆上开的空间,则 ptr 在栈上,*ptr 在堆上。

globalVar 为全局变量在数据段 staticGlobalVar 为静态全局变量在静态区 staticVar 为静态局部变量在静态区 localVar 为局部变量在栈 num1 为局部变量在栈
void Test() {
// 1.malloc/calloc/realloc 的区别是什么?
int* p2 = (int*)calloc(4, sizeof(int));
int* p3 = (int*)realloc(p2, sizeof(int) * 10); // 这里需要 free(p2) 吗?
// 不需要。因为原地扩容 p2 和 p3 一样,如果是异地扩容 p2 会先释放掉
free(p3);
}
【面试题】
三者都是 C 标准库的堆内存分配函数,核心区别在于:malloc 只分配不初始化,calloc 分配并清零,realloc 用于调整已有内存大小。
calloc 相当于 malloc + memset(),但性能略低,适合需要初始值为 0 的场景,比如初始化数组或结构体。
realloc 有一个常见陷阱,它可能返回新地址,如果内存不够用,它会在别处重新分配并把数据拷过去。所以不能用原指针接收返回值,否则失败返回 NULL,原指针就丢了,造成内存泄漏。
在现代 C++ 项目里我们一般使用 new/delete 或智能指针,但在嵌入式、性能敏感或纯 C 场景下,这三个函数还是很有用的,理解它们的行为差异对排查内存问题很重要。
malloc 的核心是向操作系统申请一大块内存,然后自己管理这块内存的分配与回收,尽量减少系统调用次数。
第一层:两个系统调用
malloc 底层靠两个系统调用向 OS 要内存:
| 系统调用 | 用途 | 特点 |
|---|---|---|
brk / sbrk | 移动堆顶指针,扩展堆 | 适合小内存,连续分配 |
mmap | 直接映射一块匿名内存 | 适合大内存(通常 > 128KB) |
小内存用 brk 扩堆,大内存直接 mmap,这样大块内存 free 之后可以直接还给 OS,不会一直占着。
第二层:空闲链表管理(核心)
malloc 不是每次都问 OS 要内存,而是维护一个空闲块链表,free 掉的内存先留着复用:
堆内存布局: [ chunk1 | chunk2(free) | chunk3 | chunk4(free) | ... ]
↓ ↓
free list ←——————————————→ free list
每个内存块前面有一个隐藏的 header,记录元数据:
struct chunk {
size_t size; // 块大小
int is_free; // 是否空闲
struct chunk* next; // 下一个空闲块
};
调用 malloc(n) 拿到的指针,其实指向 header 后面,free(p) 时会往前偏移读取 header,这就是为什么 free 不需要传大小。
第三层:分配策略
面试官可能追问'怎么从空闲链表里找块',常见策略:
| 策略 | 描述 | 特点 |
|---|---|---|
| First Fit | 找第一个够大的块 | 快,但碎片多 |
| Best Fit | 找最接近大小的块 | 碎片少,但慢 |
| Worst Fit | 找最大的块切割 | 剩余块更大,适合大量小分配 |
glibc 的 ptmalloc 用的是分箱(Bins)策略,按大小分类管理:
small bins → 固定大小的小块(< 512B),精确匹配
large bins → 大块,范围匹配
unsorted bin → 刚 free 的块先放这里,延迟归类
fast bins → 极小块的缓存,不合并,极速复用
按大小分箱,找对应的 bin 去拿,比遍历链表快很多。
第四层:两个重要机制(加分)
free(B) 之后: [ A(used) | B(free) | C(free) ] → [ A(used) | B+C(free) ]
相邻的空闲块会合并,防止碎片化。glibc 用 footer 记录前一块状态,O(1) 完成合并。
glibc 的 ptmalloc 给每个线程分配独立的 arena(内存池),减少锁竞争。这也是 tcmalloc、jemalloc 这些替代品的优化核心——更细粒度的线程本地缓存。
收尾(体现工程视野)
glibc 默认的 ptmalloc 在多线程高并发下性能一般,所以很多高性能项目会替换成 tcmalloc(Google)或 jemalloc(Facebook),它们在线程本地缓存和碎片控制上做了更多优化。
OS 怎么给内存 → malloc 怎么管理 → 怎么找空闲块 → 碎片怎么处理 → 多线程怎么办
(brk/mmap) (空闲链表+header) (分箱策略) (合并) (arena/tcmalloc)
C 语言内存管理方式在 C++ 中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,因此 C++ 又提出了自己的内存管理方式:通过 new 和 delete 操作符进行动态内存管理。
C++ 中通过 new 和 delete 操作符进行动态内存管理。

void Test() {
// 动态申请一个 int 类型的空间
int* ptr1 = new int;
// 动态申请一个 int 类型的空间并初始化为 10
int* ptr2 = new int(10);
// 动态申请 10 个 int 类型的空间
int* ptr3 = new int[10];
// 多个对象初始化
int* ptr4 = new int[10]{0}; // 全部初始化为 0
int* ptr5 = new int[10]{1, 2, 3, 4, 5}; // 初始化一部分
delete ptr1;
delete ptr2;
delete[] ptr3;
delete[] ptr4;
delete[] ptr5;
}

注意:申请和释放单个元素的空间,使用 new 和 delete 操作符,申请和释放连续的空间,使用 new[] 和 delete[],注意:匹配起来使用。
#include <iostream>
using namespace std;
class A {
public:
A(int a = 0) : _a(a) { cout << "A():" << this << endl; }
~A() { cout << "~A():" << this << endl; }
private:
int _a;
};
int main() {
// new/delete 和 malloc/free 最大区别是 new/delete 对于【自定义类型】除了开空间还会调用构造函数和析构函数
A* p1 = (A*)malloc(sizeof(A));
A* p2 = new A(1);
free(p1);
delete p2;
// 内置类型是几乎是一样的
int* p3 = (int*)malloc(sizeof(int));
int* p4 = new int;
free(p3);
delete p4;
A* p5 = (A*)malloc(sizeof(A) * 10);
A* p6 = new A[10];
free(p5);
delete[] p6;
return 0;
}
注意:在申请自定义类型的空间时,new 会调用构造函数,delete 会调用析构函数,而 malloc 与 free 不会。
#include <iostream>
using namespace std;
class A {
public:
A(int a1 = 0, int a2 = 0) : _a1(a1), _a2(a2) { cout << "A(int a1 = 0, int a2 = 0)" << endl; }
A(const A& aa) : _a1(aa._a1) { cout << "A(const A& aa)" << endl; }
A& operator=(const A& aa) { cout << "A& operator=(const A& aa)" << endl;
if (this != &aa) { _a1 = aa._a1; }
return *this;
}
~A() { cout << "~A()" << endl; }
void Print() { cout << "A::Print->" << _a1 << endl; }
A& operator++() { _a1 += 100; return *this; }
private:
int _a1 = 1;
int _a2 = 1;
};
int main() {
A* p1 = new A(1);
A* p2 = new A(2, 2);
// 用有名对象
A aa1;
;
;
A* p3 = A[]{aa1, aa2, aa3};
A* p4 = A[]{(, ), (, ), (, )};
A* p5 = A[]{{, }, {, }, {, }};
;
}

new 和 delete 是用户进行动态内存申请和释放的操作符,operator new 和 operator delete 是系统提供的全局函数,new 在底层调用 operator new 全局函数来申请空间,delete 在底层通过 operator delete 全局函数来释放空间。
/* operator new:该函数实际通过 malloc 来申请空间,当 malloc 申请空间成功时直接返回;申请空间 失败,尝试执行空间不足应对措施,如果改应对措施用户设置了,则继续申请,否则抛异常。 */
void* __CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc) {
// try to allocate size bytes
void* p;
while ((p = malloc(size)) == 0)
if (_callnewh(size) == 0) {
// report no memory
// 如果申请内存失败了,这里会抛出 bad_alloc 类型异常
static const std::bad_alloc nomem;
_RAISE(nomem);
}
return (p);
}
// operator delete: 该函数最终是通过 free 来释放空间的
void operator delete(void* pUserData) {
_CrtMemBlockHeader* pHead;
RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));
if (pUserData == NULL) return;
_mlock(_HEAP_LOCK); /* block other threads */
__TRY /* get a pointer to memory block header */
pHead = pHdr(pUserData);
/* verify block type */
_ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));
_free_dbg(pUserData, pHead->nBlockUse);
__FINALLY _munlock(_HEAP_LOCK); /* release other threads */
__END_TRY_FINALLY
return;
}
// free 的实现
#define free(p) _free_dbg(p, _NORMAL_BLOCK)
通过上述两个全局函数的实现知道,operator new 实际也是通过 malloc 来申请空间,如果 malloc 申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异常。operator delete 最终是通过 free 来释放空间的。
如果申请的是内置类型的空间,new 和 malloc,delete 和 free 基本类似,不同的地方是:new/delete 申请和释放的是单个元素的空间,new[] 和 delete[] 申请的是连续空间,而且 new 在申请空间失败时会抛异常,malloc 会返回 NULL。
new 的原理:

delete 的原理:

new T[N] 的原理:
delete[] 的原理:
定位 new 表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。
使用格式:
new (place_address) type 或者 new (place_address) type(initializer-list)
place_address 必须是一个指针,initializer-list 是类型的初始化列表
使用场景: 定位 new 表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用 new 的定义表达式进行显示调构造函数进行初始化。
#include <iostream>
using namespace std;
class A {
public:
A(int a = 0) : _a(a) { cout << "A():" << this << endl; }
~A() { cout << "~A():" << this << endl; }
private:
int _a;
};
// 定位 new/replacement new
int main() {
// p1 现在指向的只不过是与 A 对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行
A* p1 = (A*)malloc(sizeof(A));
new (p1) A; // 注意:如果 A 类的构造函数有参数时,此处需要传参
p1->~A();
free(p1);
A* p2 = (A*)operator new(sizeof(A));
new (p2) A(10);
p2->~A();
operator delete(p2);
return 0;
}
malloc/free 和 new/delete 的共同点是:都是从堆上申请空间,并且需要用户手动释放。不同的地方是:
[] 中指定对象个数即可;深入理解 C/C++ 内存分布和动态内存管理机制是编写高效、安全程序的关键基础。从代码区、静态存储区到堆栈的布局,再到 malloc/free 与 new/delete 的底层实现差异,每个环节都直接影响程序的性能和稳定性。
C 语言通过 malloc/calloc/realloc/free 提供灵活的手动内存管理,而 C++ 的 new/delete 不仅实现了内存分配释放,还融合了构造函数和析构函数的调用,为面向对象编程提供完整支持。operator new/delete 的底层机制、定位 new 表达式等高级特性,进一步扩展了内存控制的精细度。
掌握这些知识能帮助开发者避免内存泄漏、野指针等常见问题,同时在性能优化和复杂系统设计中做出明智选择。理解不同内存管理方式的适用场景,是每位 C/C++ 程序员从入门到精通的必经之路。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online