第三讲 | C/C++内存管理完全手册

发布于:2025-03-25 ⋅ 阅读:(13) ⋅ 点赞:(0)

一、 C/C++内存分布

如图,从C/C++语言角度对内存区域进行划分;若从操作系统角度对内存区域进行划分,会将常量区叫做代码段,会将静态区叫做数据段:

在这里插入图片描述

指针:内存地址按字节为单位的编号。
空指针是有效的地址,是第0个字节的地址,系统默认会把这块地址省去,不用这块地址,所以不能访问空指针(不能对空指针解引用)。

C/C++内存管理认知:需要明确定义的不同变量分别存储在内存的哪个区域。

我们来看下面的一段代码和相关问题:

在这里插入图片描述

在这里插入图片描述

  1. 全局变量、全局/局部静态变量在静态区(数据段)。

数组名含义:

  1. 表示整个数组:sizeof(数组名)、数组名单独存在
  2. 首元素的地址:数组名进行运算,例如,解引用运算*char2表示首元素

num1、char2都表示局部数组,函数栈帧销毁了,它们也跟着销毁。常量字符串在常量区,常量字符串(5个字节)的内容拷贝给char2,函数栈帧销毁与常量字符串无关,函数栈帧销毁后常量字符串还在常量区。在常量区的都不能修改。

  1. 局部变量、局部数组都在函数栈帧中,即在栈上。

  2. const修饰的变量在栈上。但是pChar3所在的一行不涉及const,因为这里const修饰的是*pChar3,与pChar3无关,pChar3与ptr1都是大小为4个字节的局部指针变量,在栈上。pChar3中存储常量字符串的地址,pChar3指向常量字符串,*pChar3 == 'a',字符’a’在常量区。ptr1存储动态开辟空间的起始地址,动态开辟在堆上,即*ptr1在堆上。

  3. 加一题:这里b是常变量,是可以修改b的值的,取b的地址&b,&b是const int* 类型,强制类型转换成int*后解引用修改。打印出a、b的地址,发现是挨着的,也就说明const修饰的变量是在栈上,即b在栈上。

为什么输出结果是挨着得呢?
因为栈区就是从高地址到低地址分配的,一般内存有空间的话,就是按照顺序分配。

#include <iostream>
using namespace std;
int main()
{
	int a = 1;
	const int b = 0;
	cout << &a << endl;
	cout << &b << endl;
	return 0;
}

在这里插入图片描述


栈:向下增长。在栈中定义的变量,后定义的地址会越来越小;后调用的函数地址也是会越来越小。

堆:向上增长。后malloc()出来的地址会比先malloc()出来的地址大。

整个内存中栈、堆需要的空间最大,所以它们统一向中间生长,实际上堆的空间最大,因为动态开辟要预留空间。栈一般不需要太多空间,Linux下一个栈帧默认只开8M(800万Byte),超过就会栈溢出。

1MB = 1024KB = 1024 * 1024 Byte = 10^6Byte

重点学习堆,因为堆是需要手动管理的,而其他内存区域是自动管理的。

在这里插入图片描述

【说明】

  1. 栈又叫堆栈–非静态局部变量/函数参数/返回值等等,栈是向下增长的。
  2. 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享共享内存,做进程间通信。(Linux没学到这块,现在只需要了解一下)
  3. 堆用于程序运行时动态内存分配,堆是可以上增长的。
  4. 数据段–存储全局数据和静态数据。
  5. 代码段–可执行的代码/只读常量。

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

void Test()
{
	int* p2 = (int*)calloc(4, sizeof(int));
	int* p3 = (int*)realloc(p2, sizeof(int) * 10);
	// 这里需要free(p2)吗?
	free(p3);
}

