首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >【C++基础篇】学习C++就看这篇--->内存管理之new和delete

【C++基础篇】学习C++就看这篇--->内存管理之new和delete

作者头像
HABuo
发布2025-07-15 14:21:39
发布2025-07-15 14:21:39
15700
代码可运行
举报
运行总次数:0
代码可运行

主页:HABUO🍁主页:HABUO

🍁C++入门到精通专栏🍁

🍁如果再也不能见到你,祝你早安,午安,晚安🍁

前言: 从这篇开始,我们就进入C++的新的章节,本篇博客也是后面学习的基础,是一个过渡篇,绝大多数知识仅是了解型的,但有部分知识还是相对比较重要的。这篇博客我们主要了解学习下述知识:operator new与operator delete函数、new和delete、定位new等,希望大家有所收获!

📕一、C/C++内存分布

我们来回顾下C/C++内存分布:

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

2️⃣内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享共享内存,做进程间通信。(在后续Linux博客的更新中会进行详细介绍)

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

4️⃣数据段--存储全局数据和静态数据。

5️⃣代码段--可执行的代码/只读常量。

见下图:

想自测的小伙伴可以看下述的题:

代码语言:javascript
代码运行次数:0
运行
复制
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在哪里?____ staticGlobalVar在哪里?____ staticVar在哪里?____

localVar在哪里?____ num1 在哪里?____ char2在哪里?____

pChar3在哪里?____ ptr1在哪里?____ *char2在哪里?___

*pChar3在哪里?____ *ptr1在哪里?____

答案如下:

代码语言:javascript
代码运行次数:0
运行
复制
int globalVar = 1;//全局变量定义在数据段
static int staticGlobalVar = 1;//静态全局变量也定义在数据段
//上面这两个全局变量是在main函数之前就已经初始化,在哪都能用,作用域是全局的
//但是这两个是有区别的,区别在于链接属性是不一样的,globalVar是所有文件可见,staticGlobalVar只在当前文件可见
void Test()
{
	static int staticVar = 1;//静态局部变量也定义在数据段,这一个是运行到这里再初始化,它的作用域在Test函数中,只能在Test函数中使用
	int localVar = 1;//局部变量定义在栈区
	int num1[10] = { 1, 2, 3, 4 };//数组定义在栈区
	char char2[] = "abcd";//数组定义在栈区
	const char* pChar3 = "abcd";//指针定义在栈区
	int* ptr1 = (int*)malloc(sizeof(int) * 4);//ptr1是指针存放在栈区,但是它所指向的内容却是存放在堆区
	int* ptr2 = (int*)calloc(4, sizeof(int));//ptr2是指针存放在栈区,但是它所指向的内容却是存放在堆区
	int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4);//ptr3是指针存放在栈区,但是它所指向的内容却是存放在堆区
	free(ptr1);
	free(ptr3);
}

需要注意:上面讲述了全局的全局变量和静态变量的区别,以及局部的静态变量与全局变量的区别。

趁势我们把这些内容也回顾下:

sizeof(num1) = ____; sizeof(char2) = ____; sizeof(pChar3) = ____;

sizeof(ptr1) = ____; strlen(char2) = ____; strlen(pChar3) = ____;

答案如下:

sizeof(num1) = 40; sizeof(char2) = 5; sizeof(pChar3) = 4/8;

sizeof(ptr1) = 4/8; strlen(char2) = 4; strlen(pChar3) = 4;

sizeof 和 strlen 区别

本质不同

  • sizeof
    • 编译时运算符(不是函数),在编译期间确定结果。
    • 功能:计算变量或数据类型所占内存的字节数(包括字符串末尾的 \0)。
  • strlen
    • 运行时函数(定义在 <string.h> 中)。
    • 功能:计算字符串的实际长度(从起始地址到第一个 \0 前的字符数,不包括 \0)。
代码语言:javascript
代码运行次数:0
运行
复制
//静态字符数组
char str[] = "Hello";
//sizeof(str) → 结果为 6(5个字符 + 结尾的 \0)。
//strlen(str) → 结果为 5(只计算有效字符 'H','e','l','l','o')。

//指针指向的字符串
const char* ptr = "Hello";
//sizeof(ptr) → 结果为 指针大小(4 或 8 字节,取决于系统)。
//strlen(ptr) → 结果为 5(字符串内容长度)。

