首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >C++内存管理:new和delete详细解析

C++内存管理:new和delete详细解析

原创
作者头像
李昂
发布2025-10-21 16:47:24
发布2025-10-21 16:47:24
2050
举报
文章被收录于专栏:C++C++

目录

一. C/C++内存分布

1.1 内存分布问题

1.2 内存区域划分

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

三. C++内存管理方式

3.1 new/delete操作内置类型

3.2 new和delete操作自定义类型

3.3 new出现错误时的处理

四. operator new与operator delete函数(重点)

4.1 operator new 和 operator delete 的源码分析

4.1.1 operator new 函数

4.1.2 operator delete 函数

4.2 new 和 delete 的底层机制

五. new和delete的实现原理

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

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

结语:


一. C/C++内存分布

在C语言中,我们已经学过了有关内存管理的知识,我们先看下面的一端代码和相关问题来回顾一下

1.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.代码段(常量区)

  • globalVar在哪里?__C__
  • staticGlobalVar在哪里?__C__
  • staticVar在哪里?__C__
  • localVar在哪里?__A__
  • num1 在哪里?__A__

解析: globalVar 为全局变量,存储在静态区;staticGlobalVar 为静态全局变量,存储在静态区;staticGlobalVar 我局部静态变量,存储在静态区;localVar 为局部变量,存储在栈区;num1是局部变量,同时也是数组名,数组名表示整个数组,也存储在栈区 注意:数组名在进行运算(加1,解引用等)的时候,数组名就代表数组首元素的地址

  • char2在哪里?__A__
  • * char2在哪里?_A__ // 因为这个数组是在栈上的,所以解引用得到的数组第一个元素也是在栈上的
  • pChar3在哪里?__A__
  • * pChar3在哪里?__D__ // 这个字符串是在常量区的,所以解引用得到的是这个这个字符串的第一个元素也在常量区
  • ptr1在哪里?__A__
  • * ptr1在哪里?__B__

解析: char2是局部变量,也是数组名,表示整个数组,存储在栈区;char2数组中保存的数据实际上是从常量区拷贝得来的,然后保存在了栈区,*char2得到的是数组的首元素,因为数组中的数据存储在栈区,所以*char2在栈区;pChar3是一个字符串指针,是局部变量,存储的是一个地址在栈区;pChar3中保存的是字符串首元素的地址,解引用的到的是字符串首个字符,由于字符串存储在常量区,所以*pChar3在常量区;ptr是局部指针变量,在栈区;*ptr1得到的是动态申请空间的数据在堆区。

可以通过图像来加深理解

char2 和 Pchar3 我们可以通过下面这个图示来更加清晰地理解一下

1.2 内存区域划分

  • 又叫堆栈--非静态局部变量/函数参数/返回值等等,栈是向下增长的。
  • 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接创建共享共享内存,做进程间通信。(先了解一下)
  • 用于程序运行时动态内存分配,堆是可以上增长的。
  • 数据段--存储全局数据和静态数据。
  • 代码段--可执行的代码/只读常量。

常量区的误区

代码语言:javascript
复制
// 常量区的数据是不能被修改的
int main()
{
    char char2[] = "abcd"; // 数组
	const char* pChar3 = "abcd"; // pChar存储的是常量字符串指针,指向在静态区的字符串的首元素的地址

	*char2 += 1;
	// (*(char*)pChar3) += 1; // err 存放在常量区,不能通过强转来修改

	// C++中const修饰的局部变量叫常变量,是存放在栈区的
	// 可以取到它的地址const int* ,然后强转成int*,再解引用,是能修改的
	const int i = 0;
	int j = 1;
	cout << &i << endl;
	cout << &j << endl;
	cout << pChar3 << endl; // char*类型无法直接打印出地址
	cout << (void*)pChar3 << endl;

	return 0;
}

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

我们先看下列代码,然后思考一下问题回顾一下C语言中的动态内存管理

代码语言:javascript
复制
void Test()
{
	// 1.malloc/calloc/realloc的区别是什么?
	int* p2 = (int*)calloc(4, sizeof(int));
	int* p3 = (int*)realloc(p2, sizeof(int) * 10);

	// 这里需要free(p2)吗?
	free(p3);
}

不需要。如果是原地扩容,不需要free(p2),因为p1和p2地址是一样的,不允许分段释放;如果是异地扩容也不需要释放p2,因为realloc会将原来的空间释放掉,然后再开辟一块新的空间,把原数据拷贝进去。

