c/c++内存管理

C和C++内存管理

(一)C/C++内存分布

我们可以在任务管理器中查看电脑当前进程:

下图是在c语言阶段我们学过的内存区域划分,这一块在C++同样适用:

说明:

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

那我们先根据几个题目来回忆我们c语言中内存如何分配的:

前五个比较简单,大家一定不能出错

第一个:A,main函数外定义的变量都是静态的,生命周期都是从程序开始到结束

第二个:A,全局静态一样

第三个:A,由static修饰的也是全局的

第四个:A,没有static修饰的都是局部的

第五个:A,这里的数组名代表整个数组,也是在局部的栈里存放

后面六个难度上升了很多:

第六个:A,char2也是数组,并且我是我们主动申请的,所以也在栈

第七个:A,*char2是解引用,这里的char2代表的首元素地址,首元素a在栈上

第八个:A, pchar3是一个指针,不管它的const修饰指向的内容还是指针,指针都存于栈

第九个:D,*pchar3也是指向第一个元素的地址,但是内容部分用const修饰了,所以在常量区

第十个:A,ptr1是一个指针

第十一个:B,ptr1所指向的内容使我们主动开辟的,所以在堆区。

(二)C语言动态内存管理

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

这一块在c语言部分讲的很多了,所以我就简单的带大家回忆一下,

malloc:开固定的空间,不初始化。

calloc:开空间,初始化0.

realloc:支持拓展我们原来开的空间。

free:释放我们主动开辟的空间。

【面试题】

  1. malloc/calloc/realloc的区别?
  2. malloc的实现原理? 视频链接:https://www.bilibili.com/video/BV117411w7o2/?spm_id_from=333.788.videocard.0

这一块就这样过了,如果这一块还不懂得,建议回去补一下c语言的课。

(三)c++内存管理

(3.1)new/delete操作内置类型

我们简单得来使用一下new和delete

cpp 复制代码
int main()
{
	int* p1 = new int;
	int* p2 = new int[10];//简化了c语言sizeof与强转部分
	delete p1;
	delete[] p2;

	//初始化
	int* p3 = new int(3);
	int* p4 = new int[10] {0};//全都初始化为0
	int* p5 = new int[10] {1, 2, 3, 4};//前四个值初始化
	delete p3;
	delete[] p4;
	delete[] p5;
	return 0;
}

(3.2)new和delete操作自定义类型

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

int main()
{
	A* p1 = new A;
	A* p2 = new A(1);
	delete p1;
	delete p2;
	return 0;
}

大家可以看到c++这块new与delete会自动调用自定义类型的构造和析构函数。

这一块非常的方便我们来演示一下c++里的链表:

cpp 复制代码
struct ListNode
{
	int _val;
	ListNode* next;

	ListNode(int val) :_val(val),next(nullptr){}
};
//c++的struct里的变量与函数均为共有的,这一部分前面讲过
int main()
{
	ListNode* n1 = new ListNode(1);
	ListNode* n2 = new ListNode(2);
	ListNode* n3 = new ListNode(3);
	ListNode* n4 = new ListNode(4);
	ListNode* n5 = new ListNode(5);
	n1->next = n2;
	n2->next = n3;
	n3->next = n4;
	n4->next = n5;
	return 0;
}

这里是不是相较于c里的初始化啊,插入什么的简单许多。

总之我们只需要记住在c++的使用过程中使用new,delete就行,因为new包含了malloc,delete包含了free。

此外我们在使用new和时是不是与malloc还有一个差别,那就是没有判断是否申请成功,C++引入了抛异常。

抛异常在c++进阶里才会详细的讲解,这里我们简单的演示一下:

cpp 复制代码
int main()
{
	try
	{
		throw try catch
		void* p1 = new char[1024 * 1024 * 1024];
		cout << "p1" << endl;
		void* p2 = new char[1024 * 1024 * 1024];
		cout << "p2" << endl;
		void* p3 = new char[1024 * 1024 * 1024];
		cout << "p3" << endl;
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;//发生了什么错误
	}
	return 0;
}

void func()
{
	//throw try catch
	void* p1 = new char[1024 * 1024 * 1024];
	cout << "p1" << endl;
	void* p2 = new char[1024 * 1024 * 1024];
	cout << "p2" << endl;
	void* p3 = new char[1024 * 1024 * 1024];
	cout << "p3" << endl;
}

int main()
{
	try
	{
		func();
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;//发生了什么错误
	}
	return 0;
}

上面是在main函数里申请失败,下面是在调用的一个函数里失败。

cpp 复制代码
void func()
{
	int n = 0;
	while (1)
	{
		char* p1 = new char[1024 * 1024];//1024*1024byte是1M
		cout << "p1->" << endl;
		n++;
		cout << n << endl;;
	}
}

