C++初阶:内存管理C/C++内存分布及管理方式 | new/delete实现原理及operator new和operator delete函数

Posted 跳动的bit

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++初阶:内存管理C/C++内存分布及管理方式 | new/delete实现原理及operator new和operator delete函数相关的知识,希望对你有一定的参考价值。

文章目录

【写在前面】

在 C 语言中我们也学过内存管理,可转至动态内存管理那些事:malloc、calloc、realloc、free

C/C++ 的内存管理跟 JAVA 这些语言是不同的 —— JAVA 的程序不是直接跑在操作系统上的,JAVA 是在 JVM 虚拟机上运行的;C/C++ 的程序是直接跑在 OS 上的,这也是为什么我们学习 C/C++ 要学习内存管理的原因,所以 C/C++ 的学习者需要对系统了解的更深,而对于系统的知识更多的会在 Linux 系统编程的阶段去学习。

这里我们会学习 new/delete 的使用,以及 new/delete 的底层原理。

一、C/C++内存分布

💦 填空题 && 选择题

int globalVar = 1;
static int staticGlobalVar = 1;
void Test()

	static int staticVar = 1;
 	int localVar = 1;
 	const int localVal1 = 1;
 
	int num1[10] = 1, 2, 3, 4;
 	char char2[] = "abcd";
 	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 存储于
  • localVar1 存储于
  • num1 存储于
  • char2 存储于
  • *char2 存储于
  • pChar3 存储于
  • *pChar3 存储于代码段
  • ptr1 存储于
  • *ptr1 存储于

🔑 填空:

  • sizeof(num1) = 40
  • sizeof(char2) = 5
  • sizeof(pChar3) = 4/8
  • sizeof(ptr1) = 4/8
  • strlen(char2) = 4
  • strlen(pChar3) = 4

📝说明

注意这里比较容易出错的是:

💦 C/C++内存分布示意图


📝说明

栈 ❓

栈又叫堆栈,注意区分数据结构中的栈

函数调用建立栈帧,函数中的参数、局部变量都存在栈帧中

栈是向下增长的,比如 main 函数调用 f 函数:

堆 ❓

注意区分数据结构中的堆

malloc、calloc、realloc 都会在堆上开辟空间

堆是向上增长的,理论上后 malloc 的内存地址比先 malloc 的要大,但是也不一定,因为有可能下一次申请的是之前别人释放回来的:

内存映射段 ❓

内存映射段是高效的 I/O 映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享内存,做进程间通信。

虚拟内存 | 物理内存 ❓

虚拟内存跟物理内存要按页映射,就是对应起来。程序访问虚拟内存,实际要转到对应的物理内存。

内存区域特点 ❗

  1. 这几个区域堆是很大的 —— 你可以认为如果在 32 位下虚拟内存总共占 4G ,内核占 1G,剩下的 3G 空间大部分都是堆的
  2. 从图看,栈和堆差不多大,实际上栈很小 —— Linux下一般只有 8M,所以递归深度太深,很容易导致栈溢出 (Stack overflow)
  3. 数据段和代码段也不是很大 —— 因为没有多少数据 (全局数据 + 静态数据)

🍳拓展

对于内存划分、虚拟内存和物理内存映射可以去看:

《深入理解计算机系统》-> 虚拟存储器

程序员的自我修养》-> 编译链接、动态库等

二、C语言中动态内存管理方式

💦 malloc/calloc/realloc和free

p2 calloc一块空间后,p3又realloc p2,要对p2 free吗 ❓

void Test ()

	int* p1 = (int*) malloc(sizeof(int));
	free(p1);
 	int* p2 = (int*)calloc(4, sizeof (int));
	int* p3 = (int*)realloc(p2, sizeof(int)*10);
	free(p3);

📝说明

这里涉及了 realloc 的开辟原理

所以对于上面程序的写法并不好:

【面试题】malloc/calloc/realloc的区别

malloc -> 开空间

calloc 等价于 malloc + memset(0) -> 开空间 + 初始化

realloc 单独使用时能实现 malloc 的效果 (不会初始化) -> 开空间 | 对 malloc/calloc 的空间扩容

三、C++内存管理方式

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

💦 new/delete操作内置类型

int main()

	//库函数
	int* p1 = (int*)malloc(sizeof(int));
	free(p1);
	
	//操作符/关键字
	int* p2 = new int;
	delete p2;

	return 0;

📝说明