其实realloc()并不是那么的高效,扩容的时候有原地扩容和异地扩容,上面的例子中从16B扩容到40B,若原来空间之后有足够的24B空间,那么就可以原地扩容,此时,p2、p3是一样的,free(p3)就相当于把p2指向的空间也给释放了,所以不用再释放一次,但是p2这时是个野指针;若是原来空间之后没有足够的24B空间(分配给别人了),那么就异地扩容,p3指向新的空间,用完肯定要手动释放这块空间,p2指向的原来的空间会自动释放给操作系统,所以不用手动free()释放,不过这时p2是个野指针。
在这里插入图片描述

【面试题】

  1. malloc/calloc/realloc的区别? https://blog.csdn.net/Future_yzx/article/details/145299472?spm=1001.2014.3001.5506
  2. malloc的实现原理? glibc中malloc实现原理 : https://www.bilibili.com/video/BV117411w7o2/?spm_id_from=333.788.videocard.0&vd_source=dc7a926badd09458939fa8246d82a9e3

三、 C++内存管理方式

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

C、C++内存管理方式区别:

  1. malloc()、free()等是库里的函数
  2. new、delete关键字是操作符

1. new/delete操作内置类型

new默认不会初始化,与malloc()没什么区别。但是语法上new支持初始化,有初始化的方式:

申请和释放单个元素的空间,使用new和delete操作符,初始化用();申请和释放连续的空间,使用new[]和delete[],初始化用{}。注意要匹配起来使用。

void Test()
{
	// 动态申请一个int类型的空间
	int* ptr4 = new int;
	// 动态申请一个int类型的空间并初始化为10
	int* ptr5 = new int(10);
	// 动态申请3个int类型的空间
	int* ptr6 = new int[3];
	// 动态申请10个int类型的空间并初始化
	int* ptr7 = new int[10] { 1, 2, 3, 4 };//与数组一样,后面的6个空间默认初始化为0
	delete ptr4;
	delete ptr5;
	delete[] ptr6;
	delete[] ptr7;
}

在这里插入图片描述

2. new和delete操作自定义类型

new/delete 和 malloc/free在申请内置类型的空间时作用几乎是一样的。

C中malloc()、free()对自定义类型不适用,malloc()出来的对类对象中的成员变量没法初始化。

new/delete 和 malloc/free第一大区别:在申请自定义类型的空间时,如果只是new 类,new会调用默认构造(无默认构造函数会报错); 如果是 new 类(2),这就是在调用非默认构造函数;所以new 的顺序就是,1、 先开辟空间(实际上就是调用operator new函数) 2、 调用构造函数 。不是只能调用默认构造。delete除了会释放空间之外还会调用析构函数。而malloc与free不会调用函数的。

#include <iostream>
using namespace std;
class A 
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A():" << this << endl;
	}
	~A()
	{
		cout << "~A():" << this << endl;
	}
private:
	int _a;
};
int main()
{
	// 第一大区别:开辟自定义类型的空间时new/delete会额外调用默认构造函数/析构函数,而malloc()/free()不会
	A* p1 = (A*)malloc(sizeof(A));
	// 没有对象,new一个。this指针就是对象的地址,即p2
	A* p2 = new A(1);
	free(p1);
	delete p2;

	// 开辟内置类型的空间时几乎是一样的
	int* p3 = (int*)malloc(sizeof(int));
	int* p4 = new int;
	free(p3);
	delete p4;

	int* p5 = (int*)malloc(10 * sizeof(int));
	int* p6 = new int[10];
	free(p5);
	delete[] p6;
	return 0;
}

在这里插入图片描述

new ListNode(1)相当于之前学习初阶数据结构时buyNode()函数的功能:申请结点空间 + 初始化结点。

#include <iostream>
using namespace std;
struct ListNode
{
	int _val;
	ListNode* _next;
	ListNode(int val)
		:_val(val)
		, _next(nullptr)
	{}
};
int main()
{
	ListNode* node1 = new ListNode(1);
	ListNode* node2 = new ListNode(2);
	ListNode* node3 = new ListNode(3);
	ListNode* node4 = new ListNode(4);
	node1->_next = node2;
	node2->_next = node3;
	node3->_next = node4;

	ListNode* cur = node1;
	while (cur)
	{
		cout << cur->_val << " -> ";
		cur = cur->_next;
	}
	cout << "nullptr" << endl;
	return 0;
}

