C++ 内存管理:自定义类型 new/delete 调用构造 / 析构深度解析

🔥小叶-duck个人主页

❄️个人专栏《Data-Structure-Learning》

《C++入门到进阶&自我学习过程记录》

未择之路,不须回头
已择之路,纵是荆棘遍野,亦作花海遨游


目录

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

1、内存分布问题

2、概念说明

二、C/C++中动态内存管理方式解析

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

2、C++中内存管理方式

[2.1 new/delete操作内置类型](#2.1 new/delete操作内置类型)

[2.2 new/delete操作自定义类型(重点掌握)](#2.2 new/delete操作自定义类型(重点掌握))

[2.2.1 new 创建单个对象](#2.2.1 new 创建单个对象)

[2.2.2 创建对象数组](#2.2.2 创建对象数组)

[2.3 new 申请空间失败时的处理](#2.3 new 申请空间失败时的处理)

[三、operator new 与 operator delete 函数](#三、operator new 与 operator delete 函数)

[1、operator new 函数](#1、operator new 函数)

[2、operator delete 函数](#2、operator delete 函数)

[四、new 和 delete 的底层实现原理](#四、new 和 delete 的底层实现原理)

[1、 内置类型](#1、 内置类型)

2、自定义类型

[2.1 new 的原理](#2.1 new 的原理)

[2.2 delete 的原理](#2.2 delete 的原理)

[2.3 new TN 的原理](#2.3 new T[N] 的原理)

[2.4 delete 的原理](#2.4 delete[ ] 的原理)

[五、定位 new 表达式(placement-new)(了解即可)](#五、定位 new 表达式(placement-new)(了解即可))

[六、malloc/free 和 new/delete 的区别](#六、malloc/free 和 new/delete 的区别)

1、共同点

2、不同点

结束语


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

1、内存分布问题

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

//C/C++内存分布问题
int globalVar = 1;
static int staticGlobalVar = 1;
int main()
{
	static int staticVar = 1;
	int localVar = 1;
	int num1[10] = { 1, 2, 3, 4 };
	char char2[] = "abcd";
	const char* pChar3 = "abcd";

	*char2 += 1;
	//(*(char*)pChar3) += 1;

	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);

	const int i = 10;
	int j = 1;
	cout << &i << endl;
	cout << &j << endl;
	cout << (void*)pChar3 << endl;

	return 0;
}

1.选择题:

选项: A.栈 B.堆 C.数据段(静态区) D.代码段(常量区)

globalVar在哪里?++C++

staticGlobalVar在哪里?++C++

staticVar在哪里?++C++

localVar在哪里?++A++

num1 在哪里?++A++

解析: 前五个都比较好区分,因为数据段是存放全局数据和静态数据, 所以globalVarstaticGlobalVar 以及staticVar 均在数据段,localVarnum1 为局部变量是存放栈区中。

char2在哪里?++A++

*char2在哪里?++A++

pChar3在哪里?++A++

*pChar3在哪里?++D++

ptr1在哪里?++A++

*ptr1在哪里?++B++

解析 :这五个就需要好好讲解了,首先我们要知道局部变量就是在栈区进行创建的,所以 char2pChar3ptr1 这三个局部变量都是存放在栈区。

对于 char2:char char2\[\] = "abcd"; 我们学习了类和对象把这个代码在语义上可以理解为C++中的拷贝构造 ,char2 本身作为局部变量存放在栈区,编译器会把常量区的这四个字符(a、b、c、d)以及 \0 逐个拷贝到栈上的 char2 数组分配的内存空间。

对于 pChar3:由于 pChar3 本身就是字符指针,存放的就是字符串的地址 ,这里就没有拷贝一说 ,这样其实就能和C++中的引用结合来理解,这也是为什么 const char* pChar3 = "abcd"; 要加 const 进行修饰了,引用其实也能理解为地址,本质 还是因为两者地址是相同的 ,如果不加上 const 进行修饰也就会导致权限放大而报错。所以对pChar3 进行解引用 指向的位置就是常量区的字符串的首元素

对于 ptr1:ptr1 就比较简单了,我们之前就讲过动态开辟的空间是存放在堆区 的,由于 ptr1 本身就是整型指针,存放的就是动态开辟的空间的地址 ,对其进行解引用 后指向的位置就是堆区中动态开辟的空间的首元素

2.填空题:

sizeof(num1) = ++40++; // 数组大小,10个整形数据一共40字节

sizeof(char2) = ++5++; // sizeof 求大小包括 \0 的空间

strlen(char2) = ++4++; // strlen 求长度不包括 \0 的长度

sizeof(pChar3) = ++4++; // pChar3 为指针,指针大小只为4或者8

strlen(pChar3) = ++4++;// strlen 求长度不包括 \0 的长度

sizeof(ptr1) = ++4++;// ptr1 是指针,指针大小只为4或者8

具体各个变量所在位置可以直接看下图:

2、概念说明

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

内存映射段:是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享共享内存,做进程间通信。(Linux如果没学到这块,现在只需要了解一下)

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

数据段 :存储全局数据静态数据

代码段:可执行的代码 / 只读常量。

二、C/C++中动态内存管理方式解析

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

C语言中的动态内存管理方式我们之前就学过,这里就不再过多赘述里面的相关知识点了,大家有想详细了解的可以看我下面Gitee里讲解的动态内存管理:

动态内存管理/动态内存管理/study9.22.c · 小叶鸭/c语言学习 - 码云 - 开源中国

我们直接来看一段代码并回答一下相关问题:

cpp 复制代码
#include <iostream>
using namespace std;
int main()
{
	// 1.malloc/calloc/realloc的区别是什么?
	int* p2 = (int*)calloc(4, sizeof(int));
	int* p3 = (int*)realloc(p2, sizeof(int) * 50);
	cout << p2 << endl;
	cout << p3 << endl;

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

先回答一下第二题:是否需要free(p2)?

是不需要free(p2)。因为 realloc 扩容分两种情况:(1)原地扩容 ;(2)异地扩容
原地扩容
时 p2 和 p3 两者本身地址就是同一块地址 ,free了 p3 时 p2 也就跟着被free了;

异地扩容 时 p2 和 p3 指向不同的地址,但原来p2 指向的内存块已经被 realloc 自动释放了,也不需要free p2

面试题

1.malloc/calloc/realloc的区别?
malloc
:分配指定字节内存,不初始化,数据随机。

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

2.malloc的实现原理?

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

这个问题的详细解答和说明大家可以学到后面再回过头来看,通过这个视频可以先深入了解一下。

参考视频: 【CTF】GLibc堆利用入门-机制介绍_哔哩哔哩_bilibili

2、C++中内存管理方式

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

2.1 new/delete操作内置类型

cpp 复制代码
int main()
{

	int* p1 = new int;	    // 单个对象(未初始化)
	int* p2 = new int[10];  // 数组(未初始化)

	delete p1;
	delete[] p2;
	return 0;
}
cpp 复制代码
int main()
{
	//申请对象 + 初始化
	int* p3 = new int(5);	    // 单个对象(初始化为5)
	int* p4 = new int[10] {1, 2, 3, 10};  // 数组(初始化为{1, 2, 3, 10},后面默认初始化为0)

	delete p3;
	delete[] p4;
	return 0;
}

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

大家可能会觉得这些操作我们之前C语言中使用的 malloc 等操作也可以实现,虽然没这么方便,但也不是完全不行,那C++到底为什么要引入新的 new/delete 操作符呢?

只针对于上面的内置类型的确没有意义,但对于C++中的类类型而言就非常有意义了。

2.2 new/delete操作自定义类型(重点掌握)

2.2.1 new 创建单个对象
cpp 复制代码
class A
{
public:
	A(int a = 0)
		: _a(a)
	{
		cout << "A():" << this << endl;
	}

	~A()
	{
		cout << "~A():" << this << endl;
	}
private:
	int _a;
};

int main()
{
	// malloc 只开空间,不会去自动调用构造进行初始化,不太好使
    A* p1 = (A*)malloc(sizeof(A));

    // new 操作符在动态开辟空间的同时,对于类类型而言还会自动调用对应的构造进行初始化
    A* pa1 = new A;
    A* pa2 = new A(1);

    // delete 操作符会自动调用析构函数
    delete pa1;
    delete pa2;
	return 0;
}

并且对于之前数据结构 学过的创建链表而言,new/delete 操作符的作用相比 malloc 会显得尤为突出:

cpp 复制代码
class ListNode
{
public:
	ListNode(int val = 0)//链表的构造函数
		:_val(val)
		,_next(nullptr)
	{

	}

	int _val;
	ListNode* _next;
};

int main()
{
	//创建链表
	ListNode* n1 = new ListNode(1); //用 new 在动态开辟空间的同时还能自动调用构造函数进行初始化
	ListNode* n2 = new ListNode(2);
	ListNode* n3 = new ListNode(3);
	ListNode* n4 = new ListNode(4);
	n1->_next = n2;
	n2->_next = n3;
	n3->_next = n4;
	//相较于之前创建链表既要写一个开辟空间的函数还要写一个初始化的函数 new 操作符进行了极大的优化
	return 0;
}
2.2.2 创建对象数组

第一种方法:

cpp 复制代码
class A
{
public:
	A(int a = 0)
		: _a(a)
	{
		cout << "A(int a)" << this << endl;
	}

	~A()
	{
		cout << "~A()" << this << endl;
	}

	A(const A& ra)
	{
		cout << "A(const A& ra)" << this << endl;

	}
private:
	int _a;
};

int main()
{
	A a1 = A(1);
	A a2 = A(2);
	A a3 = A(3);

	//第一种写法(比较繁琐而且不能进行优化,一般不会用)
	A* p1 = new A[3]{ a1, a2, a3 };
	//new A[3]会先创建3个数组元素(A对象),通过a1、a2、a3分别给这3个元素进行初始化
	//此时就会调用拷贝构造函数(形参为类类型)
    delete p1;
	return 0;
}

第二种写法(匿名对象):

cpp 复制代码
class A
{
public:
	A(int a = 0)
		: _a(a)
	{
		cout << "A(int a)" << this << endl;
	}

	~A()
	{
		cout << "~A()" << this << endl;
	}

	A(const A& ra)
	{
		cout << "A(const A& ra)" << this << endl;

	}
private:
	int _a;
};

int main()
{
	//第二种写法
    A* p2 = new A[3]{ A(1), A(2), A(3) };
    //通过匿名对象可实现连续构造 + 拷贝构造 -> 编译器会优化为直接构造
    delete []p2;
	return 0;
}

第三种写法(隐式类型转换):

cpp 复制代码
class A
{
public:
	A(int a = 0)
		: _a(a)
	{
		cout << "A(int a)" << this << endl;
	}

	~A()
	{
		cout << "~A()" << this << endl;
	}

	A(const A& ra)
	{
		cout << "A(const A& ra)" << this << endl;

	}
private:
	int _a;
};

int main()
{
	//第三种写法
    //A aa1 = 1;//隐式类型转换
    A* p3 = new A[3]{ 1, 2, 3 };
    //先创建3个数组元素(A对象),通过1, 2, 3分别给这3个元素进行初始化
    //此时就会进行隐式类型转换
    //和第二种方法一样,会进行连续构造 + 拷贝构造 -> 编译器会优化为直接构造
    delete []p3;
	return 0;
}

2.3 new 申请空间失败时的处理

对于C语言中动态开辟空间的malloc 函数 在空间申请失败后会返回 NULL ,而new 在申请空间失败时会抛异常

cpp 复制代码
//1024 * 1024相当于就是1兆(1MB)
void func()
{
	int i = 1;
	int* ptr = nullptr;
	do 
	{
		ptr = new int[1024 * 1024];//throw
		cout << i++ << ":" << ptr << endl;
	} while (ptr);
	cout << i++ << ":" << ptr << endl;
}
int main()
{
	try
	{
		func();//可以直接跳跃函数到异常
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;//打印错误信息
	}
	return 0;
}

通过打印的结果就能发现我所用的电脑当前所能申请到的最大空间约为9个G。

三、operator new 与 operator delete 函数

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

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);
}

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 new 实际也是通过 malloc 来申请空间 ,如果 malloc 申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异常。operator delete 最终是通过 free 来释放空间的。

四、new 和 delete 的底层实现原理

1、 内置类型

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

2、自定义类型

2.1 new 的原理

(1) 先调用 operator new 函数申请空间

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

2.2 delete 的原理

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

(2) 再调用 operator delete 函数释放对象的空间

2.3 new TN 的原理

(1) 先调用 operator new\[\] 函数,在 operator new\[\] 中实际调用 operator new 函数完成N个对象空间的申请

(2) 再在申请的空间上执行N次构造函数

2.4 delete 的原理

(1) 先在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理

(2) 再调用 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 的定义表达式进行显示调构造函数进行初始化

cpp 复制代码
class A
{
public:
	A(int a = 0)
		: _a(a)
	{
		cout << "A(int a)" << this << endl;
	}

	~A()
	{
		cout << "~A()" << this << endl;
	}

private:
	int _a;
};

//定位new表达式(placement-new)的使用
int main()
{
	// p1现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行
	A* p1 = (A*)operator new(sizeof(A));
	//显示调用构造函数
	new(p1)A(1);//有参数就需要传,这里其实可以传也可以不传,看你实现的构造函数

	p1->~A();//析构可以直接这样用,构造不行;
	//p1->A(); //error C7624: 类型名称"A"不能出现在类成员访问表达式的右侧
	operator delete(p1);
	return 0;
}

六、malloc/free 和 new/delete 的区别

1、共同点

都是从堆上申请空间 ,并且需要用户手动释放

2、不同点

(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++的内存管理就讲解完了,虽然内容比较多,但大部分都只需要了解知道即可,最重要就是熟练掌握 new 以及 delete 操作自定义类型的用法,简单来说就是匹配使用不要胡来。 希望这篇文章对大家学习C++能有所帮助!

C++参考文档:
https://legacy.cplusplus.com/reference/
https://zh.cppreference.com/w/cpp
https://en.cppreference.com/w/

相关推荐
clint4561 小时前
C++进阶(1)——前景提要
c++
夜悊5 小时前
C++代码示例:进制数简单生成工具
c++
郝学胜_神的一滴6 小时前
CMake 021: IF 条件判据详诠
c++·cmake
_wyt00120 小时前
洛谷 B3930 [GESP202312 五级] 烹饪问题 题解
c++·gesp
玖玥拾1 天前
C/C++ 数据结构(七)栈、容器适配器
c语言·数据结构·c++··容器适配器
один but you1 天前
constexpr函数
c++
凡人叶枫1 天前
Effective C++ 条款41:了解隐式接口和编译期多态
java·开发语言·c++·effective c++
凡人叶枫1 天前
Effective C++ 条款42:了解 typename 的双重意义
java·linux·服务器·c++
小胖xiaopangss1 天前
BRpc使用
c++·rpc
-森屿安年-1 天前
63. 不同路径 II
c++·算法·动态规划