C++—22、C++ 中析构函数Destructors及堆和栈回顾

学习笔记,侵权立删!祝愿大美临沂越来越好!

一、析构函数

今天我们要学习的是析构函数,它们有相似之处,构造函数在你创建一个对象实例的时候运行,而析构函数是在你销毁一个对象的时候运行。在任何时候,当一个对象被销毁时,析构函数都会被调用。构造函数通常用在你需要设置变量或者做些初始化工作的时候,同样,析构函数可以用在你释放任何内容或者需要清理内存空间(释放动态分配的内存)。通常情况下,析构函数是由系统自动调用的。当对象的生命周期结束时,系统会自动调用析构函数来释放资源。

析构函数同时适用于栈和堆分配的内存。如果你用new关键字创建一个对象(存在于堆上),然后你调用delete,析构函数就会被调用。如果你只有基于栈的对象,当跳出作用域的时候这个对象会被删除,所以这时候析构函数也会被调用。

二、堆和栈知识回顾

C++中,内存提供了存储数据和程序运行所需的空间。内存分为好几个区域:堆、栈、自由存储区、全局/静态变量存储区和常量存储区。

栈:是由编译器在需要时自动分配,不需要时自动清除的变量存储区。通常存放局部变量、函数参数等。

堆:是由new分配的内存块,由程序员释放(编译器不管),一般一个new与一个delete对应,一个new[]与一个delete[]对应。如果程序员没有释放掉, 资源将由操作系统在程序结束后自动回收。

自由存储区:是由malloc等分配的内存块,和堆十分相似,用free来释放。

全局/静态存储区:全局变量和静态变量被分配到同一块内存中(在C语言中,全局变量又分为初始化的和未初始化的,C++中没有这一区分)。

常量存储区:这是一块特殊存储区,里边存放常量,不允许修改。

(注意:堆和自由存储区其实不过是同一块区域,new底层实现代码中调用了malloc,new可以看成是malloc智能化的高级版本)

在C++中,堆(heap)和栈(stack)是两种不同的内存分配和管理方式。它们在内存分配方式、生命周期和访问方式上等都有显著的区别。如下:

在C++中,堆(Heap)和栈(Stack)是两种不同的内存区域,它们的管理方式、生命周期、存储内容等都有显著区别。理解这两者的区别对于高效编写C++程序和优化性能至关重要。下面我将详细解释堆和栈的区别。

1.、内存分配方式

栈(Stack):

  • 分配方式: 栈内存由编译器自动分配和释放。当一个函数被调用时,函数的局部变量和参数会被压入栈中;当函数返回时,这些变量会自动从栈中弹出。
  • 速度: 由于栈内存的分配和释放是由编译器自动管理的,速度非常快,并且开销很低
  • 分配顺序: 栈内存遵循LIFO(Last In, First Out,后进先出)的原则,变量按声明的相反顺序从栈中弹出。

堆(Heap):

  • 分配方式: 堆内存的分配和释放是由程序员显式控制的,通常通过newdelete关键字(或mallocfree函数)进行分配和释放。堆内存没有固定的分配和释放顺序。
  • 速度: 由于需要动态分配和释放,堆内存的管理速度相对较慢,并且存在内存碎片化的风险。
  • 分配灵活性: 堆内存适合分配大小不确定或生命周期超过函数调用的对象。

2、 存储内容

栈(Stack):

  • 存储内容: 栈内存主要用于存储函数的局部变量、函数参数和返回地址等。由于栈内存的空间有限,通常只用于存储小型的、生命周期较短的数据。
  • 生命周期: 栈中的变量在函数调用期间存在,当函数返回时,这些变量会自动销毁

堆(Heap):

  • 存储内容: 堆内存用于存储动态分配的对象或数据结构,如链表、树、图等。这些对象可以在多个函数之间共享,并且它们的生命周期由程序员控制。
  • 生命周期: 堆中分配的内存不会自动释放,必须由程序员手动释放,否则会导致内存泄漏。

3.、生命周期管理

栈(Stack):

  • 自动管理: 栈内存的管理是自动的,程序员不需要手动管理内存的分配和释放。当函数执行完毕时,栈上的内存自动回收,这减少了内存泄漏的风险。
  • 作用域限制: 栈上的变量只能在其作用域内访问,一旦作用域结束,变量就被销毁。

堆(Heap):

  • 手动管理: 堆内存需要程序员手动管理,即在使用完毕后,必须显式地调用deletefree来释放内存。如果忘记释放,就会导致内存泄漏。
  • 持久性: 堆上的内存可以在程序的整个生命周期内存在,直到被显式释放。

4.、内存大小限制

栈(Stack):

  • 大小限制: 栈的大小通常较小,具体大小由操作系统或编译器决定。在大多数系统中,栈的大小是有限的,通常在几MB到几十MB之间。如果栈上分配的内存超过其限制,可能会导致栈溢出(Stack Overflow)。
  • 局限性: 由于栈的空间有限,通常不适合存储大型数据结构或对象

堆(Heap):

  • 大小限制: 堆的大小仅受限于系统的可用内存,因此堆适合用于分配大块内存。堆的总大小可以达到几GB甚至更多,具体取决于系统的物理内存和操作系统的配置。
  • 适用场景: 当需要存储大量数据或数据结构时,堆内存是合适的选择。

5.、常见问题