//固定长度数组
char arr[100] = "abc";
//sizeof(arr) → 结果为 100(整个数组的内存大小)。
//strlen(arr) → 结果为 3(字符串 "abc" 的长度)。

总结如下:

特性

sizeof

strlen

类型

运算符(编译时求值)

函数(运行时求值)

参数

可接受变量、数据类型或表达式

只接受字符串地址(const char*)

计算内容

内存占用大小(包括 \0)

字符串有效长度(不包括 \0)

安全性

无副作用(不访问内存)

需遍历字符串直到 \0(可能越界)

数组退化

对数组返回实际大小

数组退化为指针后失效

指针处理

返回指针本身的大小(4/8 字节)

返回指针指向字符串的长度

📕二、C++内存管理方式

✨2.1 C语言内存管理方式(回顾)

C语言当中我们知道malloc/calloc/realloc/free,在此进行回顾下:

特性

malloc

calloc

realloc

初始化

不初始化(垃圾值)

初始化为零

保留原数据,新增部分不初始化

参数

1 个(总字节数)

2 个(元素数量 × 元素大小)

2 个(原指针 + 新字节数)

主要用途

通用内存分配

需要初始化的数组

调整已分配内存的大小

内存计算

直接指定字节数

自动计算 num * size

修改现有内存块大小

性能

较快(不初始化)

较慢(需初始化)

可能涉及内存复制(较慢)

malloc (Memory Allocation)

  • 功能:分配指定字节数的未初始化内存
  • 特点
    • 只接受一个参数:需要分配的字节数size
    • 分配的内存内容是未初始化的(包含随机垃圾值)
    • 适用于需要精确控制内存大小的场景
代码语言:javascript
代码运行次数:0
运行
复制
void* malloc(size_t size);
int* arr = (int*)malloc(5 * sizeof(int)); // 分配 20 字节(假设 int 占 4 字节)

calloc (Contiguous Allocation)

  • 功能:分配指定数量和大小的内存块并初始化为零
  • 特点
    • 接受两个参数:元素数量(num)和单个元素大小(size
    • 分配的内存内容会被自动初始化为 0(比 malloc 更安全)
    • 总分配大小 = num * size
    • 适用于需要初始化数组的场景
代码语言:javascript
代码运行次数:0
运行
复制
void* calloc(size_t num, size_t size);
int* arr = (int*)calloc(5, sizeof(int)); // 分配并初始化 5 个 int(全 0)

realloc (Re-allocation)

  • 功能调整已分配内存块的大小
  • 特点
    • 接受两个参数:原内存指针(ptr)和新字节大小new_size
    • 可能的行为:
      • 原地扩展/缩小内存(如果后续空间足够)
      • 重新分配新内存,复制旧数据,释放旧内存
    • 不会初始化新增的内存区域
    • 如果 ptrNULL,则等价于 malloc(new_size)
代码语言:javascript
代码运行次数:0
运行
复制
void* realloc(void* ptr, size_t new_size);
int* new_arr = (int*)realloc(arr, 10 * sizeof(int)); // 扩展到 40 字节

✨2.2 new/delete

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

用法如下:

代码语言:javascript
代码运行次数:0
运行
复制
class A
{
public:
	A(int n = 0)
	{
		cout << "A()" << endl;
	}
	~A()
	{
		cout << "~A()" << endl;
	}
};
int main()
{
	A* p1 = (A*)malloc(sizeof(A));
    free(p1);
	A* p2 = new A;//对于自定义类型是如此
	delete p2;
    int* p3 = new int;//对于内置类型这样定义
    int* p4 = new int(100);//这是定义个int类型并初始化的形式
    int* p5 = new int[100];//这是定义数组的形式
    delete[100] p5;//对于数组释放空间是这样定义
    int* p6 = new int[100]{ 1,2,3 };//这是定义数组的形式并初始化的形式
	return 0;
}

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

相信会有这样的疑惑,既然C++继承了C语言的用法,即默认malloc、calloc等函数是可以使用的,为什么还要new/delete这些呢?

这就需要讲述它们的差别:

new与malloc

  • 对于内置类型来说,new和malloc没有什么区别
  • 对于自定义类型来说,new会调用自定义类型的构造函数,而malloc不会
  • new可以通过构造函数初始化变量的内容

delete与free

  • 对于内置类型,delete和free没有什么区别
  • 对于自定义类型,delete会调用自定义类型的析构函数,而free不会
  • delete面对不同的情况需要加上[ ],然而free不用考虑

✨2.3 operator new与operator delete函数

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

operator new 和operator delete是系统提供的全局函数

new在底层调用operator new全局函数来申请空间,delete在底层通过 operator delete全局函数来释放空间

operator new使用方式:

代码语言:javascript
代码运行次数:0
运行
复制
void* p1 = malloc(1024);
void* p2 = operator new (1024);

可以观察下面operator new库中的实现本质就是用malloc申请空间

代码语言:javascript
代码运行次数:0
运行
复制
void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
void *p;
while ((p = malloc(size)) == 0)
	if (_callnewh(size) == 0)
	{
	    // 如果申请内存失败了,这里会抛出bad_alloc 类型异常
	    static const std::bad_alloc nomem;
	    _RAISE(nomem);
	}
return (p);
}