如果同一块空间释放两次会有什么危害? 一块空间释放后,可能有分配给别人使用,那么第二次释放时就有可能会将别人的空间给释放掉了

面试题

1、malloc/calloc/realloc的区别?

  • malloc:分配指定字节内存,不初始化,数据随机。
  • calloc:分配内存并初始化为 0,需指定元素个数和单个元素字节数。
  • realloc:调整已分配内存大小,可扩容或缩容,可能复用原内存或重新分配并复制数据,有两种情况,原地扩容或者异地扩容,这个在上面也提到过。

2、malloc的实现原理?

  • 基于brk(调整数据段地址)和 mmap(映射新内存区域)系统调用,结合内存池管理(按大小分类维护空闲块,分配时快速匹配,减少系统调用开销)

参考视频:glibc中malloc实现原理

三. C++内存管理方式

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

3.1 new/delete操作内置类型

代码语言:javascript
复制
int main()
{
	// 动态申请空间,但是不初始化
	int* p1 = new int;      //单个对象
	int* p2 = new int[10];  // 数组

	// 申请空间的同时初始化
	int* p3 = new int(1);
	int* p4 = new int[10] {1, 2, 3}; // 前三个为初始化的值,后几个默认为0

	delete p1;
	delete[] p2;
	delete p3;
	delete[] p4;

	return 0;
}

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

3.2 new和delete操作自定义类型

在申请自定义类型的空间时,new会调用构造函数,delete会调用析构函数,而malloc与 free不会

代码语言:javascript
复制
class A
{
public:
 A(int a = 0)
 : _a(a)
 {
 cout << "A():" << this << endl;
 }
 ~A()
 {
 cout << "~A():" << this << endl;
 }
private:
    int _a;
}

struct ListNode
{
	ListNode* _next;;
	int _val;

	ListNode(int val)
		:_next(nullptr)
		,_val(val)
	{}
}

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]{1, 2, 3}; // 创建多个对象,会调用多次构造;前三个用给的值来调用构造,其余默认用0来调用构造
	free(p5);
	delete[] p6; // 销毁时,调用多次析构

    // 创建链表
  	// 创建链表节点,将他们链接即可
	ListNode* n1 = new ListNode(1);
	ListNode* n2 = new ListNode(2);
	ListNode* n3 = new ListNode(3);

	return 0;
}

3.3 new出现错误时的处理

malloc在申请空间失败时会返回一个空指针,而new在申请空间时会抛异常

代码语言:javascript
复制
int main()
{
    //这个程序退出码不为0,说明为异常退出,跟malloc不一样,new申请失败会抛异常
	// 抛异常时C++处理错误的一种方式
	// 捕获异常
	try 
	{
		int i = 1;
		int* ptr = nullptr;
		do {
			// 用这种方法来调试,或者也可以在上层工具栏用条件断点
			if (i == 500)
			{
				int x = 0;
			}
			ptr = new int[1024 * 1024]; // 在异常的地方会直接throw e,throw一个对象,跳转到捕获的地方
			cout << i++ << ":" << ptr << endl;
		} while (ptr); // 失败的时候不会返回空指针,而是抛异常
		
		cout << i++ << ":" << ptr << endl;
	}
	// new失败后不是返回空,而是将执行流跳转到这里
	catch (const exception& e) // e是包含错误信息的对象
	{
		cout << e.what() << endl;
	}
	
	// 如果异常不被捕获,程序就会异常结束
	// 异常被捕获,程序能正常结束,并说明异常信息。所以异常必须被捕获

	return 0;
}

异常捕获可以跨函数跳转

代码语言:javascript
复制
void func()
{
	int i = 1;
	int* ptr = nullptr;
	do {
		// 用这种方法来调试,或者也可以在上层工具栏用条件断点
		if (i == 500)
		{
			int x = 0;
		}
		ptr = new int[1024 * 1024]; 
		cout << i++ << ":" << ptr << endl;
	} while (ptr); 

	cout << i++ << ":" << ptr << endl;
}

int main()
{
	try
	{
		func();
	}
	catch (const exception& e) // 可以跨函数跳转
	{
		cout << e.what() << endl;
	}

	return 0;
}

四. operator new与operator delete函数(重点)

