首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >C++ 内存管理:深入了解new、delete与内存管理方式

C++ 内存管理:深入了解new、delete与内存管理方式

作者头像
君辣堡
发布2025-12-20 09:08:46
发布2025-12-20 09:08:46
320
举报

在进入这篇的学习之前,我们做一做以下这些题目( 涉及C语言的内存管理知识):

1.内存管理经典题目

代码语言:javascript
复制
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.代码段(常量区)

1.globalVar 在哪里? ____ 2. staticGlobalVar在哪里?____ 3.staticVar在哪里?____

4.localVar 在哪里? ____ 5.num1 在哪里?____ 6.char2在哪里?____

7.* char2 在哪里? ___ 8.pChar3在哪里?____ 9. * pChar3 在哪里? ____

10.ptr1在哪里?____ 11. * ptr1 在哪里? ___

下面我开始解答:

1.

globalVar全局变量,所以是存储在 全局存储区/静态存储区/数据段,选C

2.

static GlobalVar全局变量,所有存储在全局存储区/静态存储区/数据段,选C

3.

statiVar静态的局部变量,所以存储在静态存储区/全局存储区/数据段,选C

4.

localVar是局部变量,所以存储在栈区选A

5.

num1是整型数组,是局部变量,存储在区。选A

6.

char2是字符串数组,其 所在地址 接收了字符串内容的拷贝 ,是局部变量,存储在栈区,选A

7.

*char2是字符串数组的解引用获取的是数组内容,而内容是字符串内容的拷贝而非字符串本

,所以数据存储在 栈区,选A

8.

pChar3是字符指针, 字符指针本身是局部变量存储在栈区,但他指向字符串常量,并且const修饰导致指针指向不可修改,所有等效于指向的字符串本身 即存储在常量区/代码段,选D

9.

*pChar3是对字符指针的解引用,获取的是指针指向的首地址,也就是字符串常量的首地址,

存储在 常量区/代段,选D

10.

ptr1是整型指针,指向了malloc在堆区开辟的一片空间,ptr本身属于局部变量,所以存储栈区,选A

11.*ptr1是整型指针的解引用,获取的是malloc开辟在堆区的那片空间,所有存储在堆区,选B


看完这些题,你是否找回了对C语言内存分配的记忆?那我们来认识一下C/C++中程序在内存区的划分:

2.C/C++中程序在内存区的划分

如图,这是上面一题在执行程序时各自在内存区的划分,顺便说明一下:

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

2. 内存映射段 是高效的 I/O 映射方式,用于装载一个共享的动态内存库。用户可使用系统接口

创建共享共享内存,做进程间通信。 ( 现在只需要了解一下,后面Linex会讲解)

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

4. 数据段/静态区/全局区 -- 存储全局数据和静态数据

5. 代码段/常量区 -- 可执行的代码/ 只读常量

上面的说明中提及了 栈是向下增长的,我用代码举例:

如图,地址向下递减,这就是栈向下增长的逻辑,向低地址的地方开辟空间给后来的变量或者函数

堆也同理,大方向是向高地址的地方开辟空间给后来的变量或者函数但是不总是这样堆区比较复杂,这点我们以后再理解。


C语言中动态内存管理方式

在堆区动态开辟内存和释放内存,我们之前(C语言)使用这几个函数:malloc,calloc,realloc,free

那这几个函数又有什么区别呢?

malloc(size_t size):

在堆区动态开辟一块大小为size字节的连续空间,并返回指向该内存的void*类型指针,不初始化(可能残留之前的数据,需要手动初始化,如使用memset初始化)

calloc(size_t nmemb, size_t size):

在堆区动态开辟nmemb个大小为size的空间,并返回指向该内存的void*类型指针,会将其初始化为0空间总大小为 nmemb*size

realloc(void* ptr, size_t new_size):

对一块已分配的内存进行扩容然后返回扩容后的首地址。这又分为异地扩容和原地扩容

异地扩容:若原内存块没有足够大的连续空间,会在堆区重新分配一块空间足够的地方,将ptr的原数据复制过去,然后释放ptr的原内存块,返回新地址(可能导致指针失效,需注意)

原地扩容:原内存块后有足够大的连续空间,则会直接在该内存块后面直接扩展,然后返回ptr

free(void* ptr):

将动态分配的内存(堆区)归还给操作系统,但记住其本质:只负责归还,不负责清除数据。归还的内存,系统可再次分配使用。


3.C++中内存管理方式

C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,因