malloc/free 和 new/delete 有什么区别 ❓

  • 如果动态申请的对象是内置类型,那么 malloc 和 free 没有区别
  • 如果动态申请的对象是自定义类型,那么 malloc 和 free 有区别

💦 new和delete操作自定义类型

class A

public:
	A(int a = 0/*int b = 0*/)
		:_a(a)
	
		cout << "A()" << endl;	
	
	~A()
	
		cout << "~A()" << endl;	
	
private:
	int _a;
;
int main()

	A* p3 = (A*)malloc(sizeof(A));
	free(p3);
	
	A* p4 = new A;
	//A* p4 = new A(10);
	//A* p4 = new A(10, 20);
	delete p4;
	
	//数组
	int* p5 = (int*)malloc(sizeof(int) * 10);
	free(p5);

	int* p6 = new int[10];
	delete[]p6;

	A* p7 = new A[10];//调用10次构造
	delete[]p7;//调用10次析构
	
	return 0;

📝说明

  • 对于内置类型 malloc/free 仅仅会开空间/释放空间
  • 对于自定义类型 new/delete 不仅仅会开空间/释放空间,还会调用构造函数和析构函数 —— 调用构造函数时还可以传参,且可以传多个参数

注意我们在 new A 类时不需要默认构造函数;但是在 new A[10] 时则需要默认构造函数

在 C++ 中建议尽量使用 new/delete,因为 malloc/free 能做到的,new/delete 也能做到;new/delete 能做到的,malloc/free 不一定能做到。

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

这种特性有什么用 ❓

struct ListNode

	int _val;
	ListNode* _next;
	ListNode(int val)
		: _val(val)
		, _next(nullptr)
	
;
int main()

	//C
	ListNode* n1 = (ListNode*)malloc(sizeof(ListNode));
	n1->_val = 1;
	n1->_next = nullptr;

	//C++
	ListNode* n2 = new ListNode(1);
	
	return 0;

牛角尖问题 ❓

int* p1 = (int*)malloc(sizeof(int) * 10);
free(p1);
delete p1;

int* p2 = new int;
delete p2;
free(p2);

int* p2 = new int[10];
delete[]p2;
delete[10]p2;
delete p2;//err
free p2;/err

📝说明

注意,一定要匹配使用:malloc ↔ free 、new ↔ delete、new 类型[] ↔ delete[]类型,否则可能会崩溃。

四、operator new与operator delete函数 —— 重点

💦 operator new与operator delete函数

new 和 delete 是用户进行动态内存申请和释放的操作符,operator new 和 operator delete 严格来说不是 new 和 delete 的重载 (名字确实容易误导),而是系统提供的全局库函数,new 在底层调用 operator new 全局函数来申请空间,delete 在底层通过 operator delete 全局函数来释放空间。

new A:

  1. 申请内存 —— 调用 operator new
  2. 构造函数

delete A:

  1. 析构函数
  2. 释放内存 —— 调用 operator delete

new 的反汇编 ❗

❓ 为什么要去调用 operator new 而不是调用其它的呢 ❔

int main()

	//malloc失败返回空
	char* p1 = (char*)malloc(0xffffffff);
	if (p1 == NULL)
	
		printf("malloc fail\\n");
	
	else
	
		printf("malloc success:%p\\n",p1);
	
	//new失败抛异常
	char* p2 = new char[0x7fffffff];
	if (p1 == NULL)
	
		printf("malloc fail\\n");
	
	else
	
		printf("malloc success:%p\\n", p1);
		
	return 0;

📝说明

因为 new 和 malloc 它们失败时,处理的方式不一样

malloc 失败了,这里的检查起作用了

new 失败了,这里的检查没起作用,还引发了一个崩溃 —— 抛异常后没有解决

抛异常 ❓

int main()

	try
	
		char* p2 = new char[0x7fffffff];//出错,抛异常,它会跳到捕获异常的位置
	
	catch(const exception& e)
	
		cout << e.what() << endl;	
	
	return 0;

📝说明


这里就可以看到 new 和 malloc 处理的方式不一样,毕竟一个是面向过程,一个是面向对象

关于什么是异常后面我们会具体学习

所以再看 new 的底层的实现,new 的底层申请内存时是不能让 malloc 去完成的,因为 malloc 失败就直接返回空了,就无法达到让它失败后抛异常的机制,所以其中就产生了 operator new

