C/C++内存管理:深入理解new和delete

C++内存管理:深入理解new和delete

github地址

有梦想的电信狗


前言

本文针对C/C++以及操作系统的内存管理,深入探索动态内存管理。


C++内存管理:深入理解new和delete

1. C/C++内存分布

在C/C++中,内存分为以下几个区域(以32位操作系统为例):

  • 栈(Stack):存储非静态局部变量、函数参数、返回值等,由编译器自动管理,向下增长。
  • 堆(Heap) :动态内存分配区域,需手动管理(malloc/freenew/delete),向上增长。
  • 数据段(静态区) :存储全局变量和静态变量(如static int)。
  • 代码段(常量区) :存放可执行代码和只读常量(如字符串常量"abcd")。

示例分析

cpp 复制代码
int globalVar = 1;            // 数据段
static int staticGlobalVar = 1; // 数据段,静态区
void Test() {
    static int staticVar = 1; // 数据段,静态区
    int localVar = 1;         // 栈区
    int num1[10] = {1, 2};    // 栈区
    char char2[] = "abcd";    // 栈(数组),*char2在栈,只读区的数据会被复制到栈上的数组内存中
    const char* pChar3 = "abcd"; // pChar3在栈区,*pChar3在代码段
    int* ptr1 = new int[4];   // ptr1在栈,指向堆内存
}

2. C语言动态内存管理

C语言通过以下函数管理堆内存:

  • malloc:分配未初始化的内存。
  • calloc:分配并初始化为0。
  • realloc:调整已分配内存的大小,单位为字节。
  • free:释放内存。

区别与陷阱

  • malloc vs calloccalloc会初始化内存数据为0。
cpp 复制代码
int* p2 = (int*)calloc(4, sizeof(int));
//分配大小为4个int的空间,并初始化为0
  • realloc使用 :若realloc失败返回NULL,原指针需保留避免泄漏。

  • realloc在扩大内存时会有两种情况。

    • 原地扩容:当前内存块之后的空间足够大,直接对当前空间进行扩容。
    • 异地扩容 : 当前内存块之后的空间不能满足新空间的大小,先在其他空间开辟一块更大空间,将原来空间的数据拷贝到新空间返回新空间的地址。
cpp 复制代码
int* p3 = (int*)realloc(p2, sizeof(int)*10);
// ... 
free(p3); // 申请成功时,无需再free(p2),realloc已处理

3. C++的new和delete

C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力(比如想在申请完空间后自动完成初始化),而且使用起来比较麻烦(比如需要对指针进行强制类型转换),因此C++又提出了自己的内存管理方式:通过newdelete操作符进行动态内存管理,对自定义类型支持自动调用构造/析构函数。

3.1 操作内置类型

cpp 复制代码
int* ptr1 = new int;       // 未初始化
int* ptr2 = new int(10);   // 初始化为10
int* ptr3 = new int[10];   // 分配数组,但未初始化
// 分配数组,并进行初始化
int* ptr4 = new int[3]{1, 2, 3};
//不完全初始化时,剩余空间自动初始化为 0   
int* ptr5 = new int[10]{1, 2, 3};

delete ptr1;
delete ptr2;

delete[] ptr3;	// 数组需用delete[]
delete[] ptr4;
delete[] ptr6;             

由上图我们可以看出:

  • newmalloc都不会对空间进行初始化,申请的空间都需要我们进行初始化。
  • new可以在申请空间时进行初始化,malloc不能。
  • new数组时,可以初始化,不完全初始化时,其余位置自动初始化为0。

3.2 操作自定义类型

  • new:分配内存并调用构造函数。
  • delete:调用析构函数并释放内存。

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

有如下类:

cpp 复制代码
class A {
public:
	A(int a = 0)
		: _a(a){
		cout << "A():" << this << endl;
	}
	A(const A& aa)
		:_a(aa._a)
	{ cout << " A(const A& aa) " << endl; }
	~A(){ cout << "~A():" << this << endl; }
private:
	int _a;
};
  • new/delete对于【自定义类型】除了开空间还会调用构造函数和析构函数
cpp 复制代码
//调用了构造函数的,用传入的参数初始化
//没有初始化的,调用默认构造函数

//此处初始化发生了隐式类型转换
A* p1 = new A[4]{1, 2, 3, 4};
	
//利用匿名对象进行初始化
A* p2 = new A[4]{ A(1), A(2), A(3) };
//调用构造函数后又拷贝构造,编译器优化成直接构造
//数组中的第四个元素,自动调用默认构造函数进行初始化

//自动调用析构函数
delete[] p1; 
delete[] p2;
										

3.3 创建多少个对象,就调用多少次构造函数

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