此C++又提出了自己的内存管理方式:通过 new和delete操作符 进行动态内存管理


3.1new/delete操作内置类型
代码语言:javascript
复制
void Test()
{
  // 动态申请一个int类型的空间
  int* ptr4 = new int;
  
  // 动态申请一个int类型的空间并初始化为10
  int* ptr5 = new int(10);
  
  // 动态申请10个int类型的连续空间
  int* ptr6 = new int[10];  //实际上是10个int元素的连续数组
  delete ptr4;
  delete ptr5;
  delete[] ptr6;   //申请用了 [],那销毁也要用[],不用会报错
}

还可以给申请的数组初始化:int* ptr6 = new int[10]{1,2,3}; 剩下未初始化的默认为0

销毁就还是一样:delete[ ] ptr6;

注意:申请和释放单个元素的空间,使用new和delete操作符, 申请和释放连续的空间,使用

new[]和delete[],匹配起来使用。

以上是对new delete对内置类型的代码解析。


3.2new和delete操作自定义类型

new/delete 和 malloc/free最大区别是 new/delete对于【自定义类型】除了开空间还会调用构造函数和析构函数

如图,使用new自动调用了默认构造函数,使用delete自动调用了析构函数。

对于内置类型而言,因为不需要调用析构和构造,所以这两种方式显得没区别。但明显用new更方便,代码可读性更高。

这图与上面结论一样,new更便捷,可读性更高。


3.3malloc和new对错误的处理:if判空 和 抛异常捕获

既然malloc和new都是动态开辟空间的,那总会开辟空间失败,虽然概率很小。那我们看看

malloc和new是怎么对错误进行处理的?

malloc:malloc开辟空间后,会返回该内存的首地址给一个指针接收,若内存开辟失败,则malloc会返回NULL给指针,此时只需要用if判断语句判断该指针是否为NULL即可

new:new分配空间失败后,会抛异常(std::bad_alloc),不会返回NULL。此时需要用

try-catch 块 捕获异常 ( try-catch 和 抛异常 涉及的内容较为复杂,以后再讲)。抛异常必须被捕获,如果没被捕获则程序会崩溃(运行到错误的地方,导致程序终止),除非你明确不可能抛异常,不然必须捕获,捕获的代码演示:

代码语言:javascript
复制
try
{
	// 可能抛 std::bad_alloc 的代码,例如:
    int* p = new int[1000000000]; 
    delete[] p;
}
catch (const exception& e)
// 精准捕获 new 分配失败的异常类型
{
	cout << e.what() << endl;
}

其中try括号中的那块内容可以是函数,由函数间接捕获(执行进入函数发现抛异常,然后跳转到main函数中的捕获),也可以直接塞可运行代码(如图中代码),直接对代码捕获。

函数内部抛异常但未在函数内捕获,异常会自动上传到外层作用域(平行作用域也不影响异常传播,比如上面的例子:函数与main函数),直到被某个try-catch块 捕获。一直未被捕获则导致程序会终止。

没学过的参数涉及更多知识,目前只需了解


3.4operator new与operator delete函数(重点)

new和delete是用户进行动态内存申请和释放的操作符operator new 和operator delete

系统提供的全局函数new在底层调用operator new全局函数来申请空间,delete在底层通过

operator delete全局函数来释放空间。

operator new 和 operator delete 不算是 对new和delete 的重载,算是两个 特殊的,系统提供的全局函数用作new和delete的底层调用

以下是这两个函数在库中的部分源码:

operator new:

代码语言:javascript
复制
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就是封装了malloc函数。图中若malloc申请空间失败,则会尝试释放内存,然后抛异常。 为什么不直接使用malloc? 因为malloc申请失败返回NULL,而C++申请失败的特性是要返回抛异常,所以进行封装。

operator delete:

代码语言:javascript
复制
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)

这里的代码就比较晦涩难懂,但你会发现其中调用了_free_dbg,再看最底下:_free_dbg其实是free的宏替换。

这说明,operator delete实际上是free的封装。 (因为operator new,搞个配套operator delete)

总结,operator new 和operator delete 其实是对malloc 和 free 的封装,并且我们可以调用。用法基本相同,唯一的区别就是operator new 失败会返回抛异常。

这两个函数我们可以调用,但一般都不这么用,因为上面说了,这两个函数是new和delete的底层调用:

new的反汇编:

如图,这是调用new的语句的反汇编代码,大部分我们都看不懂,但只需要看懂红框两句call是调用,mov是移动。