栈(Stack):

  • 栈溢出(Stack Overflow): 如果函数调用的层次过深,或在栈上分配了过多的内存(如递归过深,或创建了过大的局部数组),就可能导致栈溢出。

    栈溢出是指程序试图使用超过栈大小的内存,导致程序异常或崩溃的现象

  • 函数返回后悬空指针: 如果在函数内返回了栈上局部变量的地址,函数返回后,该地址可能会指向无效内存,导致悬空指针(Dangling Pointer)问题。

堆(Heap):

  • 内存泄漏(Memory Leak): 如果程序员忘记释放动态分配的内存,内存就会泄漏,长时间运行的程序可能会因内存不足而崩溃。
  • 内存碎片化: 频繁的内存分配和释放可能导致内存碎片化,影响程序的性能。

6.、总结

  • 栈的特点: 栈内存由编译器自动管理,分配速度快,适用于存储局部变量和小型数据结构,但空间有限,适用范围有限。
  • 堆的特点: 堆内存由程序员手动管理,适用于存储大型数据结构和对象,具有较大的空间,但分配速度较慢,容易出现内存泄漏等问题。

以上内容参考:

【面试】解释一下C++中堆和栈的区别_c++堆栈区别面试-优快云博客

什么是堆?什么是栈?他们之间有什么区别和联系? - tolin - 博客园

三、析构函数实例

析构函数的名称与类名相同,但前面有一个波浪号(~),且没有返回值和参数。总的来说构造和析构函数在声明和定义的唯一区别就是放在析构函数前面的这个波形符(~)。我们来看下面这个实例:

#include<iostream>
class Entity
{
public:
	float X,Y;
	Entity()
	{
		X = 0.0f;
		Y = 0.0f;
		std::cout << "Created Entity!" << std::endl;
	}
	
	void Print()
	{
		std::cout << X << "," << Y << std::endl;
	}
	~Entity() 
	{
		std::cout << "Destroyed Entity!" << std::endl;
	}
};

int main()
{
	Entity e;
	e.Print();

		std::cin.get();
}

在上面这个例子中,我们有一个包含两个成员X和Y的类,很明显当我们为这两个浮点数申请内存的时候并没有考虑怎样去清除,暂时先别担心,我们以后会学习内存分配等更多复杂的问题。我们在析构函数中添加了一条信息用来告诉我们这个对象已经被删除,同样构造函数中添加了创建类信息。因为这是栈分配的,我们会看到当main函数执行完的时候析构函数就会被调用。运行结果如下:

发现我们是看不到的,因为程序在那个时候会被直接关掉,也就是main内调用e,实际上是会先创建e,等到main函数执行完后才会销毁,导致我们看不见。所以我们要在这里写一个函数用来做Entity的所有操作。

代码修改如下:

#include<iostream>
class Entity
{
public:
	float X, Y;
	Entity()
	{
		X = 0.0f;
		Y = 0.0f;
		std::cout << "Created Entity!" << std::endl;
	}

	void Print()
	{
		std::cout << X << "," << Y << std::endl;
	}
	~Entity()
	{
		std::cout << "Destroyed Entity!" << std::endl;
	}
};
void Function()
{
	Entity e;
	e.Print();
}

int main()
{
	Function();

		std::cin.get();
}

运行结果如下:

而调用Function()本质上就是Function()被调用,e创建,Function()运行完,就会销毁e。如果是main内调用,实际上会创建e,等到main函数执行完才会销毁e。

我们来调试下程序,设置断点如下:

我们在24行设置了断点,然后按F5调试运行,控制台中没有任何东西,结果如下:

按F10再运行查看,你会看到“Created Entity”,结果如下:

继续按F10,去调用Print函数,你会发现X和Y被打印出来了,其结果如下:

继续F10将到达作用域的底部,跳到函数返回的地方32行代码处,如下图:

因为这个对象是在栈上创建的,所以当作用域结束时它会被自动销毁,调用析构函数,向控制台输出“Destroyed Entity!”,然后回到了函数的返回地址,没啥问题。

所以本质上来说析构函数就是一个在对象销毁时会被调用的特殊的函数和方法。在实际的工作中我们为什么要写析构函数呢?那就是如果你在构造函数中做了一些初始化工作,你可能会想要在析构函数里进行释放或者销毁工作,如果不这么做的话,就可能会造成内存泄漏。一个很好的例子就是堆分配对象,如果你手动在堆上分配了任何类型的内存空间,那么你也要手动地进行清除。如果在使用Entity或者在Entity的构造函数中进行的分配,那么你就要在析构函数中清除它们,因为析构函数调用后,那个Entity对象就不存在了。

你也可以手动调用析构函数,我见过很多人这么做,就是有点奇怪!唯一这样做的原因可能是你使用了new来进行内存分配,当你删除它时,你打算用Free函数之类的东西,然后你还要手动调用析构函数。如下:

在这里我们实际上并没有释放任何资源,只是打印了两次信息“Destroyed Entity!”,所以程序没有崩溃。不推荐这样写!

不要自己调用析构函数,系统会自己调用!

四、归纳总结

类的析构函数是类的一种特殊的成员函数,它会在每次删除所创建的对象时执行。

析构函数的名称与类的名称是完全相同的,只是在前面加了个波浪号(~)作为前缀,它不会返回任何值,也不能带有任何参数。析构函数有助于在跳出程序(比如关闭文件、释放内存等)前释放资源。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Growthofnotes

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值