在 C/C++ 开发中,内存管理是决定程序性能、稳定性的核心环节,也是面试高频考点。不少开发者对内存分布、动态内存分配方式的理解停留在表面,导致实际开发中频繁出现内存泄漏、野指针等问题。本文将从内存分布入手,逐步剖析 C / C++ 的内存管理方式,深入讲解 new/delete 的底层实现,对比不同管理方式的差异,帮大家构建完整的内存管理知识体系。下面就让我们正式开始吧!
要做好内存管理,首先得明确程序运行时内存的划分的区域。不同区域的内存,其生命周期、管理方式完全不同。我们通过一段代码结合选择题,直观理解各变量的存储位置。
先看这段包含全局变量、静态变量、局部变量、动态内存的代码:
// 全局变量
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);
}程序运行时,内存主要划分为栈、堆、数据段(静态区)、代码段(常量区)、内存映射段(Linux 系统中,用于共享库、进程间通信,暂不深入)。各区域的功能与变量存储规则如下:
内存区域 | 功能描述 |
|---|---|
栈(Stack) | 存储非静态局部变量、函数参数、返回值;向下增长(地址从高到低分配) |
堆(Heap) | 存储程序运行时动态分配的内存;向上增长(地址从低到高分配) |
数据段(静态区) | 存储全局变量、静态变量(static 修饰);程序启动时分配,退出时释放 |
代码段(常量区) | 存储可执行代码、只读常量(如字符串常量 "abcd");只读属性 |
基于以上规则,我们来来做几道变量存储位置相关的选择题:
(选项:A. 栈 B. 堆 C. 数据段 D. 代码段)