所以 operator new域malloc正常使用的方式都是一样的,只不过处理错误的方式不一样

代码语言:javascript
代码运行次数:0
运行
复制
int main()
{
	size_t size = 2;
	void* p1 = malloc(size * 1024 * 1024 * 1024);
	cout << p1 << endl; //申请失败返回0
	try
	{
		void* p2 = operator new (size * 1024 * 1024 * 1024);
		cout << p2 << endl;//失败抛异常(面向对象处理错误的方式)
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

抛异常不是本章节的重点,等到我们学习到此的时候,再着重学习!

所以malloc、operator new、new它们的关系如下:

malloc operator new -->malloc+失败抛异常 new --> operator new + 构造函数 new比malloc不一样的地方:1.调用构造函数初始化 2. 失败抛异常 delete和free不一样的地方在于delete调用析构函数清理 operator delete 和free没有本质区别

补充知识: 相信上述代码你关注到了 size_t size = 2; size * 1024 * 1024 * 1024; 为什么不写成2*1024*1024*1024?注意此处我们是为了让它开辟不出空间,如果写成2*1024*1024*1024那这里就会报溢出错误,原因如下:

  • 整数类型范围
    • 在 C ++ 中,整数类型有不同的长度和表示范围。int 通常是一个有符号类型,在 32 - bit 系统中占 32 位。它的取值范围是 - 2^(31) 到 2^(31) - 1,即 - 2147483648 到 2147483647。当计算结果超过这个范围时,就会发生溢出
    • size_t 是一个无符号整数类型,用于表示大小和索引等非负值。在 32 - bit 系统中,size_t 通常是 32 位无符号整数,其范围是 0 到 4294967295无符号整数在计算过程中不会出现负数,当计算结果超过其最大值时,会产生溢出,但不会像有符号整数那样出现负值,而是会循环回到 0 并继续计算
  • 编译阶段和运行阶段的检查差异
    • 编译器在编译阶段会进行语法检查和部分语义检查,其中包括常量表达式的整数溢出检查。如果一个常量表达式(如 2 * 1024 * 1024 * 1024)在编译阶段就可以确定其结果超出了目标整数类型的范围,编译器会报错
    • 对于包含变量的表达式(如 size * 1024 * 1024 * 1024),编译器无法在编译阶段确定其值是否会溢出,因为变量的值是在程序运行时才确定的。所以在编译阶段不会报错,只有在运行时,当这个表达式的值超出了程序能够处理的范围(例如内存分配时超出可用内存等),才会出现运行时错误。
  • 内存分配限制
    • 在 32 - bit 操作系统中,地址总线的宽度限制了内存寻址的范围。虽然理论上 32 - bit 地址总线可以寻址 4GB(2^32 字节)的内存,但实际操作系统会将一部分地址空间用于系统用途(如内核空间等),用户态进程可用的内存地址空间通常小于 4GB。所以即使是使用 size_t 类型来计算内存分配大小,当分配的内存大小超过系统可用内存或者用户态进程可分配内存限制时,也会出现内存分配失败的情况,这属于运行时错误,这也正是我们想要的结果,需要通过检查返回值(如 operator new 分配内存失败会抛出异常或者返回空指针,取决于是否使用了 nothrow 版本)来处理。

operator delete使用方式:

代码语言:javascript
代码运行次数:0
运行
复制
operator delete(p2)

operator delete库中的实现如下所示:

代码语言:javascript
代码运行次数:0
运行
复制
#define free(p) _free_dbg(p, _NORMAL_BLOCK)
void operator delete(void *pUserData)
{
     _CrtMemBlockHeader * pHead;
     RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));
     if (pUserData == NULL)
         return;
     _mlock(_HEAP_LOCK);
     __TRY
         pHead = pHdr(pUserData);
         _ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));
         _free_dbg( pUserData, pHead->nBlockUse );
     __FINALLY
         _munlock(_HEAP_LOCK);
     __END_TRY_FINALLY
     return;
}

