跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI 生图工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++算法

C/C++ 内存分布深度剖析:从代码区到堆栈,new/delete 与 malloc/free 详解

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

二进制发布于 2026/3/30更新于 2026/9/1079 浏览
C/C++ 内存分布深度剖析:从代码区到堆栈,new/delete 与 malloc/free 详解

C/C++ 内存分布深度剖析:从代码区到堆栈,new/delete 与 malloc/free 详解

前言

在 C/C++ 程序的世界中,内存管理是开发者必须掌握的核心技能之一。从代码区的静态指令到堆栈的动态分配,从 C 语言的 malloc/free 到 C++ 的 new/delete,内存的布局与管理方式直接影响程序的性能、稳定性与安全性。理解这些机制不仅能帮助开发者编写高效的代码,还能避免内存泄漏、野指针等常见问题。

本文将系统剖析 C/C++ 内存分布的各个区域,对比 C 语言与 C++ 在动态内存管理上的异同,深入探讨 new/delete 的实现原理及其与 malloc/free 的本质区别。同时,还会介绍 operator new、定位 new 表达式等高级特性,为读者呈现一套完整的内存管理知识体系。

1. C/C++ 内存分布

程序的内存布局显示了其数据在执行过程中如何存储在内存中。它帮助开发者高效理解和管理内存。

  • 内存被划分为代码、数据、堆和栈等部分。
  • 了解内存布局有助于优化性能、调试以及防止分段错误和内存泄漏等错误。

内存分布示意图

1.1 栈 (Stack)

栈又叫堆栈 – 非静态局部变量/函数参数/返回值等等,栈是向下增长的。

存储内容: ├── 局部变量(非 static) ├── 函数参数 ├── 函数返回地址 ├── 函数调用的上下文信息(寄存器状态等) └── 局部常量(const 局部变量)

特点: ├── 编译器自动管理(自动分配和释放) ├── 从高地址向低地址增长 ↓ ├── 函数结束时自动释放 └── 空间有限(通常 1-8MB)

void func(int param) {
    // param 在栈上
    int local = 10; // 局部变量在栈上
    const int LOCAL_CONST = 5; // 局部常量也在栈上
    char buffer[100]; // 局部数组在栈上
}
// 函数结束,所有局部变量自动释放

1.2 内存映射段 (Memory Mapped Segment)

内存映射段是高效的 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

1.3 堆 (Heap)

堆用于程序运行时动态内存分配,堆是可以上增长的。

存储内容: ├── malloc / calloc / realloc 分配的内存 ├── new / new[] 分配的内存 └── 动态创建的对象

特点: ├── 程序员手动管理(分配和释放) ├── 从低地址向高地址增长 ↑ ├── 生命周期由程序员控制 └── 不释放会导致内存泄漏

int* p = (int*)malloc(sizeof(int) * 10); // 堆上分配
int* arr = new int[100]; // C++ 堆上分配
free(p); // 必须手动释放
delete[] arr; // 必须手动释放

1.4 数据段 (Data Segment)

数据段 – 存储全局数据和静态数据。

存储内容: ├── 已初始化的全局变量 └── 已初始化的静态变量(static)

特点:可读写,程序启动时加载

int global_var = 100; // 已初始化全局变量
static int static_var = 50; // 已初始化静态变量

1.5 代码段 (Code Segment)

代码段 – 可执行的代码/只读常量。

存储内容: ├── 程序的可执行机器指令(编译后的代码) ├── 字符串常量(如 "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 为局部变量在栈

2. C 语言中动态内存管理方式:malloc/calloc/realloc/free

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);
}

【面试题】

  1. malloc/calloc/realloc 的区别?

三者都是 C 标准库的堆内存分配函数,核心区别在于:malloc 只分配不初始化,calloc 分配并清零,realloc 用于调整已有内存大小。

calloc 相当于 malloc + memset(),但性能略低,适合需要初始值为 0 的场景,比如初始化数组或结构体。

realloc 有一个常见陷阱,它可能返回新地址,如果内存不够用,它会在别处重新分配并把数据拷过去。所以不能用原指针接收返回值,否则失败返回 NULL,原指针就丢了,造成内存泄漏。

  • realloc(NULL, size) 等价于 malloc(size),这是标准保证的行为;
  • calloc 的清零在某些系统上会利用 OS 已清零的页,不一定比 malloc + memset 慢;
  • 实际项目中更推荐封装一层,统一做 NULL 检查,避免忘记处理分配失败。