所以图中调用(call)了operator new函数 和 A类的构造函数。 这说明new的底层确实是调用了operator new,new的异常也是从operator new中抛出。

delete的反汇编:

图中第一句红框的代码是A类产生的辅助函数,现在不需要了解那么细,只需要知道它会展开两个核心步骤,也就是下面两个红框:

1.调用析构函数 2.调用operator delete函数。

是先使用析构,然后使用operator delete 原因: 因为析构要析构的是指向的资源,若先delete删除了指针,那就找不到指向的资源了(指向丢失,会非法访问、删除资源,导致野指针),所以需要先用析构清理掉指向的资源,然后再删除指针。


4.new和delete的实现原理

上面学习了operator delete 和 operator new ,现在我们更全面地总结一下:

内置类型

如果申请的是内置类型的空间, new和malloc,delete和free 基本类似,不同的地方是

new/delete 申请和释放的是单个元素的空间, new[] 和 delete[] 申请的是连续空间,而且 new 在申

请空间失败时会抛异常, malloc 会返回 NULL 。

2 自定义类型

new 的原理

1. 调用 operator new 函数申请空间

2. 在申请的空间上执行构造函数,完成对象的构造

delete 的原理

1. 在空间上执行析构函数,完成对象中资源的清理工作

2. 调用 operator 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来释放空间


4.1new和delete的扩展知识

代码语言:javascript
复制
//A类的私有成员仅有int _a

A* p1 = new A[10];

delete[] p1; 

如图,根据注释可以直到,一个 A对象占4个字节,new动态开辟了10个A类的连续空间,然后返回首地址给p1,所以可以知道: p1是40个字节 但是,系统会在数组头部额外分配4字节,存放数组元素数量,(开辟的单位大小空间个数,也就是10)

如图,原本预计是40字节,但是系统多加了4个字节的空间,用来存放数字,这个数字就是表示开多少空间的,实际上是分配了44字节。

别小看这个数字,系统区分 delete[ ] p1 和delete p1 看的就是这个:delete[ ] p1,加了方括号,系统会自动读取头部的元素数量(10),并对每个对象调用析构函数(10次)。 若是delete p1,则系统会默认只开了一个单位大小的空间,系统会默认只调用一次析构函数,导致剩余 9 个对象的资源未被清理,引发内存泄漏和野指针风险。

所以new [ ] 要配对 delete [ ]


5. 定位new表达式(placement-new) (了解)

定位 new 表达式是在 已分配的原始内存空间中调用构造函数初始化一个对象

使用格式:

new (place_address) type 或者 new (place_address) type(initializer-list)

place_address必须是一个指针,initializer-list是类型的初始化列表

使用场景:

定位 new 表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如

果是自定义类型的对象,需要使用 new 的定义表达式进行显示调构造函数进行初始化

小知识:内存池的目的是高效获取资源和管理资源。这个后面会学

代码语言:javascript
复制
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);

如图,p1现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行。malloc只是负责分配空间,不负责构造对象,没构造就没对象。(同理换成operator new也一样,因为只是malloc的封装,不会进行构造)

这时候就需要显式调用构造函数,然后普通方法都不能调用,这时候定位new的作用就显现出来:new(p1)A

因为A有默认构造函数,所以不需要给参数,若A无默认构造,则需根据函数参数给参数,比如:A(int a),此时 new(p1) A(1)

free不能清理资源,所以得显式调用析构函数去清理资源,析构和构造不同,析构可直接调用:p1->~A();


6.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在释放空间前会调用析构函数完成

空间中资源的清理释放

区别,总结于3个方面: 用法(1),核心特性(2-5),原理(6)。 死记硬背背不会的,理解知识更好。

C++内存管理就先到这了,这一篇干货满满,请大家喝水观看!有问题和错误欢迎指出,请各位多多点赞支持~

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-12-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.内存管理经典题目
  • 2.C/C++中程序在内存区的划分
  • C语言中动态内存管理方式
    • malloc(size_t size):
    • calloc(size_t nmemb, size_t size):
    • realloc(void* ptr, size_t new_size):
    • free(void* ptr):
  • 3.C++中内存管理方式
    • 3.1new/delete操作内置类型
    • 3.2new和delete操作自定义类型
    • 3.3malloc和new对错误的处理:if判空 和 抛异常捕获
    • 3.4operator new与operator delete函数(重点)
  • 4.new和delete的实现原理
    • 4.1new和delete的扩展知识
  • 5. 定位new表达式(placement-new) (了解)
  • 6.malloc/free和new/delete的区别
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档