在这里插入图片描述
new/delete 和 malloc/free第二大区别:malloc()与new开辟空间失败的机制不一样及其处理错误的机制不一样,不过一般情况下不会开辟失败,导致开辟空间失败的原因就是空间不够。

malloc()与new开辟空间失败的机制及其处理机制:malloc()会返回空,解决办法就是手动检查。new失败的机制不是返回空指针,而是会抛异常(后续《异常》章节会讲到),解决办法就是在调用链的任意一层有处理就行。

#include <iostream>
using namespace std;
int main()
{
	//手动检查
	int* ptr = (int*)malloc(sizeof(int));
	if (ptr == NULL)
	{
		perror("malloc fail!");
		exit(1);
	}
	return 0;
}

抛出的异常会往捕获异常的方向走,在当前函数没有捕获就会往下一层走。在当前层/外层有捕获就行,若直到main()函数里都没有捕获就会报错,最后main()函数返回值为非0,程序会异常退出。

在这里插入图片描述
32位平台下,堆总共是4G。32位下,指针大小是4个字节,编址成2^32个指针,一个指针就是一个字节的地址,2^32个指针就有2^32个字节,即2^32byte == 4G

1G = 1024MB = 1024 * 1024 KB = 1024 * 1024 * 1024 Byte = 2^30Byte
1024 = 2^10

#include <iostream>
using namespace std;
double Divide(int a, int b)
{
	
	// 当b == 0时抛出异常
	if (b == 0)
	{
		string s("Divide by zero condition!");
		throw s;
	}
	else
	{
		return ((double)a / (double)b);
	}
}
int main()
{
	size_t x = 0;
	int* ptr1 = nullptr;
	try
	{
		do {
			// ptr1 = (int*)malloc(10 * 1024 * 1024);// ()里是字节个数10MB
			// 失败了抛异常
			//ptr1 = new int[500 * 1024 * 1024];
			ptr1 = new int[10 * 1024 * 1024];// []里是对象个数
			if (ptr1)
				//x += 10 * 1024 * 1024;// malloc()的
				x += 10 * 1024 * 1024 * 4;

			cout << ptr1 << endl;
		} while (ptr1);
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}

	cout << x << endl;
	cout << x/(1024*1024) << endl;

	//try {
	//	cout << Divide(10, 0) << endl;
	//}
	//catch (const string& s)
	//{
	//	cout << s << endl;
	//}

	return 0;
}

在这里插入图片描述

四、operator new和operator delete函数(重点)

这两个函数不是对new、delete操作符的重载,它们就是全局的库函数。

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

从汇编的角度看,运算符new、delete本质/核心机制就是分别调用两个函数:new(operator new和构造函数)、delete(析构函数和operator delete):
在这里插入图片描述

new底层为什么不直接调用malloc()呢?而是要通过operator new函数?因为malloc()失败返回的是空,而C++这种面向对象的机制里要求库里面失败了不再用返回空、返回错误码去表达错误,而是要通过异常,那么malloc()失败了返回空就不符合机制,解决办法:用operator new函数对malloc()封装一下就行,这样operator new实际上就是在通过malloc来申请空间。

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

总结:

  1. new 的顺序就是,1、 先开辟空间(在堆上调用operator new函数,实际上就是在堆上调用malloc()函数) 2、 调用构造函数 。
  2. delete的顺序是,1、先调用析构函数(对申请资源的释放) 2、释放空间(在堆上调用operator delete函数,实际上就是在堆上调用free()函数)。