在现代 C++ 项目里我们一般使用 new/delete 或智能指针,但在嵌入式、性能敏感或纯 C 场景下,这三个函数还是很有用的,理解它们的行为差异对排查内存问题很重要。

  1. malloc 的实现原理?

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 去拿,比遍历链表快很多。

第四层:两个重要机制(加分)

  1. 内存合并(Coalescing)
free(B) 之后: [ A(used) | B(free) | C(free) ] → [ A(used) | B+C(free) ]

相邻的空闲块会合并,防止碎片化。glibc 用 footer 记录前一块状态,O(1) 完成合并。

  1. 线程安全

glibc 的 ptmalloc 给每个线程分配独立的 arena(内存池),减少锁竞争。这也是 tcmalloc、jemalloc 这些替代品的优化核心——更细粒度的线程本地缓存。

收尾(体现工程视野)

glibc 默认的 ptmalloc 在多线程高并发下性能一般,所以很多高性能项目会替换成 tcmalloc(Google)或 jemalloc(Facebook),它们在线程本地缓存和碎片控制上做了更多优化。

OS 怎么给内存 → malloc 怎么管理 → 怎么找空闲块 → 碎片怎么处理 → 多线程怎么办
(brk/mmap) (空闲链表+header) (分箱策略) (合并) (arena/tcmalloc)

3. C++ 内存管理方式

C 语言内存管理方式在 C++ 中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,因此 C++ 又提出了自己的内存管理方式:通过 new 和 delete 操作符进行动态内存管理。

C++ 中通过 new 和 delete 操作符进行动态内存管理。

  • new 负责申请内存,new 操作符返回的是申请到的内存空间的起始地址,需要指针存放。
    • new 申请一个变量的空间,new[] 申请一个数组的空间
  • delete 负责释放(回收)内存
    • delete 负责释放一个变量的空间,delete[] 释放一个数组的空间
  • new 和 delete 配对,new[] 和 delete[] 配对使用

3.1 new/delete 操作内置类型

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 操作符,申请和释放连续的空间,使用 new[] 和 delete[],注意:匹配起来使用。

3.2 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(1, 1);
    A aa2(2, 2);
    A aa3(3, 3);
    A* p3 = new A[3]{aa1, aa2, aa3}; // 用匿名对象:构造 + 拷贝构造 -> 构造
    A* p4 = new A[3]{A(1, 1), A(2, 2), A(3, 3)};
    // 用隐式类型转换
    // A aa1 = { 1, 1 };
    A* p5 = new A[3]{{1, 1}, {2, 2}, {3, 3}};
    return 0;
}

new/delete 自定义类型

4. operator new 与 operator delete 函数

4.1 operator new 与 operator delete 函数

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 来释放空间的。

5. new 和 delete 的实现原理

5.1 内置类型

如果申请的是内置类型的空间,new 和 malloc,delete 和 free 基本类似,不同的地方是:new/delete 申请和释放的是单个元素的空间,new[] 和 delete[] 申请的是连续空间,而且 new 在申请空间失败时会抛异常,malloc 会返回 NULL。

5.2 自定义类型

new 的原理:

  1. 调用 operator new 函数申请空间;
  2. 在申请的空间上执行构造函数,完成对象的构造。

new 原理

delete 的原理:

  1. 在空间上执行析构函数,完成对象中资源的清理工作;
  2. 调用 operator delete 函数释放对象的空间。

delete 原理

new T[N] 的原理:

  1. 调用 operator new[] 函数,在 operator new[] 中实际调用 operator new 函数完成 N 个对象空间的申请;
  2. 在申请的空间上执行 N 次构造函数。

delete[] 的原理:

  1. 在释放的对象空间上执行 N 次析构函数,完成 N 个对象中资源的清理;
  2. 调用 operator delete[] 释放空间,实际在 operator delete[] 中调用 operator delete 来释放空间。

6. 定位 new 表达式 (placement-new)

定位 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;
}

7. malloc/free 和 new/delete 的区别

