C&C++内存管理

Posted 蓝乐

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C&C++内存管理相关的知识,希望对你有一定的参考价值。

一.C/C++内存分配

C/C++的内存分布是什么样的呢?C/C++程序中的内存划分为栈、堆、静态区、常量区等区域。


其中, 栈又叫堆栈,存储非静态局部变量/函数参数/返回值等等,栈是向下增长的。内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享内存,做进程间通信(后面会详细介绍)。堆用于程序运行时动态内存分配,堆是可以上增长的。数据段或者说静态区存储全局数据和静态数据。代码段(常量区)中存放可执行的代码/只读常量。
在这里面,以32位系统来说,内核空间有1G左右;剩下的3G中,堆占了大部分空间;相对的,栈区的空间很小,只有几M左右,也因此当递归调用深度太深的时候常会出现栈溢出的现象;其次,由于没有多少数据,数据段和代码段也不是很大。

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

在之前的文章中我们已经学习了C语言的动态内存管理方式,主要是
malloc/calloc/realloc和free构成,可以参考:【C语言】动态内存分配这篇文章。这里不再过多介绍。

三.C++内存管理方式

对于C语言中的动态内存管理方式,C++可以继续使用。同时C++也引入了new和delete两个关键字进行动态内存分配。
接下来我们来介绍一下new/delete和malloc/free之间有什么区别。

1.new/delete处理内置类型

int main()
{
	//malloc向内存申请4个整型大小的空间
	int* p1 = (int*)malloc(sizeof(int) * 4);
	//new向内存申请4个整型大小的空间
	int* p2 = new int[4];
	//free释放掉p1申请的空间
	free(p1);
	p1 = nullptr;
	//delete释放掉p2申请的空间
	delete[] p2;
	return 0;
}


可以看到p1和p2都向堆区申请了16个字节大小的空间,同时申请的内存都没有初始化,好像malloc/free和new/delete没有什么区别似的。确实,对于内置类型,二者的作用完全一样,没有区别;但对于自定义类型,就有所不同了。
其次,需要注意的是,申请和释放连续的空间,使用new[]和delete[];对于内置类型,释放连续的空间时,看起来delete和delete[]没有区别,因为本质上都会调用free去释放这块连续的空间,但对于自定义类型就完全不同了,如果不加以区分程序可能会崩溃。

2.new/delete处理自定义类型

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

int main()
{
	A* p1 = (A*)malloc(sizeof(A));
	A* p2 = new A;
	A* p3 = (A*)malloc(sizeof(A) * 5);
	A* p4 = new A[5];
	free(p1);
	delete p2;
	free(p3);
	delete[] p4;
	return 0;
}


通过调试我们可以发现malloc/free只是向内存申请空间,但是对于自定义类型,并不会调用它的构造函数和析构函数,而new/delete不仅会申请空间,同时还会调用对象的构造函数和析构函数。
其次,如果这里的delete[]改成delete,会发现:编译没有问题,但程序运行最终崩溃了。这是因为对于p4,本来申请了5个对象,但最终只释放了一个对象的空间,也只调用了一个析构函数,出现了内存泄露,因此程序崩溃了。

所以,在使用delete时,一定要记住:如果申请了一块连续的空间,需要使用new[]/delete[]!

四.operator new与operator delete函数

1.概念

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