new和delete是用户进行动态内存申请和释放的操作符,operator new 和 operator delete 是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过 operator delete全局函数来释放空间。

4.1 operator new 和 operator delete 的源码分析

4.1.1 operator new 函数

代码语言:javascript
复制
/*
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 new 函数底层使用的还是malloc来开辟空间;如果 malloc申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施 就继续申请,否则就抛异常

4.1.2 operator delete 函数

代码语言:javascript
复制
/*
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 delete 本质上是通过free来释放空间的

4.2 new 和 delete 的底层机制

我们可以通过汇编来观察new和delete底层时如何实现的

代码语言:javascript
复制
class A
{
public:
	A(int a = 0)
		: _a(a)
	{
		cout << "A():" << this << endl;
	}
	~A()
	{
		cout << "~A():" << this << endl;
	}
private:
	int _a;
};

int main()
{
	// new实际上是malloc的封装,又增加了抛异常的部分;底层其实还是使用malloc开辟空间
	// A* p1 = (A*)operator new(sizeof(A));
	A* p1 = new A(1); // 先调用opeartor new()开空间,再调用构造函数
	
	A* p2 = new A[10]; // 空间是一次开好的,然后调用n次构造
	// 如果显示的写析构,那无论是否需要释放空间,new开空间时会多开出4个字节的空间,记录对象的个数
	// 这样,在delete调用析构时,就知道要调用几次析构函数了‘
	// 如果不显示的写析构,new就不会多开空间

	// delete是free的封装
	delete p1; // 先调用析构,再调用opeartor delete()销毁空间

	return 0;
}

new的反汇编

在汇编指令中,我们可以观察出,new是先调用了operator new函数,然后又调用了构造函数

delete的反汇编

在汇编指令中,我们可以观察出,delete是先调用了析构,又调用了operator delete函数

我们再来看一下p2底层创建了多大的空间

p2明明创建了10个对象,每个对象大小为4byte,一共应该是40byte大小的内存空间,为什么多了4byte呢? 多开的这4byte的空间,记录的是创建的对象的个数;在 C++ 中,当使用new[ ]动态分配对象数组时,如果类 A 有析构函数,编译器会在分配的内存块开头额外存储一个 “数组长度信息”,用于在delete[ ]前知道需要调用多少次析构函数。如果不显示写析构函数,就不会多开辟空间来记录对象个数 简单来说,类是否有析构函数,会影响new[ ]时是否额外分配 “数组长度信息” 的空间,从而导致总分配内存大小不同。 值得一提是这也就是为什么我们前面提到new和delete要匹配使用,如果不匹配的话那这里释放的就有问题了,从中间那个位置开始释放掉了。但是我们知道空间的释放只可以从起始位置开始释放,不可以分段释放

五. 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来释 放空间

六. 定位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和delete实现new和delete的功能
// 定位new/replacement new
int main()
{
    // p1现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行
     A* p1 = (A*)malloc(sizeof(A));
     new(p1)A;  // 注意:如果A类的构造函数有参数时,此处需要传参
     p1->~A();
     free(p1);

	A* ptr = (A*)operator new(sizeof(A));
	// 显示调用构造函数初始化
	new(ptr)A(1);
	// ptr->A(1); // err 不能这样显示调用构造

	// 显示调用析构
	ptr->~A();
	operator delete(ptr);

	return 0;
}

​编辑

七. 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++——类和对象(1)

C++——类和对象(2)

如有不足或改进之处,欢迎大家在评论区积极讨论,后续我也会持续更新C++相关的知识。文章制作不易,如果文章对你有帮助,就点赞收藏关注支持一下作者吧,让我们一起努力,共同进步!

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一. C/C++内存分布
    • 1.1 内存分布问题
    • 1.2 内存区域划分
  • 二. C语言中动态内存管理方式:malloc/calloc/realloc/free
  • 三. C++内存管理方式
    • 3.1 new/delete操作内置类型
    • 3.2 new和delete操作自定义类型
    • 3.3 new出现错误时的处理
  • 四. operator new与operator delete函数(重点)
    • 4.1 operator new 和 operator delete 的源码分析
      • 4.1.1 operator new 函数
      • 4.1.2 operator delete 函数
    • 4.2 new 和 delete 的底层机制
  • 五. new和delete的实现原理
  • 六. 定位new表达式(placement-new) (了解)
  • 七. malloc/free和new/delete的区别
  • 结语:
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档