malloc/free 和 new/delete 的共同点是:都是从堆上申请空间,并且需要用户手动释放。不同的地方是:

  1. malloc 和 free 是函数,new 和 delete 是操作符;
  2. malloc 申请的空间不会初始化,new 可以初始化;
  3. malloc 申请空间时,需要手动计算空间大小并传递,new 只需在其后跟上空间的类型即可,如果是多个对象,[] 中指定对象个数即可;
  4. malloc 的返回值为 void,在使用时必须强转,new 不需要,因为 new 后跟的是空间的类型;
  5. malloc 申请空间失败时,返回的是 NULL,因此使用时必须判空,new 不需要,但是 new 需要捕获异常;
  6. 申请自定义类型对象时,malloc/free 只会开辟空间,不会调用构造函数与析构函数,而 new 在申请空间后会调用构造函数完成对象的初始化,delete 在释放空间前会调用析构函数完成空间中资源的清理释放。

结语

深入理解 C/C++ 内存分布和动态内存管理机制是编写高效、安全程序的关键基础。从代码区、静态存储区到堆栈的布局,再到 malloc/free 与 new/delete 的底层实现差异,每个环节都直接影响程序的性能和稳定性。

C 语言通过 malloc/calloc/realloc/free 提供灵活的手动内存管理,而 C++ 的 new/delete 不仅实现了内存分配释放,还融合了构造函数和析构函数的调用,为面向对象编程提供完整支持。operator new/delete 的底层机制、定位 new 表达式等高级特性,进一步扩展了内存控制的精细度。

掌握这些知识能帮助开发者避免内存泄漏、野指针等常见问题,同时在性能优化和复杂系统设计中做出明智选择。理解不同内存管理方式的适用场景,是每位 C/C++ 程序员从入门到精通的必经之路。

目录

  1. C/C++ 内存分布深度剖析:从代码区到堆栈,new/delete 与 malloc/free 详解
  2. 前言
  3. 1. C/C++ 内存分布
  4. 1.1 栈 (Stack)
  5. 1.2 内存映射段 (Memory Mapped Segment)
  6. 1.3 堆 (Heap)
  7. 1.4 数据段 (Data Segment)
  8. 1.5 代码段 (Code Segment)
  9. 2. C 语言中动态内存管理方式:malloc/calloc/realloc/free
  10. 3. C++ 内存管理方式
  11. 3.1 new/delete 操作内置类型
  12. 3.2 new 和 delete 操作自定义类型
  13. 4. operator new 与 operator delete 函数
  14. 4.1 operator new 与 operator delete 函数
  15. 5. new 和 delete 的实现原理
  16. 5.1 内置类型
  17. 5.2 自定义类型
  18. 6. 定位 new 表达式 (placement-new)
  19. 7. malloc/free 和 new/delete 的区别
  20. 结语

更多推荐文章

查看全部
  • Ubuntu 升级 OpenSSH 10.2p1 与 OpenSSL 3.5.4 避坑实践
  • AMD 显卡部署 llama.cpp 的兼容性解决方案与性能优化
  • Rokid JSAR 开发实战:Web 技术栈 AR 环境搭建与 3D 时钟项目解析
  • 开源知识库 RAGFlow 从部署到实战操作详解
  • Spring Boot智能仓库管理系统设计与实现
  • 企业级大模型接入全流程:从需求分析到持续优化的八步指南
  • Redis Java 客户端选型与 SpringBoot 集成指南
  • 前端下载二进制流文件的几种做法
  • LLaMA-Factory 大模型 LoRA 微调实战指南
  • 机器学习中的逻辑回归
  • VSCode 自定义 Copilot Agent 与 Awesome Agent 模板
  • NDM(Neat Download Manager)安装配置教程(Windows/macOS)
  • Docker 容器实战:MySQL、Redis 部署与资源管理
  • SSM 框架下二手房公司管理系统的设计与实现
  • Linux 高级 IO:Epoll 模型与 TCP 服务器实现
  • 网络安全基础概念、核心领域及常见威胁解析
  • Stable Diffusion WebUI 本地部署教程
  • LeetCode 原地复写零:双指针逆向填充 O(n) 时间 O(1) 空间解法
  • 科大讯飞、小米、百度等互联网大厂算法工程师面试题汇总
  • OpenClaw 漏洞预警:AI 代理安全审计与日志追溯

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online