【C++】内存管理,深入解析new、delete
【C++】内存管理
文章目录
- 【C++】内存管理
- 一、C/C++内存分布
- 二、C语言中动态内存管理方式:
- 关键区别总结
- 三、C++内存管理方式
- 3.1new和delete操作内置类型:
- 3.2 new和delete操作自定义类型
- 3.2 new申请空间失败
- 四、new 和 delete 的实现原理
- 4.1 operator new 与 operator delete函数
- 4.2 new 和 delete 的实现原理
- 4.2.1 内置类型
- 4.2.2 自定义类型
- 4.3 不匹配使用的后果
- 4.3.1 内置类型
- 4.3.2 自定义类型
- 4.3.3 数组
- 4.3.4 总结
- 五、内存泄漏
- 5.1 内存泄漏的定义
- 5.2 C++ 的内存泄漏问题
- 5.3 对比其他语言
- 5.4 内存泄漏的常见场景对比
- 5.5 如何避免内存泄漏
- C++ 的解决方案
- 其他语言的解决方案
- 5.6 总结
- 六、定位new表达式(placement-new)
- 七、总结:malloc/free和new/delete的区别
一、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在哪里?C
staticGlobalVar在哪里?C
staticVar在哪里?C
localVar在哪里?A
num1 在哪里?A
char2在哪里?A
*char2在哪里?A
pChar3在哪里?A
*pChar3在哪里?D
ptr1在哪里?A
*ptr1在哪里?B
- 栈又叫堆栈–非静态局部变量/函数参数/返回值等等,栈是向下增长的。
- 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享共享内存,做进程间通信。(Linux课程如果没学到这块,现在只需要了解一下)
- 堆用于程序运行时动态内存分配,堆是可以上增长的。
- 数据段–存储全局数据和静态数据
二、C语言中动态内存管理方式:
在C语言中,我们会用到malloc/calloc/realloc/free
这些函数来动态管理内存
简单做道题,回顾一下malloc、calloc、realloc的特性:
void Test ()
{int* p1 = (int*)malloc(sizeof(int));int* p2 = (int*)calloc(4, sizeof (int));int* p3 = (int*)realloc(p2, sizeof(int)*10);// 这里需要free(p2)吗?free(p1 );free(p3 );
}
ans:
如果是原地扩容,那p2和p3指向的就是同一块空间,p2、p3释放其中一个即可。若是异地扩容,返回p3指向的新空间之前就会自动把原先p2的空间释放掉,所以这里两种情况,我们只释放p3就可以了
面试题:
-
malloc/calloc/realloc
的区别?在 C 语言中,
malloc
、calloc
和realloc
均用于动态内存分配,但功能不同:malloc
分配指定字节的未初始化内存,适用于通用场景;calloc
按元素数量和大小分配内存并初始化为全零,适合需要默认清零的结构或数组;realloc
用于调整已分配内存块的大小(扩展或缩小),可能复制数据到新地址。区别在于malloc
不初始化且参数为总字节数,calloc
自动初始化但效率略低,而realloc
可动态伸缩内存,需注意其可能返回新指针,且未扩展区域内容不确定。三者均需配合free
释放内存,避免泄漏。
关键区别总结
特性 | malloc | calloc | realloc |
---|---|---|---|
初始化 | 未初始化 | 初始化为零 | 保留原数据(新区域未初始化) |
参数形式 | 总字节数 | 元素个数 × 元素大小 | 原指针 + 新字节数 |
适用场景 | 通用动态分配 | 需要零初始化的结构/数组 | 动态调整内存大小 |
性能开销 | 低(无初始化) | 较高(需清零) | 可能涉及内存复制 |
内存连续性 | 保证连续 | 保证连续 | 可能改变地址(需重新赋值指针) |
//函数原型:
void* malloc (size_t size);//参数:size:内存块的大小(以字节为单位)
//返回值
//成功后,指向函数分配的内存块的指针。
//此指针的类型始终为 ,可以将其转换为所需的数据指针类型,以便可取消引用。
//如果函数无法分配请求的内存块,则返回 null 指针。void*void* calloc (size_t num, size_t size);//参数:num:需要分配元素的个数,size:每个元素的大小
//返回值
//成功后,指向函数分配的内存块的指针。
//此指针的类型始终为 ,可以将其转换为所需的数据指针类型,以便可取消引用。
//如果函数无法分配请求的内存块,则返回 null 指针。void*void* realloc (void* ptr, size_t size);//参数ptr:指向先前使用或分配的内存块的指针。或者这可以是 null 指针,在这种情况下,将分配一块新的空间(就像被调用一样)。//返回值:
//指向重新分配的内存块的指针,该块可能与 相同或新位置。
//此指针的类型为 ,可以将其转换为所需的数据指针类型,以便可取消引用。ptrvoid*
三、C++内存管理方式
C语言内存管理方式在C++中可以继续使用,但是C++是面向对象的语言,有些地方就无能为力,而且使用起来比较麻烦,特别是自定义类型的初始化和销毁。因此C++又提出了自己的内存管理方式:通过new
和delete
操作符进行动态内存管理。
3.1new和delete操作内置类型:
void Test()
{// 动态申请一个int类型的空间int* ptr4 = new int;// 动态申请一个int类型的空间并初始化为10int* ptr5 = new int(10);// 动态申请10个int类型的空间int* ptr6 = new int[3];//释放空间delete ptr4;delete ptr5;delete[] ptr6;
}
注意:申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用
new[]和delete[],注意:匹配起来使用。
实现原理:
如果申请的是内置类型的空间,new和malloc,delete和free基本类似,不同的地方是:new/delete申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申请空间失败时会抛异常,malloc会返回NULL。
3.2 new和delete操作自定义类型
#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)" << this << endl;}A(const A& ra): _a1(ra._a1), _a2(ra._a2){cout << "A(const A& ra)" << endl;}~A(){cout << "~A():" << this << endl;}
private:int _a1;int _a2;
};
int main()
{
// new/delete 和 malloc/free最大区别是
// new/delete 对于【自定义类型】除了开空间还会调用构造函数和析构函数A* p1 = (A*)malloc(sizeof(A));A* p2 = new A(1, 2);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
不会。 对于内置类型来说两者没有什么区别
换句话可以说,new/delete
是malloc/free
的升级版,new
和delete
其实是为了处理自定义类型而生的!!
3.2 new申请空间失败
失败的原因大概率为虚拟内存空间不足:
在申请失败时,会抛异常
64位
四、new 和 delete 的实现原理
4.1 operator new 与 operator delete函数
new
和delete
是用户进行动态内存申请和释放的操作符,operator new
和operator delete
是系统提供的全局函数,new
在底层调用operator new
全局函数来申请空间,delete
在底层通过operator delete全局函数来释放空间。
以下为operator new、 operator delete
的源码
/*
operator new:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间
失败,尝试执行空 间不足应对措施,如果改应对措施用户设置了,则继续申请,否
则抛异常。
*/
void* __CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{// try to allocate size bytesvoid* 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_FINALLYreturn;
}
/*
free的实现
*/
#define free(p) _free_dbg(p, _NORMAL_BLOCK)
总的来说就是:operator new 实际也是通过malloc来申请空间,如果malloc申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异常。operator delete 最终是通过free来释放空间的。
4.2 new 和 delete 的实现原理
4.2.1 内置类型
如果申请的是内置类型的空间,new
和malloc
,delete
和free
基本类似,不同的地方是:new / delete
申请和释放的是单个元素的空间,new[]/delete[]
申请的是连续空间,而且new
在申请空间失败时会抛异常,malloc
会返回NULL
。
4.2.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来释放空间
4.3 不匹配使用的后果
4.3.1 内置类型
int main()
{int* p1 = new int[10];free(p1); //正常释放return 0;
}
如果是内置类型,构造函数和析构函数都对内置类型不处理,所以用delete和free没什么区别
4.3.2 自定义类型
-
不需要资源清理的自定义类型
class A { private:int _a1 = 0; }; int main() {A* p2 = new A;free(p2);//只释放了空间,没有调析构函数return 0; }
自定义类型,构造函数和析构函数都会处理,但如果我们用free,就少调了一次析构函数,但是该类的析构函数并不需要清理资源,所不调用也是无所谓的。
-
需要资源清理的自定义类型
class Stack { public:Stack(){cout << "Stack()" << endl;_a = new int[4];_top = 0;_capacity = 4;}~Stack(){cout << "~Stack()" << endl;delete[] _a;_top = _capacity = 0;}private:int _top;int _capacity;int* _a; }int main() {Stack* st = new Stack;//delete st;free(st);//没有调析构函数return 0; }
这里的Stack类型是有向堆另外申请空间的,当我们使用free来释放空间时,free只会将st所指向的空间释放,而成员变量_a所指向的空间就会泄漏
但是这个地方编译器并没有报错,这说明编译器对于内存泄露不是很敏感,我们作为程序员要尽量自己去避免这种情况!!
4.3.3 数组
我们运行一下下面的代码:
class A
{
public:A(char a1 = 0)//默认构造函数: _a1(a1){cout << "A(char a1 = 0)" << this << endl;}~A()//析构函数{cout << "~A():" << this << endl;}
private:char _a1;
};class B
{
public:B(char b1 = 2): _b1(b1){cout << "B(char b1 = 2)" << this << endl;}
private:char _b1;
};int main()
{//int* p1 = new int[10];A* p1 = new A[5];B* p2 = new B[5];//没有匹配使用//free(p1);//直接崩溃//delete p1;//调用了一次析构,然后崩溃//匹配使用delete[] p1;//正常运行//没有匹配使用//free(p2);//正常运行//delete p2;//正常运行//匹配使用delete[] p2;//正常运行return 0;
}
通过上面的代码,我们发现当我们用new开了一个元素个数为10的A类型数组,开的时候没问题,但当程序走到free的时候却崩溃了?我们知道这是因为没有匹配使用new/delete,但本质上是为什么呢?
问题主要在于new
的机制:
在new数组时,编译器会在开数组前,会在数组前面多开四个字节的空间来记录开辟元素的个数,这个记录主要是当我们在用delete[]
时[]
中并没有指明有多少个元素,也就不知道要析构多少次。所以就是给后面delete[]
用来调用析构函数的
我们分别看一下A、B类型的空间创建和销毁的过程
-
有析构函数的A类型的数组:
对比上面的Stack类型,虽然Stack类型内存泄漏,但他却没有直接程序崩溃。而这里就直接崩溃了。检查机制是由编译器决定的,所以在不同的编译器下,对于空间泄漏的检查都会有所差别,所以我们为了减少这种偶然性的问题,我们就必须匹配使用new/delete,有空间资源申请的自定义类型,就必须要写析构函数,从而更加好的管理内存。
-
没有析构函数的B类型的数组:
因为没有析构函数,在销毁空间时也不会调用,所以就不许要多开4个字节
4.3.4 总结
- 对于内存泄漏问题,不同编译器的检查机制是不一样的。
- 不匹配使用new/delete的风险是不可控制的,我们必须要做到匹配使用
- 对于有空间申请,需要用到资源清理的自定义类型,必须要写好析构函数,否则会导致内存泄漏。
五、内存泄漏
C++ 的内存泄漏问题与其他语言(如 Java、Python、C#、Rust 等)有显著差异,主要源于其 手动内存管理 的特性。
5.1 内存泄漏的定义
内存泄漏(Memory Leak):程序在动态分配内存后,未能正确释放已不再使用的内存,导致系统可用内存逐渐减少,最终可能引发性能下降或崩溃。
5.2 C++ 的内存泄漏问题
原因
- 手动管理内存:C++ 中需显式调用
new
/malloc
分配内存,delete
/free
释放内存。 - 未释放的场景:
- 忘记调用
delete
。 - 异常导致未执行释放代码。
- 指针所有权不清晰(如多个指针指向同一内存,重复释放或未释放)。
- 容器或对象内部未正确释放资源。
- 忘记调用
示例
void leak() {int* ptr = new int[100]; // 分配内存// ...使用 ptr...// 不匹配使用delete,或者忘记使用
}
后果
- 长期运行的程序(如服务器、嵌入式系统)会逐渐耗尽内存。
- 调试困难:泄漏点可能隐蔽,需借助工具(如 Valgrind、AddressSanitizer)检测。
5.3 对比其他语言
(1) 手动管理内存的语言(如 C)
- 相似性:与 C++ 类似,需手动管理
malloc
/free
。 - 差异:C++ 的构造函数/析构函数、异常机制可能增加泄漏风险(如构造函数抛出异常导致未执行析构函数)。
(2) 半自动管理语言(如 Objective-C、Rust)
-
Objective-C(ARC):
- 自动引用计数(ARC):编译器自动插入
retain
/release
代码,减少泄漏。 - 循环引用:仍需开发者处理(通过
weak
引用)。
- 自动引用计数(ARC):编译器自动插入
-
Rust:
- 所有权系统:编译器静态检查内存生命周期,确保无泄漏(除非显式使用
unsafe
)。 - 无垃圾回收:通过所有权、借用规则实现内存安全。
- 所有权系统:编译器静态检查内存生命周期,确保无泄漏(除非显式使用
(3) 自动垃圾回收语言(如 Java、Python、C#、Go)
- 垃圾回收(GC):内存由运行时自动回收,开发者无需手动释放。
- 优点:几乎无需担心内存泄漏。
- 缺点:
- 不可控性:GC 触发时机不确定,可能引发短暂停顿。
- 循环引用:某些语言(如 Python)的 GC 能处理循环引用,但需依赖算法(如分代回收)。
- 非内存资源泄漏:如文件句柄、数据库连接仍需手动释放(需
try-with-resources
或using
语句)。
(4) 智能指针语言(现代 C++)
-
C++11 引入的智能指针:
std::unique_ptr
:独占所有权,自动释放内存。std::shared_ptr
:共享所有权,引用计数为 0 时释放。std::weak_ptr
:解决shared_ptr
的循环引用问题。
-
示例:
void no_leak() {auto ptr = std::make_unique<int[]>(100); // 自动释放 }
-
优点:大幅减少泄漏风险,但需正确使用(如避免循环引用)。
5.4 内存泄漏的常见场景对比
语言 | 泄漏风险 | 主要原因 | 典型场景 |
---|---|---|---|
C++ | 高 | 手动管理、异常、复杂对象生命周期 | 未配对的 new /delete |
C | 高 | 类似 C++,但无异常和析构函数 | malloc 后忘记 free |
Java | 低 | 循环引用或未关闭资源 | 静态集合持有对象、未关闭流 |
Python | 低 | 循环引用(需 GC 支持) | 复杂对象引用关系 |
Rust | 极低 | 所有权系统强制安全 | 需显式使用 unsafe 代码 |
C# | 低 | 非托管资源未释放 | 未调用 Dispose() 的文件操作 |
5.5 如何避免内存泄漏
C++ 的解决方案
- 优先使用智能指针:替代原始指针。
- RAII 原则:资源获取即初始化(如通过构造函数分配,析构函数释放)。
- 工具检测:
- Valgrind:检测内存泄漏和越界访问。
- AddressSanitizer(ASan):实时检测内存错误。
- 避免裸指针:仅在必要时使用(如与 C 库交互)。
其他语言的解决方案
- Java/Python:依赖 GC,但需注意非内存资源(如文件、网络连接)的手动释放。
- Rust:依赖编译器检查,几乎无需手动管理。
- C#:使用
using
语句或IDisposable
接口释放资源。
5.6 总结
- C++ 的内存泄漏风险最高:源于手动管理和复杂生命周期。
- 其他语言通过自动管理降低风险:
- GC 语言:以运行时开销为代价,简化内存管理。
- Rust:通过编译时检查实现零成本安全性。
- 现代 C++ 的最佳实践:智能指针 + RAII + 工具检测,可接近自动管理语言的安全性。
通过理解不同语言的内存管理机制,可以更好地规避泄漏问题,选择适合场景的工具和策略。
六、定位new表达式(placement-new)
定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象
使用格式:
//不带参
new(place_address)type;
//带参
new(place_address) type(initializer-list);
// | | |
// 地址 类型 类型的初始化列表
#include <iostream>
using namespace std;class A
{
public:A(int a = 0): _a(a){cout << "A():" << this << endl;}~A(){cout << "~A():" << this << endl;}void Printt(){cout << _a << endl;}
private:int _a;
};
// 定位new/replacement new
int main()
{// p1现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行A* p1 = (A*)malloc(sizeof(A));//只申请了空间,并没有调用构造函数,没有构造出对象cout << "p1:" << p1 << endl;new(p1)A; // 注意:如果A类的构造函数不是默认构造函数时,就必须要传参。但这里是默认构造,所以可以不用传参。p1->Printt();//资源清理p1->~A();free(p1);//delete p1;//因为定位出了对象,所以也可以通过delete释放空间,当然最好还是要匹配使用cout << endl;A* p2 = (A*)operator new(sizeof(A));//只申请了空间,并没有调用构造函数,没有构造出对象cout << "p2:" << p2 << endl;new(p2)A(10);//带参数构造出A对象p2->Printt();//资源清理p2->~A();operator delete(p2);//delete p2;return 0;
}
new就可以完成初始化这个工作,那为什么我们还要用malloc、再用定位new初始化,这不是多此一举吗??
确实是这样的!!!定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化。
在有些场景下,我们需要不断地new
一块新区域,这个过程是和操作系统上的堆完成交互的,如果我们不断地交互,其实是很麻烦的,为了优化性能,有了内存池这个概念,也就是我们先在堆上一次性malloc出一大块的区域,之后如果是处理小数据,我就不需要一直跟操作系统交互,只需要在我们的内存池里面拿空间就可以了,再用定位new去初始化!!这样减少了对象之间的交互,提高了效率。
七、总结:malloc/free和new/delete的区别
malloc/free
和new/delete
的共同点是:都是从堆上申请空间,并且需要用户手动释放。
不同的地方是
malloc
和free
是函数,new
和delete
是操作符malloc
申请的空间不会初始化,new
可以初始化malloc
申请空间时,需要手动计算空间大小并传递,new
只需在其后跟上空间的类型即可,如果是多个对象,[]
中指定对象个数即可- .
malloc
的返回值为void*
, 在使用时必须强转,new
不需要,因为new
后跟的是空间的类型 malloc
申请空间失败时,返回的是NULL
,因此使用时必须判空,new
不需要,但是new
需要捕获异常
式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化。