看不懂没关系,只需要抓住重点: delete里面调用了: _free_dbg(p, _NORMAL_BLOCK) 然而此函数正是free函数定义的宏替换!

说明delete的底层实现实际上 是调用了C语言中的free函数

delete是使用free来释放空间的,因此两者毫无差别

总结一下: operator new和operator delete本质就是为new和delete而生的,事实上new和delete底层实现上就是用 operator new和operator delete。new --> operator new + 构造函数,delete -->operator delete + 析构函数

✨2.4 operator new与operator delete类专属重载(了解即可)

在 C++ 中,operator newoperator delete类专属重载允许你为特定类定制其对象的内存分配和释放行为。

1️⃣目的: 替换默认的全局 newdelete 对于该类对象的行为。

2️⃣作用域: 这些重载函数是类的静态成员函数(即使你不显式写 static,它们也自动是 static 的)。它们只影响该类自身对象的创建和销毁(通过 new 表达式和 delete 表达式)。

3️⃣控制权: 让你接管:

分配: 如何获取一块足够容纳单个类对象或对象数组的原始内存。

释放: 如何归还这块内存给系统或内存池。

4️⃣不涉及构造/析构: operator new 只负责分配原始、未初始化的内存。对象构造(构造函数调用)发生在 operator new 返回之后,由编译器插入的代码完成。同样,operator delete 只负责释放对象已被析构后的内存。对象析构(析构函数调用)发生在调用 operator delete 之前。

代码语言:javascript
代码运行次数:0
运行
复制
typedef struct ListNode_C
{
	int _val;
	struct ListNode_C* _next;
	struct ListNode_C* _prev;

	void* operator new (size_t n)
	{
		void* p = nullptr;
		p = allocator<ListNode_C>().allocate(1);
		cout << "memory pool allocate" << endl;
		return p;
	}
	void operator delete(void* p)
	{
		allocator<ListNode_C>().deallocate((ListNode_C*)p, 1);
		cout << "memory pool deallocate" << endl;
	}
}ListNode_C;

class List
{
	ListNode_C* p = new ListNode_C;
};

✨2.5 定位new

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

代码语言:javascript
代码运行次数:0
运行
复制
	A* p1 = new A;//这里会自动调用构造函数
	delete p1;//这里会自动调用析构函数

想模拟上述的行为

代码语言:javascript
代码运行次数:0
运行
复制
	//显式调用A的构造函数和析构函数
	A* p2 = (A*)operator new(sizeof(A));
	//对已经存在的一块空间调用构造函数初始化,定位new/replacement new
	new(p2)A(10); //new(空间的指针)类型(参数)// 注意:如果A类的构造函数有参数时,此处需要传参
	p2->~A();
	operator delete(p2);

总结一下: 定位new的作用就是对已经存在的空间调用构造函数初始化的一种方式,可以说operator加上定位new就是new


📕三、总结

本篇博客我们总结学习回顾了C/C++内存分布主要包括栈、内存映射段、堆、数据段和代码段。栈用于存储局部变量和函数参数等,堆用于动态内存分配,数据段存储全局和静态数据,代码段存放可执行代码和只读常量。C语言通过malloccallocreallocfree进行内存管理,而C++引入了newdelete操作符,适用于对象和数组的动态内存管理。new会调用构造函数初始化对象,delete会先调用析构函数再释放内存。C++还允许重载operator newoperator delete来自定义内存分配和释放行为,甚至支持定位new表达式在已分配内存中初始化对象。与C语言的内存管理函数相比,C++的newdelete更面向对象,能自动处理构造和析构过程。希望大家有所收获!


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

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 📕一、C/C++内存分布
  • 📕二、C++内存管理方式
    • ✨2.1 C语言内存管理方式(回顾)
    • ✨2.2 new/delete
    • ✨2.3 operator new与operator delete函数
    • ✨2.4 operator new与operator delete类专属重载(了解即可)
    • ✨2.5 定位new
  • 📕三、总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档