C/C++ 内存管理:从入门到面试通关
📖 本文导读:C/C++ 内存管理是面试中的核心考点,本文从内存分布、动态内存管理函数,到 C++ 的
new/delete操作符及其底层原理,再到常见的内存泄漏问题,进行了全面系统的梳理。下面先通过目录一览全文结构。
📑 文章目录
- 📚 1. C/C++ 内存分布
- 🛠️ 2. C 语言中动态内存管理方式:malloc/calloc/realloc/free
- 🚀 3. C++ 内存管理方式
- ⚙️ 4. operator new 与 operator delete 函数(重点)
- 🧬 5. new 和 delete 的实现原理
- 📍 6. 定位 new 表达式 (placement-new)(了解)
- 📝 7. 常见面试题
- 🎯 总结
📚 1. C/C++ 内存分布
在开始学习 C/C++ 的内存管理之前,我们首先需要了解程序运行时,各种变量和数据在内存中是如何分布的。我们先来看一段代码和相关问题。
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在哪里? CstaticGlobalVar在哪里? CstaticVar在哪里? ClocalVar在哪里? Anum1在哪里? Achar2在哪里? A*char2在哪里? ApChar3在哪里? A*pChar3在哪里? Dptr1在哪里? A*ptr1在哪里? B
📝 填空题
sizeof(num1)= 40sizeof(char2)= 5strlen(char2)= 4sizeof(pChar3)= 4 (32位系统) / 8 (64位系统)strlen(pChar3)= 4sizeof(ptr1)= 4 (32位系统) / 8 (64位系统)
💡 sizeof 和 strlen 区别?
sizeof:是一个操作符,计算的是变量或类型所占内存的字节数,在编译时就已经确定。strlen:是一个库函数,专门用于计算以\0结尾的字符串的长度(不包含\0),在运行时才能确定。
🗺️ 内存区域划分说明
- 栈 (Stack):又叫堆栈,存储非静态局部变量、函数参数、返回值等。栈是向下增长的。
- 内存映射段 (Memory Mapping Segment):高效的 I/O 映射方式,用于装载共享的动态内存库。用户可使用系统接口创建共享内存,做进程间通信。
- 堆 (Heap):用于程序运行时动态内存分配。堆是向上增长的。
- 数据段 (Data Segment):存储全局变量和静态变量。
- 代码段 (Code Segment):存储可执行的代码和只读常量。
🛠️ 2. C 语言中动态内存管理方式:malloc/calloc/realloc/free
C 语言使用 malloc、calloc、realloc 和 free 这四个函数来进行动态内存管理。
void Test ()
{
// 1. malloc/calloc/realloc 的区别是什么?
int* p1 = (int*) malloc(sizeof(int));
free(p1);
int* p2 = (int*)calloc(4, sizeof (int));
int* p3 = (int*)realloc(p2, sizeof(int)*10);
// 这里需要 free(p2) 吗?
free(p3 );
}
🤔 面试题:malloc/calloc/realloc 的区别?
| 函数 | 功能 | 特点 |
|---|---|---|
malloc |
在堆上分配一块指定大小的内存 | 内存内容不初始化,是随机值 |
calloc |
在堆上分配一块内存,并初始化为 0 | 参数为元素个数和每个元素大小,自动计算总大小 |
realloc |
调整已分配内存块的大小 | 可以扩大或缩小,扩大时可能移动内存地址 |
注意:realloc 在扩大内存时,如果原空间后面没有足够空间,会重新找一块更大的内存,并将原数据拷贝过去,然后释放原空间。此时,p2 指针可能已经失效,所以不需要再 free(p2)。
🤔 面试题:malloc 的实现原理?
malloc 的实现依赖于操作系统的内存管理机制(如 glibc 中的 ptmalloc)。其核心原理是:
malloc会维护一个空闲内存链表。- 当用户请求内存时,它会遍历链表,找到一块足够大的空闲内存块。
- 如果找到,将其分割成两部分:一部分返回给用户,另一部分重新放回空闲链表。
- 如果找不到,它会向操作系统申请更多的堆内存(通过
brk()或mmap()系统调用),然后重复查找过程。
🚀 3. C++ 内存管理方式
C 语言的内存管理方式在 C++ 中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦。因此,C++ 又提出了自己的内存管理方式:通过 new 和 delete 操作符进行动态内存管理。
3.1 🎯 new/delete 操作内置类型
void Test()
{
// 动态申请一个 int 类型的空间
int* ptr4 = new int;
// 动态申请一个 int 类型的空间并初始化为 10
int* ptr5 = new int(10);
// 动态申请 10 个 int 类型的空间
int* ptr6 = new int[10];
delete ptr4;
delete ptr5;
delete[] ptr6;
}
注意:申请和释放单个元素的空间,使用 new 和 delete 操作符;申请和释放连续的空间,使用 new[] 和 delete[]。一定要匹配起来使用,否则可能导致内存泄漏或程序崩溃。
3.2 🎯 new 和 delete 操作自定义类型
new/delete 和 malloc/free 最大的区别在于:new/delete 对于自定义类型,除了开空间还会调用构造函数和析构函数。
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)); // C
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 不会。
⚙️ 4. operator new 与 operator delete 函数(重点)
new 和 delete 是用户进行动态内存申请和释放的操作符,而 operator new 和 operator delete 是系统提供的全局函数。new 在底层调用 operator new 全局函数来申请空间,delete 在底层通过 operator delete 全局函数来释放空间。
4.1 🔍 operator new 的实现原理
/*
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);
}
4.2 🔍 operator delete 的实现原理
/*
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 的原理
- 调用
operator new函数申请空间。 - 在申请的空间上执行构造函数,完成对象的构造。
🔴 delete 的原理
- 在空间上执行析构函数,完成对象中资源的清理工作。
- 调用
operator delete函数释放对象的空间。
🟢 new T[N] 的原理
- 调用
operator new[]函数,在operator new[]中实际调用operator new函数完成 N 个对象空间的申请。 - 在申请的空间上执行 N 次构造函数。
🔴 delete[] 的原理
- 在释放的对象空间上执行 N 次析构函数,完成 N 个对象中资源的清理。
- 调用
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 表达式进行显示调用构造函数进行初始化。
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. 常见面试题
7.1 🤔 malloc/free 和 new/delete 的区别
| 对比项 | malloc/free |
new/delete |
|---|---|---|
| 本质 | 函数 | 操作符 |
| 初始化 | 申请的空间不会初始化 | new 可以初始化 |
| 空间大小 | 需要手动计算空间大小并传递 | 只需跟上类型,[] 中指定对象个数即可 |
| 返回值 | 返回 void*,使用时必须强转 |
返回对应类型的指针,不需要强转 |
| 失败处理 | 返回 NULL,使用时必须判空 |
抛出 bad_alloc 异常,需要捕获异常 |
| 自定义类型 | 只会开辟空间,不会调用构造函数与析构函数 | 申请空间后会调用构造函数,释放前会调用析构函数 |
7.2 💧 内存泄漏
7.2.1 什么是内存泄漏,内存泄漏的危害
什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。
void MemoryLeaks()
{
// 1. 内存申请了忘记释放
int* p1 = (int*)malloc(sizeof(int));
int* p2 = new int;
// 2. 异常安全问题
int* p3 = new int[10];
Func(); // 这里 Func 函数抛异常导致 delete[] p3 未执行,p3 没被释放.
delete[] p3;
}
7.2.2 内存泄漏分类(了解)
C/C++ 程序中一般我们关心两种方面的内存泄漏:
- 堆内存泄漏 (Heap leak):堆内存指的是程序执行中依据须要分配通过
malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的free或者delete删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生 Heap Leak。 - 系统资源泄漏:指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
7.2.3 如何检测内存泄漏(了解)
在 VS 下,可以使用 Windows 操作系统提供的 _CrtDumpMemoryLeaks() 函数进行简单检测,该函数只报出了大概泄漏了多少个字节,没有其他更准确的位置信息。
int main()
{
int* p = new int[10];
// 将该函数放在 main 函数之后,每次程序退出的时候就会检测是否存在内存泄漏
_CrtDumpMemoryLeaks();
return 0;
}
////////////////////////////////////////////////////////
// 程序退出后,在输出窗口中可以检测到泄漏了多少字节,但是没有具体的位置
Detected memory leaks!
Dumping objects ->
{79} normal block at 0x00EC5FB8, 40 bytes long.
Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD
Object dump complete.
因此写代码时一定要小心,尤其是动态内存操作时,一定要记着释放。但有些情况下总是防不胜防,简单的可以采用上述方式快速定位下。如果工程比较大,内存泄漏位置比较多,不太好查时一般都是借助第三方内存泄漏检测工具处理的。
- 在 Linux 下内存泄漏检测:linux下几款内存泄漏检测工具
- 在 Windows 下使用第三方工具:VLD工具说明
- 其他工具:内存泄漏工具比较
7.2.4 如何避免内存泄漏
- 事前预防:工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。但是碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保证。
- 采用 RAII 思想或者智能指针来管理资源。
- 使用内部私有内存管理库:有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
- 事后查错:出问题了使用内存泄漏工具检测。不过很多工具都不够靠谱,或者收费昂贵。
总结一下:内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具。
🎯 总结
C/C++ 的内存管理是面试中的重中之重,也是编写高质量代码的基础。本文从内存分布、C 语言的内存管理函数,到 C++ 的 new/delete 操作符,再到其底层实现原理和常见的内存泄漏问题,进行了全面的梳理。
💪 经典面试题(含详细解答)
1. malloc 和 new 的区别是什么?
| 对比维度 | malloc |
new |
|---|---|---|
| 本质 | 函数(C 标准库) | 操作符(C++ 关键字) |
| 初始化 | 不初始化,内存内容为随机值 | 可以初始化(new int(10) 或 new int[10]()) |
| 空间大小 | 需手动计算并传递字节数(sizeof(int)) |
编译器根据类型自动计算,只需指定对象个数 |
| 返回值 | void*,使用时必须强转 |
返回对应类型的指针,无需强转 |
| 失败处理 | 返回 NULL,使用时必须判空 |
抛出 std::bad_alloc 异常,需捕获异常 |
| 自定义类型 | 只开辟空间,不调用构造函数/析构函数 | 开辟空间后调用构造函数,释放前调用析构函数 |
| 重载 | 不可重载 | 可以重载(全局或类内) |
| 底层实现 | 直接调用系统调用(brk/mmap) |
底层调用 operator new,而 operator new 内部调用 malloc |
核心区别一句话:new 是类型安全的操作符,会调用构造/析构函数,失败抛异常;malloc 是纯内存分配函数,不初始化,失败返回 NULL。
2. free 和 delete 的区别是什么?
| 对比维度 | free |
delete |
|---|---|---|
| 本质 | 函数 | 操作符 |
| 自定义类型 | 只释放内存,不调用析构函数 | 先调用析构函数清理资源,再释放内存 |
| 参数类型 | void*,需确保指针由 malloc 系列返回 |
对应类型的指针,需确保指针由 new 返回 |
| 数组释放 | 直接 free(ptr) 即可 |
必须用 delete[] 匹配 new[],否则行为未定义 |
| 底层实现 | 调用 _free_dbg 归还内存 |
先调用析构函数,再调用 operator delete(内部调用 free) |
关键注意:delete 会先调用析构函数释放对象内部资源(如关闭文件、释放堆内存),再释放对象本身的内存。如果对 new[] 分配的内存使用 delete(而非 delete[]),只会调用一次析构函数,导致内存泄漏或程序崩溃。
3. 什么是内存泄漏?如何避免?
什么是内存泄漏:程序在运行过程中动态分配了内存,但在使用完毕后未能释放,导致这部分内存永远无法被再次使用。内存泄漏不会导致内存物理消失,而是程序失去了对这块内存的控制权。
内存泄漏的危害:
- 程序可用内存逐渐减少,响应变慢
- 长期运行的服务(如 Web 服务器、后台进程)最终可能因内存耗尽而崩溃
- 系统资源(文件描述符、套接字等)泄漏会导致系统不稳定
如何避免内存泄漏:
| 策略 | 说明 |
|---|---|
| RAII(资源获取即初始化) | 将资源管理封装在对象中,利用对象的生命周期自动释放资源 |
| 智能指针 | 使用 std::unique_ptr、std::shared_ptr 管理动态内存,自动释放 |
| 规范编码 | 遵循"谁申请谁释放"原则,new/delete、new[]/delete[]、malloc/free 成对出现 |
| 异常安全 | 使用 RAII 避免异常导致资源未释放 |
| 内存池 | 统一管理内存分配与释放,便于追踪 |
| 检测工具 | 使用 Valgrind、AddressSanitizer、VLD 等工具定期检测 |
4. operator new 和 malloc 有什么关系?
关系总结:operator new 是 C++ 标准库提供的全局函数,其内部实现就是调用 malloc。
具体关系:
operator new封装了malloc:operator new内部调用malloc(size)来申请内存。- 失败处理不同:
malloc失败返回NULL;operator new在malloc失败后会尝试调用用户设置的new_handler,如果仍无法分配,则抛出std::bad_alloc异常。 - 可重载性:
operator new可以被用户重载(类内或全局),而malloc不可重载。 - 调用链:
new→operator new→malloc→ 系统调用(brk/mmap)。
源码示意:
void* operator new(size_t size) {
if (void* p = malloc(size))
return p;
// malloc 失败,尝试 new_handler
if (_callnewh(size))
return operator new(size); // 重试
throw std::bad_alloc(); // 抛异常
}
5. new[] 和 delete[] 是如何知道要调用多少次构造/析构函数的?
核心机制:编译器在 new[] 分配的内存块头部额外存储了元素个数。
具体流程:
new T[N] 的底层实现:
实际分配的内存布局:
[ 元素个数 N (4字节) ] [ T[0] ] [ T[1] ] ... [ T[N-1] ]
↑
返回给用户的指针
- 调用
operator new[]申请sizeof(T) * N + 4字节(多出的 4 字节存个数) - 在头部写入
N - 从
T[0]到T[N-1]依次调用 N 次构造函数 - 返回指向
T[0]的指针(跳过头部 4 字节)
delete[] p 的底层实现:
- 从
p向前偏移 4 字节,读取元素个数N - 从
p[N-1]到p[0]依次调用 N 次析构函数(逆序析构) - 调用
operator delete[]释放整块内存(含头部)
注意:对于内置类型(如 int、char),编译器会优化掉头部存储,因为内置类型不需要调用析构函数。这也是为什么对内置类型使用 delete 代替 delete[] 通常不会崩溃,但对自定义类型则会导致未定义行为。
6. 什么是定位 new 表达式?它的使用场景是什么?
定义:定位 new 表达式(placement new)是在已分配的原始内存上调用构造函数来初始化对象,而不是重新申请内存。
语法:
new (place_address) Type; // 调用默认构造函数
new (place_address) Type(args); // 调用带参构造函数
使用场景:
-
内存池(Memory Pool):从内存池中预分配一大块内存,需要创建对象时,在已分配的内存上调用定位
new初始化对象,避免频繁的系统调用。 -
自定义内存管理:在特定内存区域(如共享内存、DMA 缓冲区、特定地址的硬件寄存器映射区)构造对象。
-
容器实现:
std::vector等容器的底层实现中,使用定位new在已分配但未初始化的内存上构造元素。 -
对象池:游戏开发中常用的对象池模式,复用已分配的内存块,避免频繁的
new/delete开销。
示例:
// 内存池场景
void* buffer = operator new(sizeof(MyClass) * 100); // 预分配
MyClass* p = new (buffer) MyClass(42); // 在 buffer 上构造
p->~MyClass(); // 手动调用析构
operator delete(buffer); // 释放整块内存
7. 请描述一个对象从 new 到 delete 的完整生命周期。
以 A* p = new A(10); 和 delete p; 为例:
new 阶段(创建对象):
1. 调用 operator new(sizeof(A)) 申请内存
└─ 内部调用 malloc(sizeof(A))
└─ 系统调用 brk() 或 mmap() 从堆中分配
2. 在分配的内存上调用 A 的构造函数
└─ 初始化成员变量 _a = 10
└─ 执行构造函数体(如打印 "A(): 0x...")
3. 返回指向该对象的指针 p
对象使用阶段:
4. 通过 p 访问对象成员(p->_a、p->func() 等)
delete 阶段(销毁对象):
5. 调用 p 指向对象的析构函数
└─ 清理对象内部资源(如释放内部堆内存、关闭文件)
└─ 执行析构函数体(如打印 "~A(): 0x...")
6. 调用 operator delete(p) 释放内存
└─ 内部调用 free(p)
└─ 将内存归还给堆管理器
7. 指针 p 变为悬空指针(dangling pointer),应置为 nullptr
完整流程图:
8. 在 C++ 中,如何检测和定位内存泄漏?
常用检测方法:
| 方法 | 平台/工具 | 说明 |
|---|---|---|
_CrtDumpMemoryLeaks() |
Windows (MSVC) | 程序退出时输出泄漏信息,配合 _CrtSetBreakAlloc 可定位到具体分配点 |
| VLD(Visual Leak Detector) | Windows | 可视化显示泄漏的调用堆栈、文件名和行号 |
| Valgrind | Linux | 最常用的内存检测工具,valgrind --leak-check=full ./program |
| AddressSanitizer (ASan) | Linux/macOS/Windows | GCC/Clang 内置,编译时加 -fsanitize=address,运行时检测 |
| Dr. Memory | Windows/Linux | 开源内存调试工具 |
| 静态分析工具 | 跨平台 | PVS-Studio、Cppcheck、Clang-Tidy 等,在编译前发现潜在泄漏 |
VS 下使用 _CrtDumpMemoryLeaks 示例:
#define _CRTDBG_MAP_ALLOC // 让 malloc 被映射到调试版本
#include <stdlib.h>
#include <crtdbg.h>
int main() {
int* p = new int[10];
// 程序退出时检测泄漏
_CrtDumpMemoryLeaks();
return 0;
}
// 输出窗口显示:
// Detected memory leaks!
// Dumping objects ->
// {79} normal block at 0x00EC5FB8, 40 bytes long.
Valgrind 使用示例:
g++ -g -o program program.cpp # 编译时加 -g 保留调试信息
valgrind --leak-check=full --show-leak-kinds=all ./program
AddressSanitizer 使用示例:
g++ -fsanitize=address -g -o program program.cpp
./program # 运行时自动检测并报告泄漏
最佳实践:
- 开发阶段:使用 AddressSanitizer(性能开销小,检测全面)
- 测试阶段:使用 Valgrind(检测最全面,但运行慢)
- 发布前:使用静态分析工具做代码审查
- 日常编码:优先使用智能指针和 RAII,从源头避免泄漏
更多推荐
所有评论(0)