4. 底层原理operator new与operator delete

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

  • operator new :封装malloc,失败时抛异常(非返回NULL)。
  • operator delete :封装free

4.1 operator new的源代码

cpp 复制代码
/* 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的源代码

cpp 复制代码
//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 newmalloc的用法相同,operator deletefree的用法相同。
  • 这里只调用了一次构造函数和析构函数,同样可以说明operator newoperator delete的底层是分别调用了mallocfree

通过上述两个全局函数的实现知道,operator new 实际也是通过malloc来申请空间,如果malloc申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异常。operator delete 最终是通过free来释放空间的。


5. new/delete的实现原理

5.1 内置类型

当申请的是内置类型的空间时,newmallocdeletefree基本类似,不同的地方是:new/delete申请和释放的是单个元素的空间,new[]delete[]申请的是连续空间,而且new在申请空间失败时会抛异常,malloc会返回NULL。

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

从图片中我们可以看到,使用new时,会先调用operator new区开辟空间,再调用构造函数完成初始化。
new []delete []以及delete也是同理。此处不再赘述。


6. 定位new表达式(Placement-new)

6.1 概念

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

使用格式:

new (place_address) type或者new (place_address) type(initializer-list)
place_address必须是一个指针,initializer-list是类型的初始化列表

定位new允许在预先分配的原始内存上 调用构造函数初始化对象,常用于需要精细控制内存管理的场景(如内存池)

定位new/replacement new

这样的功能,在池化技术中很常用,内存池,线程池,连接池,进程池...

每次 在堆上申请空间,都要去找堆申请内存(在堆中申请内存是一件很耗时的操作)

因此会提前申请一大块内存,在需要的时候取出内存。

但,取出的只是内存,没有调用构造函数初始化,因此可以用定位new初始化。

cpp 复制代码
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;
}

6.2 关键点

  • 内存与对象生命周期分离

    定位new将内存分配(Allocation)与对象构造(Construction)解耦。内存可提前分配,对象构造延迟到需要时。

  • 显式析构必要

    由于对象在手动管理的内存上构造,需显式调用析构函数(obj->~Type()),否则资源可能泄漏。

  • 应用场景

    • 池化技术(内存池、线程池等):预先分配大块内存,减少频繁申请开销。
    • 自定义内存管理 :避免默认new/delete的额外开销或实现特殊内存策略。

7. 对比

理解记忆

7.1 共同点

malloc/freenew/delete的共同点是:都是从堆上申请/释放空间,并且需要用户手动释放。

7.2 区别

特性+用法
  1. malloc和free是函数,new和delete是操作符
  2. malloc申请的空间不会初始化,new可以初始化,也可以不初始化,但建议初始化。
  3. malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可, 如果是多个对象,[]中指定对象个数即可
  4. malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型
  5. malloc申请空间失败时,返回的是NULL,因此使用时必须检查是否为空,new不需要,但是new需要捕获是否有异常
底层
  1. 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理

7.3 malloc/free与new/delete的区别

特性 malloc/free new/delete
类型 函数 操作符
初始化 是(构造函数)
失败处理 返回NULL 抛出异常
内存计算 手动计算大小 自动推断类型大小
自定义类型处理 不调用构造/析构 调用构造/析构

总结

  • 优先使用new/delete:自动管理构造/析构,更安全。
  • 匹配使用new[]对应delete[],避免未定义行为。

以上就是本文的所有内容了,如果觉得文章写的不错,还请留下免费的赞和收藏,也欢迎各位大佬在评论区交流

分享到此结束啦
一键三连,好运连连!

相关推荐
ChiaWei Lee几秒前
【C++初学】C++核心编程技术详解(二):类与继承
c++
DeepLink11 分钟前
🧠 AI论文精读 :《Attention is All You Need》
人工智能·算法
仙人掌_lz12 分钟前
使用Python从零实现一个端到端多模态 Transformer大模型
开发语言·人工智能·python·ai·transformer·多模态
干净的坏蛋30 分钟前
mac 终端 code 命令打开 vscode,修改 cursor占用
ide·vscode·macos
Funny Valentine-js38 分钟前
swift菜鸟教程1-5(语法,变量,类型,常量,字面量)
开发语言·ios·swift
扰动欧几里得空间40 分钟前
通过Arduino IDE向闪存文件系统上传文件
ide
上线之叁1 小时前
小迪安全-tp框架反序列化,利用链,rce执行,文件删除
java·开发语言
躺着听Jay1 小时前
QCustomPlot-相关优化
java·qt·算法
扫地僧0091 小时前
【中大厂面试题】腾讯 后端 校招 最新面试题
java·数据结构·后端·算法·面试·排序算法
新知图书1 小时前
第一个Qt开发的OpenCV程序
开发语言·qt