C++内存管理:new/delete vs malloc/free深度解析

在 C/C++ 程序开发中,内存管理始终是绕不开的核心话题。无论是手动申请堆区空间,还是借助语言特性自动管理对象生命周期,理解内存的分布与分配方式,都是写出健壮、高效代码的前提。本文将从内存分区讲起,对比 malloc/free 与 new/delete 的差异,并深入剖析 new/delete 的底层实现原理,帮助大家系统掌握 C/C++ 的动态内存管理。

C/C++内存分布

我们知道内存一般分为栈区、堆区、静态区(数据段)、常量区(代码段)。

  • 栈区:存储局部变量、函数形参、临时变量;
  • 堆区:用于动态内存分配;
  • 静态区(数据段) :存储 static 修饰的变量、全局变量;
  • 常量区(代码段) :存储字符串常量、const 修饰的全局常量、程序代码(函数指令)。

通常堆区内存空间比较大。栈区空间是向下生长的,因为它的内存地址比较高;堆区空间是向上生长的,因为它的内存地址比较低。

在32位环境下,可以存储 232 个字节的空间,也就是 4G 的空间,在 64 位环境下就更多了。

malloc/free使用

喜欢用 malloc 和 free 的同学应该知道,调用它们要传参数,返回来的地址类型还是 void,需要指定进行强制类型转换,比较麻烦。而且在 C++ 中多了一个类类型,我们实例化一个对象的时候都要调用对应的构造函数,如果我们用 malloc 申请一块空间的话,就无法调用构造函数了,这也是一个问题。

cpp 复制代码
#include<iostream>
using namespace std;

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
	{
		cout << "Date()" << endl;
	}
	~Date()
	{
		cout << "~Date()" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
  // malloc一个Date类对象的空间
	Date* ptr = (Date*)malloc(sizeof(Date));
	if(ptr==NULL)
	{
	perror("malloc");
	exit(-1);
	}
	//释放该空间
	free(ptr);
	return 0;
}

C++ 中引入了 new 和 delete 两个关键字,用于手动向堆区申请和释放一块空间。

new/delete

new/delete使用

我们如何使用 new 和 delete 呢?是这样使用的,new + 类型,该类型是我们要申请的空间大小,后可跟 () 用于初始化我们申请的对象。

cpp 复制代码
//申请创建一个int大小的空间
int* p1 = new int;

//申请创建一个int大小的空间并初始化为10
int* p2 = new int(10);

new 申请创建后就需要 delete 释放空间,使用方式为 delete 指针,该指针指向我们向堆内存中申请的空间的初始地址。

cpp 复制代码
//释放空间
delete p1;
delete p2;

用 free 释放也可以,但我们一般习惯配套使用。

当我们想申请一个数组的空间,用 new 是这样申请的:new 类型[],[] 中写我们想申请该类型空间的个数,是连续的。如果我们想初始化,需要在 [] 后加 {},在 {} 里进行初始化。

cpp 复制代码
//申请创建10个int大小的空间
int* p3 = new int[10];

//申请创建10个int大小的空间并进行部分初始化
int* p4 = new int[10] {1,2,3,4,5};

可以看到 p4 指向的数组中,我们未给出初始化值的部分都初始化为 0 了。

我们用 delete 来释放数组空间时就必须在 delete 后加 []。

cpp 复制代码
delete[] p3;
delete[] p4;

在申请内置类型空间时 new/delete 和 malloc/free 没有什么差别,差别主要体现在申请自定义类型空间时,new 和 delete 会帮我们调用构造和析构函数,比如说之前的 Date 类。

cpp 复制代码
#include<iostream>
using namespace std;

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
	{
		cout << "Date()" << endl;
	}
	~Date()
	{
		cout << "~Date()" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
// 申请一个Date类对象大小的空间
	Date* ptr = new Date;
	// 释放该块空间
	delete ptr;
	return 0;
}

我们运行一下就可以看到确实是调用了构造和析构函数。

但new/delete究竟是如何实现这些操作的呢?我们接下来看看它的底层原理。

new/delete 的底层原理

我们来看用 new/delete 来申请/销毁一个 Stack 类对象:

cpp 复制代码
class Stack
{
public:
	Stack(int n = 4)
		:_array((int*)malloc(n * sizeof(int)))
		, _top(0)
		, _capacity(n)
	{
		if (_array == nullptr)
		{
			perror("malloc");
			exit(-1);
		}
	}