/*
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)
	//若p == 0,malloc失败,_callnewh回调机制尝试进行回调,若没有回调成功就会抛异常,抛bad_alloc
		if (_callnewh(size) == 0)
		{
			// report no memory
			// 如果申请内存失败了,这里会抛出bad_alloc 类型异常
			static const std::bad_alloc nomem;
			_RAISE(nomem);//是宏,相当于throw
		}
	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));
	//operator delete函数核心调用_free_dbg,最后一行宏函数,实际还是调用free()
	_free_dbg(pUserData, pHead->nBlockUse);
	__FINALLY
		_munlock(_HEAP_LOCK); /* release other threads */
	__END_TRY_FINALLY
		return;
}
/*
free的实现
*/
//宏函数,实际还是调用free()
#define free(p) _free_dbg(p, _NORMAL_BLOCK)

operator delete是与operator new配对的。

若先operator delete,会导致找不到堆上的空间了。正确开空间、销毁空间步骤:

在这里插入图片描述

五、new和delete的实现原理

内置类型

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

自定义类型

  • 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[]会多开4个字节,存储数据是20个字节,但是会在头上多开4个字节。多开的4个字节里面会存储对象个数。

通过p4指针往前偏移4个字节,会把这24个字节空间申请出来,把存储数值取出来,就知道了有多少个对象,再对每个对象依次调用析构函数,最后整块空间都会被释放(包括存储对象个数的空间)。但是若把析构函数屏蔽了,空间大小就是20个字节了,编译器认为A默认生成的析构函数啥事都不做,那么多开4个字节存储个数就白存储了,相当于编译器做了优化,就不存储对象个数了。

在这里插入图片描述

new delete、new[] delete[]、malloc() free()各自一定要匹配使用。没有自己写析构。没有匹配使用,但是不会内存泄漏。delete[]是知道要调用多少次析构函数,delete[]调用operator delete[],operator delete[]中调用operator delete,operator delete还是会调用free(),最终是调用free(),free()底层会记录空间有多大 。

若把析构函数注释解开,会报错。原因不是内存泄漏,而是释放的位置不对。因为写了析构,会额外存储对象个数,最后返回的是上图中p4箭头的位置,用delete,而不是用delete[]话,会从这个位置调用一次析构释放,会认为这里只有一个对象,也不会认为前面有4个字节存储对象个数,一个是析构没有调用对,还有一个是析构的位置也不对。不匹配使用是否报错是不一定的,所以,一定要匹配使用。malloc() free()不能分段申请、分段释放。

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

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

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

使用场景:
定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化。

构造函数不可以显示调用,都是自动调用的。析构函数可以显示调用。operator new和operator delete函数可以显示调用,因为是库里面的函数。

#include <iostream>
using namespace std;
class A 
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A():" << this << endl;
	}
	~A()
	{
		cout << "~A():" << this << endl;
	}
private:
	int _a;
};
int main()
{
	// 等价于new
	// 申请空间
	A* p1 = (A*)operator new(sizeof(A));//相当于A* p1 = (A*)malloc(sizeof(A));
	// 构造函数不支持这样显示调用
	//p1->A(1);
	// 定位new表达式显示调用构造函数
	new(p1)A(10);

	// 等价于delete
	p1->~A();
	operator delete(p1);//相当于free(p1);
	return 0;
}

被替换成向内存池申请内存和释放内存时就会这么写。stl_list中就会有这个机制,提高数据结构申请释放内存的效率就建立出内存池的机制。内存池申请的时候就要显示调用构造和析构。

自己去显示的开空间(堆上),但是没有构造函数初始化、析构,构造借助定位new显示调用,析构显示调用

开空间在堆上开,会借助内存池的概念

申请内存、连接、线程需要很多消耗,效率低,那么怎么样才能高效呢?建立一个池子,想要的系统资源放进去,需要的话直接拿。类比妈妈在家中掌管钱财,你每天的开销都要向妈妈要钱,但是每需要开销一次钱都要向妈妈要钱就很麻烦,效率也很低,那么这时你就可以建立自己的池化技术(类比微信钱包或银行卡),妈妈就可以把你这个月的生活费打到这里了,这样每次需要钱的时候就可以向池子里拿,花钱的效率就变高了。
在这里插入图片描述

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

realloc()只能初始化为0,而new初始化就相对灵活。

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

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