int main()
{
	//new申请空间失败的处理
	try
	{
		char* p = new char[0x7fffffff];//失败,抛异常,调到捕获异常的位置
		cout << "new success" << endl;
	}
	catch(const exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

operator new:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间失败,尝试执行空间不足应对措施,如果该应对措施用户设置了,则继续申请,否则抛异常。
同样的,delete操作符释放空间也会做两件事:调用自定义类型的析构函数清理空间和调用operator delete函数。

小结一下就是,malloc对于申请内存失败的处理是返回空指针,而new对于申请内存失败的处理是抛异常。

2.operator new 和operator delete的类专属重载

我们可以在一个类中重载这个类的专属operator new和operator delete,提高效率,但实际上这个语法使用的不多,只需了解一下。

struct ListNode
{
	int _date;
	ListNode* _next;
	static int _count;//检测是否有结点没有释放
	ListNode(int date)
		:_date(date)
		,_next(nullptr)
	{
		cout << "ListNode(int date)" << endl;
	}
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
	//重载类ListNode的专属operator new 和 operator delete
	//使用内存池申请和释放内存,提高效率。
	void* operator new(size_t n)
	{
		_count++;
		return :: operator new(n);
	}
	void operator delete(void* p)
	{
		--_count;
		return :: operator delete(p);
	}
};

五.new和delete的实现原理

1.内置类型

在上面的内容中我们已经知道了malloc/free与new/delete在处理内置类型时并没有区别,只是malloc申请空间失败时返回空指针,而new申请空间时是抛异常。

2.自定义类型

· new的原理:1.调用operator new函数申请空间。2.在申请空间上执行构造函数,完成对象的初始化。
· delete的原理:1.在空间上执行析构函数,完成对象中资源的清理工作。2.调用operator delete函数释放空间。
· new T[N]的原理:1.调用operator new[]函数,在operator new[]中实际调用N次operator new函数完成N个对象空间的申请。2.在申请的空间上执行N次构造函数
·delete[]的原理:1.在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理。2. 调用operator delete[]释放空间,实际在operator delete[]中调用N次operator delete来释放空间。
我们用栈作为例子来理解一下new/delete的原理:

struct Stack
{
	int* _a;
	int _top;
	int _capacity;
	Stack(int capacity = 4)
		:_a(new int[capacity])
		,_top(0)
		,_capacity(capacity)
	{
		cout << "Stack(int capacity = 4)" << endl;
	}
	~Stack()
	{
		delete _a;
		_top = _capacity = 0;
		cout << "~Stack()" << endl;
	}
};

int main()
{
	Stack st(5);

	Stack* pst = new Stack[5];
	delete[] pst;
	return 0;
}


在上面的代码中,对于st变量,其定义时调用构造函数位_a申请20个字节的空间,其生命周期结束时调用析构函数释放_a申请的空间。
对于pst,其定义时先用operator new申请一块空间,再调用构造函数对对象初始化,初始化时会对_a申请空间;而delete[] pst时,先调用析构函数完成对象资源的清理,即释放_a申请的空间,然后调用operator delete释放pst申请的空间。
这里也能加深我们对于析构函数完成的不是释放空间的作用而是清理资源的作用。

六.定位new表达式(placement-new)

我们知道malloc申请自定义类型的空间时并不会调用构造函数,那么有没有方法可以令malloc申请的空间调用构造函数呢?这就是定位new表达式的功能了。
定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。
使用格式:

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

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

int main()
{
	//pst现在指向的只不过是与Stack对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行
	Stack* ps = (Stack*)malloc(sizeof(Stack) * 5);
	new (ps) Stack(5);//若Stack类的构造函数没有参数,此处无需传参
	return 0;
}

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

总结

1.malloc/free与new/delete的区别

  1. malloc/free是函数,而new/delete是操作符。
  2. malloc申请空间时需要计算要申请的空间的字节数,而new申请空间只需要知道所申请的类型的个数即可。
  3. malloc的返回值为void*,使用是需要强制类型转换,而new不需要,因为new跟的是空间的类型。
  4. 对于申请内存失败,malloc的处理是返回空指针,而new的处理是抛异常
  5. 对于自定义类型,new/delete会调用其构造/析构函数,而malloc/delete不会。

2.内存泄露

1.内存泄露的概念

内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

比如说我们再堆上申请了空间,但是却没有释放这块申请的空间,也就是说这块空间的使用权没有归还给操作系统,我们不使用,也没有还给系统,别人又用不了,这就是内存泄露。

2.内存泄露的危害

通常来说,一个进程正常结束后,申请的空间便会被内存回收,归还给系统。但是如果是长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。
那么,既然内存泄露的危害如此之大,我们可以怎么解决呢?

3.如何解决内存泄露

一般来说,解决内存泄露问题大致有两种方法:1.事前预防型,比如养成良好的代码风格,申请的内存要记得释放;或者使用智能指针等。2.事后查错型,使用泄露检查工具等等(但要么不靠谱,要么价格过于昂贵)。

以上是关于C&C++内存管理的主要内容,如果未能解决你的问题,请参考以下文章

C&C++内存管理

《c++从0到99》五 C&C++内存管理

《c++从0到99》五 C&C++内存管理

《c++从0到99》五 C&C++内存管理

C++C&C++内存管理

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