	~Stack()
	{
		free(_array);
	}

private:
	int* _array;
	size_t _top;
	size_t _capacity;
};

int main()
{
	Stack* ptr = new Stack;
	delete ptr;
	return 0;
}

在这次用 new 申请空间的过程中,应该是先申请开辟了一块空间,ptr 指向该块空间。然后在这块空间上调用构造函数,该构造函数又开辟了一块空间,这个 Stack 类对象的 _array 指向该块空间。

然后在 delete 时,如果先释放掉存放 Stack 类对象的空间,那么 _array 指向的内存空间就无法被释放,造成内存泄露,所以应该是先调用相应的析构函数完成清理工作,然后再释放 ptr 指向的空间。

对于调用构造函数和析构函数我们好理解,那么它们是如何申请和释放自定义类型的空间的呢?接下来将做说明。

operator new 和 operator delete 函数

虽然 operator new 和 operator delete 很像我们之前所说过的运算符重载,但不是的,它是用来申请和释放内存空间的,它们的底层代码如下:

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);
}
/*
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 来实现的,operator delete 是靠 free 来实现的。至于使用 new 来申请一块空间为什么不用判断是否申请成功,是因为其内部有抛异常机制,这个以后会讲。

我们之前所判断的 new 是先调用函数 operator new 来申请一块类大小的空间,然后调用相应构造函数对这块空间进行初始化。然后 delete 先调用析构函数完成对对象中资源的清理,然后调用 operator delete 函数释放之前申请到的类大小的空间。我们是可以验证其过程的。

我们来到调试阶段,查看反汇编代码:

可以看到是先调用的 operator new 函数,然后调用的 Stack 函数。

对于 delete 部分我们也可以看到:

这里编译器将两个函数的调用步骤整合成一个了,我们转到X86环境下去看,比较容易观察到delete对于函数的调用。

可以看到是先调用~Stack函数,再调用的operator delete函数。

这就是new/delete的底层实现原理了。

对于数组的new/delete,则会调用operator new[]和operator delete[]函数,operator new[]就是对operator new函数的复用。

我们用new创建了n个对象,operator new[]就会调用n次operator new,然后再调用n次构造函数完成对这n个对象的初始化。

delete就会先调用n次析构函数完成对这n个对象的资源清理,然后调用operator delete[],该函数内部就会调用n次operator delete来释放资源。

定位表达式 new

C++ 提供了一种方式,可以让我们显式地调用构造函数。

比如之前的 Date 类,

cpp 复制代码
class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
	{
		cout << "Date()" << endl;
	}
	~Date()
	{
		cout << "~Date()" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

我们在使用 malloc 来申请空间后,就可以显式调用构造函数来初始化该对象。

cpp 复制代码
int main()
{
	Date* ptr =  (Date*)malloc(sizeof(Date));
	//定位 new 表达式
	new(ptr)Date(2026, 10, 12);
	free(ptr);
	return 0;
}

new后面的括号里跟指向对象的指针,然后跟构造函数名(初始化列表)就好了。

new/delete 比 malloc/free 好在哪?

  • new/delete 是关键字,malloc/free 是库函数,需要包含对应头文件。
  • 对于自定义类型,new/delete 会调用相应构造函数和析构函数,malloc/free 不会。
  • new/delete 的使用非常简单,malloc 则需要传参,拿到的地址还要进行强制类型转换。
  • new会进行抛异常,比较安全。
相关推荐
yuniko-n1 小时前
【JUC】公平锁与非公平锁
java
郑州光合科技余经理1 小时前
本地生活服务系统:预约状态和进度通知接口怎么拆
java·开发语言·前端·javascript·后端·php·生活
靠沿1 小时前
MySQL数据库基础——事务
java·数据库·mysql
Joy T7 小时前
Agent 确定性上下文设计:从 Prompt 参数注入到能力分层
java·开发语言·微服务·架构·prompt·agent context
m0_5873830010 小时前
广州 24 小时自助健身房解决方案技术实现与实战指南
java·spring·小程序·架构·需求分析
·云扬·10 小时前
Prompt 工程与安全:从 System 设计到注入防御
java·人工智能·安全·ai·prompt
代码不停11 小时前
自动化测试(java)
java·开发语言·selenium·自动化
别动我齐刘海12 小时前
Linux 系统调试学习1
linux·运维·服务器·c++·学习·机器学习·机器人
鬼手点金13 小时前
opencode-rules 插件
java·开发语言·前端·人工智能·chrome·git·github