说明:
在C 语言中,是通过 4 个函数来实现动态内存管理的,这是 C++ 内存管理的基础。这四个函数其实我们在C语言篇时已经为大家介绍过了,这里就帮大家复习一下。
void* malloc(size_t size);void*指针,失败返回NULL。// 申请4个int大小的内存(int占4字节,共16字节)
int* p = (int*)malloc(4 * sizeof(int));
if (p == NULL) { // 必须判空,防止malloc失败
perror("malloc failed");
return 1;
}void* calloc(size_t num, size_t size);void*指针,失败返回NULL。// 申请4个int大小的内存,并初始化为0
int* p = (int*)calloc(4, sizeof(int));
if (p == NULL) {
perror("calloc failed");
return 1;
}void* realloc(void* ptr, size_t size);ptr指向的动态内存大小为size 字节,分两种情况: ptr;NULL(原内存不会释放)。int* p = (int*)malloc(4 * sizeof(int));
if (p == NULL) return 1;
// 将内存大小调整为8个int(32字节)
int* new_p = (int*)realloc(p, 8 * sizeof(int));
if (new_p == NULL) {
perror("realloc failed");
free(p); // 原内存未释放,需手动释放
return 1;
}
p = new_p; // 更新指针,指向新内存void free(void* ptr);ptr指向的动态内存(归还给堆),避免内存泄漏。ptr必须指向malloc/calloc/realloc分配的内存,不能释放栈上内存;NULL指针无效果(可安全调用free(NULL))。int* p = (int*)malloc(4 * sizeof(int));
free(p); // 释放内存
p = NULL; // 避免野指针(释放后指针仍指向原地址,需置空)这是面试中必问的基础题,其核心区别体现在初始化和功能场景上:
1. 初始化差异:
malloc不初始化内存,分配后内存内容是随机值;calloc会将内存初始化为 0,适合需要 “干净” 内存的场景(如数组初始化);realloc不改变原有内存的内容(仅扩展或拷贝),新扩展的内存部分是随机值。2. 参数与功能差异:
malloc仅需传入 “总字节数”,功能是 “分配固定大小内存”;calloc需传入 “元素个数” 和 “单个元素字节数”,功能是 “分配并初始化多个相同大小的元素内存”;realloc需传入 “原内存指针” 和 “新内存总字节数”,功能是 “调整已分配内存的大小”。3. 返回值处理差异:
NULL,但realloc失败时原内存不会释放,需手动释放原指针,避免内存泄漏。 glibc 是 Linux 系统下的 C 标准库,其malloc实现基于ptmalloc(Ptmalloc2),核心思想是 “内存池 + 空闲块管理”,避免频繁向操作系统申请内存(系统调用开销大)。其关键机制如下:
1. 内存池划分:
ptmalloc 将堆内存划分为多个 “内存池”(Arena),每个线程默认有一个 Arena,减少多线程竞争(线程安全)。
2. 空闲块管理:
空闲内存块按大小分类,用 “双向链表”(空闲链表)管理:
3. 内存分配策略:
sbrk或mmap系统调用),减少空闲块碎片。4. 内存释放策略:
释放内存时,会检查相邻的空闲块,若存在则 “合并” 为大空闲块,减少碎片;若空闲块足够大,会归还给操作系统,避免内存浪费。
C 语言的内存管理方式在 C++ 中仍可使用,但存在明显缺陷,即无法自动调用自定义类型的构造 / 析构函数、使用繁琐(需手动计算大小、强转等)。因此 C++ 引入了new和delete操作符,专门用于动态内存管理。
对于 int、char 等内置类型,new/delete的功能与malloc/free类似,但用法更简洁,且失败时的处理方式不同。
类型* 指针 = new 类型(初始化值);(初始化可选)delete 指针;// 分配单个int,不初始化(内存为随机值)
int* p1 = new int;
// 分配单个int,初始化为10
int* p2 = new int(10);
delete p1; // 释放p1
delete p2; // 释放p2类型* 指针 = new 类型[元素个数];(初始化可选,C++11 后支持列表初始化)delete[] 指针;(必须用delete[],否则会内存泄漏)// 分配10个int,不初始化
int* p3 = new int[10];
// 分配10个int,列表初始化(C++11及以上)
int* p4 = new int[10]{1,2,3,4,5};
delete[] p3; // 必须用delete[],释放连续内存
delete[] p4;
注意:
new对应delete(申请和释放单个元素的空间),new[]对应delete[](申请和释放连续的空间),不匹配会导致内存泄漏或程序崩溃(尤其是自定义类型);new可直接初始化(如new int(10)),malloc需手动赋值初始化;new失败时会抛出bad_alloc异常(无需判空),malloc失败返回NULL(需判空)。 这是new/delete与malloc/free的核心差异:对于自定义类型,new会自动调用构造函数,delete会自动调用析构函数,而malloc/free仅分配 / 释放内存,不处理构造 / 析构。
#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() {
// 1. malloc/free 处理自定义类型
cout << "=== malloc/free 处理自定义类型 ===" << endl;
A* p1 = (A*)malloc(sizeof(A)); // 仅分配内存,不调用构造函数
free(p1); // 仅释放内存,不调用析构函数
p1 = NULL;
// 2. new/delete 处理自定义类型
cout << "\n=== new/delete 处理自定义类型 ===" << endl;
A* p2 = new A(10); // 1. 分配内存;2. 调用构造函数初始化
delete p2; // 1. 调用析构函数清理资源;2. 释放内存
p2 = NULL;
// 3. new[]/delete[] 处理多个自定义类型
cout << "\n=== new[]/delete[] 处理多个自定义类型 ===" << endl;
A* p3 = new A[3]; // 1. 分配3个A的内存;2. 调用3次构造函数
delete[] p3; // 1. 调用3次析构函数;2. 释放内存
p3 = NULL;
return 0;
} 运行结果如下,我们可清晰看到new/delete对构造 / 析构的调用:
=== malloc/free 处理自定义类型 ===
(无构造/析构打印,仅分配释放内存)
=== new/delete 处理自定义类型 ===
A() 构造函数:0x55f8d7a7a280
~A() 析构函数:0x55f8d7a7a280
=== new[]/delete[] 处理多个自定义类型 ===
A() 构造函数:0x55f8d7a7a290
A() 构造函数:0x55f8d7a7a294
A() 构造函数:0x55f8d7a7a298
~A() 析构函数:0x55f8d7a7a298
~A() 析构函数:0x55f8d7a7a294
~A() 析构函数:0x55f8d7a7a290分析如下:
malloc分配A类型内存时,无构造函数调用,p1指向的内存仅是 “一块大小为sizeof(A)的空间”,不能算完整的A对象;new A(10)会先分配内存,再调用构造函数初始化,p2指向的是完整的A对象;delete p2会先调用析构函数(若A有动态资源,如new的内存,会在此处释放),再释放内存,避免资源泄漏;new[]/delete[]会调用多次构造 / 析构(个数等于元素个数),确保每个对象都被正确初始化和清理。 很多人会误以为new/delete是函数,但实际它们是操作符。new在底层会调用operator new函数申请内存,delete会调用operator delete函数释放内存。这两个函数是系统提供的全局函数,我们可以查看其源码(基于 MSVC 或 glibc)理解实现逻辑。
operator new的核心功能是 “从堆上申请内存”,底层依赖malloc实现,同时处理内存申请失败的场景(抛异常)。下面是 MSVC 编译器中的简化实现:
void* __CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc) {
// 尝试分配size字节的内存
void* p;
while ((p = malloc(size)) == 0) {
// 如果malloc失败,尝试调用用户设置的内存不足处理函数
if (_callnewh(size) == 0) {
// 若没有有效的处理函数,则抛出bad_alloc异常
static const std::bad_alloc nomem;
_RAISE(nomem);
}
}
return p;
}malloc申请成功,直接返回内存指针;malloc申请失败,会尝试调用用户设置的内存不足处理函数;std::bad_alloc异常。 operator delete是系统提供的全局函数,用于释放内存,其底层依赖于free函数。下面是 MSVC 编译器中的简化实现:
void operator delete(void* pUserData) {
_CrtMemBlockHeader* pHead;
if (pUserData == NULL)
return;
// 线程同步,防止多线程冲突
_mlock(_HEAP_LOCK);
__TRY
// 获取内存块头部信息
pHead = pHdr(pUserData);
// 验证内存块类型
_ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));
// 释放内存(最终调用free)
_free_dbg(pUserData, pHead->nBlockUse);
__FINALLY
_munlock(_HEAP_LOCK);
__END_TRY_FINALLY
return;
}
// free的宏定义,实际调用_free_dbg
#define free(p) _free_dbg(p, _NORMAL_BLOCK) operator delete底层是通过free来释放内存,同时还增加了一些调试和线程同步的处理:
NULL,如果是则直接返回;free释放内存。 new和delete的实现原理因操作的类型(内置类型和自定义类型)而有所不同。
对于内置类型(如 int、char 等),new和delete的实现相对简单:
new T:调用operator new(sizeof(T))申请内存,返回指向该内存的指针;delete p:调用operator delete(p)释放内存;new T[N]:调用operator new[](N * sizeof(T))申请内存,返回指向该内存的指针;delete[] p:调用operator delete[](p)释放内存。 内置类型的new/delete与malloc/free的主要区别在于:
new失败时抛出异常,malloc返回NULL;new不需要手动计算内存大小和强转类型。 对于自定义类型,new和delete除了申请和释放内存外,还会调用构造函数和析构函数。
operator new(sizeof(T))申请内存空间new T → operator new(sizeof(T)) → 构造函数p指向的内存空间上调用析构函数,完成对象中资源的清理;operator delete(p)释放内存空间。delete p → 析构函数 → operator delete(p)operator new[](N * sizeof(T))申请内存空间(实际会多申请一些空间存储对象个数);new T[N] → operator new[](N * sizeof(T)) → N次构造函数p指向的内存空间上调用 N 次析构函数,清理每个对象的资源;operator delete[](p)释放内存空间。delete[] p → N次析构函数 → operator delete[](p) 下面我们以自定义类型A为例,分析new和delete的执行过程:
// new A(10)的执行过程:
1. 调用operator new(sizeof(A))申请内存
2. 调用A的构造函数A(10)初始化对象
// delete p的执行过程:
1. 调用p指向对象的析构函数~A()
2. 调用operator delete(p)释放内存
// new A[3]的执行过程:
1. 调用operator new[](3 * sizeof(A))申请内存
2. 依次调用3次A的构造函数初始化每个对象
// delete[] p的执行过程:
1. 依次调用3次A的析构函数清理每个对象
2. 调用operator delete[](p)释放内存定位 new 表达式允许在已分配的内存空间上调用构造函数初始化对象,主要用于内存池等场景。
// 在指定地址上构造对象,无参数
new (place_address) type;
// 在指定地址上构造对象,带参数
new (place_address) type(initializer-list); 其中,place_address是一个指向已分配内存的指针,initializer-list是构造函数的参数列表。
#include <iostream>
using namespace std;
class A {
public:
A(int a = 0) : _a(a) {
cout << "A() 构造函数:" << this << ", _a = " << _a << endl;
}
~A() {
cout << "~A() 析构函数:" << this << endl;
}
private:
int _a;
};
int main() {
// 1. 分配一块与A对象大小相同的内存(未初始化)
A* p = (A*)malloc(sizeof(A));
if (p == nullptr) {
perror("malloc failed");
return 1;
}
// 2. 使用定位new在已分配的内存上构造A对象
new(p) A(10); // 调用A的构造函数初始化p指向的内存
// 3. 使用对象
// ...
// 4. 手动调用析构函数(定位new不会自动调用析构函数)
p->~A();
// 5. 释放内存
free(p);
p = nullptr;
return 0;
}运行结果如下:
A() 构造函数:0x55f8d7a7a280, _a = 10
~A() 析构函数:0x55f8d7a7a280定位 new 主要用于内存池技术:
这种方式可以减少内存分配的开销,提高程序性能,尤其适用于频繁创建和销毁对象的场景。
p->~A());free(p))。 malloc/free和new/delete都是用于动态内存管理的工具,它们的主要区别如下:
特性 | malloc/free | new/delete |
|---|---|---|
性质 | 函数 | 操作符 |
初始化 | 不初始化内存 | 可以初始化(如new int(10)) |
内存大小 | 需要手动计算字节数 | 只需指定类型,自动计算大小 |
返回值 | 返回void*,需要强转 | 返回对应类型的指针,无需强转 |
失败处理 | 返回NULL,需手动判空 | 抛出bad_alloc异常 |
自定义类型 | 只分配 / 释放内存,不调用构造 / 析构函数 | 分配内存后调用构造函数,释放内存前调用析构函数 |
数组处理 | 需要手动计算总大小 | 使用new[]和delete[],自动处理数组 |
new/delete会自动调用构造函数和析构函数,而malloc/free不会。对于包含动态资源的类,这一点是至关重要的,它确保资源能够正确初始化和释放。
new不需要手动计算内存大小和强转类型,使用起来比malloc更简洁。
malloc通过返回NULL表示失败,需要手动检查;new通过抛出异常表示失败,需要使用try-catch处理。
operator new和operator delete可以被重载,允许自定义内存分配策略;而malloc和free不能被重载。
内存管理是 C/C++ 编程中的核心技能,本文从内存分布、C 语言内存管理、C++ 内存管理、底层实现原理等方面进行了全面解析。掌握内存管理不仅能帮助我们写出更高效、更稳定的代码,也是深入理解 C/C++ 语言特性的关键。下期博客我将为大家介绍C++模版的相关内容,请大家多多关注!