int main()
{
	try
	{
		func();
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;//发生了什么错误
	}
	return 0;
}

这个函数就是来看在32位环境下究竟能申请多少MB

大家可以看到32位(总共4G)下大概只能申请1.7G左右,所以堆在内存占大部分。

(四)operator new与operator delete函数

new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过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)//malloc
		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)//free

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

(五)new与delete的实现原理

(5.1)内置类型

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

(5.2)自定义类型

(1)new的原理

  1. 调用operator new函数申请空间
  2. 在申请的空间上执行构造函数,完成对象的构造
    (2)delete的原理
  3. 在空间上执行析构函数,完成对象中资源的清理工作
  4. 调用operator delete函数释放对象的空间
    (3)new T[N]的原理
  5. 调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对象空间的申请
  6. 在申请的空间上执行N次构造函数
    (4)delete[]的原理
  7. 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理
  8. 调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释放空间

(5.3)不匹配使用(面试可能会考)

上面第一组是可以的,第二组不行,我们可以清楚的看到第二组的free少调用了析构函数,这里有内存泄漏的风险。

那我们来看一个神奇的东西:

cpp 复制代码
class A
{
public:
	A(int a = 0, int b = 0)
	{
		_a = a;
		_b = 1;
		cout << "A()" << endl;
	}
	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a;
	int _b;
};
class B
{
private:
	int _a;
	int _b;
};
int main()
{
	int* p1 = new int[10];//这里没有调用构造函数,实质上只有malloc
	delete p1;//这里虽然不匹配,但是内存也不会出现泄漏,实质这里只使用free
	free(p1);

	A* p2 = new A[10];
	delete p2;//这里有问题

	B* p3 = new B[10];
	delete p3;//这里没有问题
	return 0;
}

大家自己可以去vs里运行一下,那为什么A有问题,B没有问题呢?

首先我们的A与B大小是不是都是8个字节,那我们都调用了10次,那么10个这样的对象是不是80个字节,但是实际上A的10个对象占了84个字节,B的10个对象占了80字节,多的4个字节是用来存储对象的个数。

那为什么A会多开4个字节存储个数呢?

这是因为B没有析构函数,所以编译器就给优化了,严格的来说多存储的个数是给delete后面的[]使用的,当类没有析构函数时,就认为没有开辟的空间需要释放,只要我们写了析构,编译器就不会再有优化,那B的大小就变为84,。

这里总结就是一定要匹配使用,不要错配。

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

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

使用格式:

new (place_address) type或者new (place_address) type(initializer-list)

place_address必须是一个指针,initializer-list是类型的初始化列表

使用场景:

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

上面什么意思呢?

就是我们可以只开空间,不初始化,也就是malloc的功能。

cpp 复制代码
class A
{
public:
	A(int a = 0, int b = 0)
	{
		_a = a;
		_b = 1;
		cout << "A()" << endl;
	}
	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a;
	int _b;
};
int main()
{
	A* ptr1 = new A;//调用了构造

	A* ptr2 = (A*)operator new(sizeof(A));//不调构造
	new(ptr2)A(1, 0);//定位new
	//下面两步合起来等于上面一步

	delete ptr1;
	
	ptr2->~A();//析构函数是可以显示调用的
	operator delete(ptr2);//下面两步合起来等于上面一步
	return 0;
}

大家可能认为这有点脱了裤子放屁对吧,但我想说的是---说得对,但它也有他自己的用途,在生活中99%的场景用不到,需要用的场景是内存池、线程池,连接池,这一块不讲,只是给大家说下有这个东西。

(七)malloc/free和new/delete的区别(重点)

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

  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在释放空间前会调用析构函数完成空间中资源的清理释放

相关推荐
武昌库里写JAVA22 分钟前
机器学习笔记 - week6 -(十一、机器学习系统的设计)
java·开发语言·算法·spring·log4j
颜淡慕潇38 分钟前
【数据库】Java 中 MongoDB 使用指南:步骤与方法介绍
java·数据库·sql·mongodb
阑梦清川39 分钟前
Java数组使用&练习(完)
java·开发语言
2401_857439691 小时前
Spring Boot在甘肃非遗文化网站开发中的应用
java·spring boot·后端
奋斗★~男孩1 小时前
C++的6种构造函数
c++
发如雪-ty1 小时前
c++11~c++20 结构化绑定
android·c++·c++20
长天一色1 小时前
C语言日志类库 zlog 使用指南(第三章 “Hello World“)
linux·c语言·openeuler
小陈又菜1 小时前
数据结构-栈(理解版)
c语言·数据结构
bossface1 小时前
理解线程库和线程排斥(锁)
linux·运维·服务器·c语言·c++
蓝瑟柳絮1 小时前
学习之什么是生成器
android·java·学习