对于 delete 就不存在失败了抛异常,我们说 malloc 会失败,但没有说 free 会失败 (free 的失败是对越界的空间 free 等),free 失败就不是说抛异常或返回空这样的概念了,这种是属于比较严重的错误,它是直接中止掉程序

operator new 和 operator delete 源码 ❗

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

operator new 就是对 malloc 的封装,目的就是如果申请内存失败了抛异常:

  • new = 封装 malloc + 失败抛异常 + 调用构造函数

operator delete 就是对 free 的封装,目的主要还是和 operator new 对应起来

  • delete = 调用析构函数 + operator delete

这里具体后面也会学习

直接使用 operator new 和 operator delete ❓

class A

public:
	A(int a = 0)
		: _a(a)
	
		cout << "A()" << endl;
	
	~A()
	
		cout << "~A()" << endl;
	
private:
	int _a;
;
int main()

	//调用构造和析构
	A* p1 = new A;
	delete p1;

	//不会调用构造和析构
	A*p2 = (A*)operator new(sizeof(A));
	operator delete(p2);
	
	return 0;

📝说明

我们直接使用 operator new 和 operator delete 本质上和 malloc 和 free 没有区别

所以平时我们也几乎不会用 operator new 和 operator delete

💦 operator new与operator delete的类专属重载

检测程序有没有 ListNode 的节点有没有释放,如果存在没有释放的节点,请说明是哪里没有释放 ❓

🔑移除链表元素

//重载一个类ListNode,专属的operator new: 
struct ListNode

	int val;
	struct ListNode* next;

	static int _count;//统计

	ListNode(int x)
		: val(x)
		, next(nullptr)
	
	//new _count就++,delete _count就--
	void* operator new(size_t n)
	
		++_count;
		return ::operator new(n);//::代表是全局的
	
	void operator delete(void* p)
	
		--_count;
		return ::operator delete(p);//::代表是全局的	
	
;

int ListNode::_count = 0;
struct ListNode* removeElements(struct ListNode* head, int val)

	struct ListNode* prev = NULL, *cur = head;
	while(cur)
	
		if(cur->val == val)
		
			prev->next = cur->next;
			delete cur;
			cur = prev->next;
			
		else
		
			prev = cur;
			cur = prev->next;	
		
	
	return head;

int main()

	ListNode* n1 = new ListNode(1);
	ListNode* n2 = new ListNode(2);
	ListNode* n3 = new ListNode(2);
	ListNode* n4 = new ListNode(3);
	ListNode* n5 = new ListNode(4);
	ListNode* n6 = new ListNode(2);
	n1->next = n2;
	n2->next = n3;
	n3->next = n4;
	n4->next = n5;
	n5->next = n6;
	n6->next = nullptr;

	ListNode* list = removeElements(n1, 2);
	cout << "没有释放的节点数量:" << ListNode::_count << endl;
	
	return 0;

📝说明

当 new ListNode 时,那么申请空间就会调用专属的 operator new 和 operator delete

了解一下即可,这个语法实际中价值不大

这里想看具体哪里没有释放那就比较复杂了,后面学了 map 后就可以行号给记录下来

五、new和delete的实现原理

💦 内置类型

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

💦 自定义类型

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

场景使用 ❓

class Stack

public:
	Stack(int capacity = 4)
		: _a(new int[capacity])
		, _size(0)
		, _capacity(capacity)
	
		cout << "Stack(int capacity = 4)" << endl;
	
	~Stack()
	
		delete[] _a;
		_size = _capacity = 0;	
		cout << "~Stack()" << endl;	 
	
private:
	int* _a;
	int _size;
	int _capacity;
;
int main()

	//1
	Stack st;

	//2
	Stack* ps = new Stack;
	delete ps;
	
	return 0;

📝说明:

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

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

使用格式:

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

如果想对 malloc 开辟的已有的一块空间去调用构造函数 ❓

struct ListNode

	int val;
	struct ListNode* next;
	ListNode(int x)
		: val(x)
		, next(nullptr)
	
		cout << "ListNode(int x)" << endl;
	
	~ListNode()
	
		cout << "~ListNode()" << endl;	
	
;
int main()

	//实例化一个对象构造函数、析构函数自动调用
	ListNode node(1C++初阶C&C++内存管理

C++模板初阶 | 内存管理

C++初阶第七篇——C/C++的内存管理(C/C++动态内存分布+new和delete的用法和实现原理)

C / C++ 内存管理

C++初阶---内存管理

C++内存管理