析构函数、虚函数、虚析构函数和内存泄漏

析构函数和虚函数和虚析构函数

什么是析构函数?

在对象结束完其生命周期后,系统会自动执行其析构函数。

例如:在建立对象时使用new为其开辟了一片内存空间,然后使用delete时会自动调用析构函数释放内存。

取名一般是类名前面加上位取反符(即~),以区分构造函数。

析构函数不带任何参数,也没有返回值,只有一个析构函数,且不能重载。

如果用户没有编写析构函数,那么系统则会自动生成一个缺省的析构函数,这个缺省的析构函数不会进行任何操作。

(即使自定义了析构函数,编译器几乎也总是会为我们合成一个析构函数,并且如果自定义了析构函数,编译器在执行时会先调用自定义的析构函数再调用合成的析构函数)

 

代码示例

复制代码
class Test
{
public:
    ~Test() { cout << "Test析构函数被调用。" << endl; }
};
int main()
{
    Test *test = new Test();
    delete test;
    return 0;
}
复制代码

输出结果为

Test析构函数被调用

 

什么是虚函数?

粗暴的讲,被virtual关键字修饰的成员函数,就是虚函数。

它可以在一个或者多个派生类中重新进行定义,通过指向派生类的基类指针或引用,访问派生类中同名覆盖成员函数,实现多态性。

 

代码示例

复制代码
class Animal
{
public:
    void talk() { cout << "Animal叫出了声,“NULL!”。" << endl; }
};
class Cat :public Animal
{
public:
    void talk() { cout << "Cat叫出了声,“喵!”。" << endl; }

};
class Dog :public Animal
{
public:
    void talk() { cout << "Dog叫出了声,“汪!”。" << endl; }

};
int main()
{
  Cat cat;
  Dog dog;
  Animal *animal_A = &cat;
  Animal *animal_B = &dog;
  animal_A->talk();
  animal_B->talk();
  return 0;
}
复制代码

输出结果为

Animal叫出了声,“NULL!

Animal叫出了声,“NULL!

 

更改基类talk函数为虚函数

 

代码示例

class Animal
{
public:
    virtual void talk() { cout << "Animal叫出了声,“NULL!”。" << endl; }
};

输出结果为

Cat叫出了声,“喵!”。

Dog叫出了声,“汪!”。

 

虚函数如何运作?

编译器处理虚函数的方法是:为每个类对象添加一个隐藏成员,隐藏成员中保存了一个指向函数地址数组的指针,称为虚表指针(vptr),这种数组成为虚函数表(virtual function table, vtbl),即每个类使用一个虚函数表,每个类对象用一个虚表指针。

因此在使用虚函数后,对象将增加一个存储地址的空间(32位系统为4字节,64位为8字节)。每个类编译器都创建一个虚函数地址表,对每个函数调用都需要增加在表中查找地址的操作。在使用虚函数时要注意内存开销问题。

 

什么是虚析构函数?

虚析构函数其实就是普通的析构函数变成了虚函数。它最主要的作用是防止内存泄露。

 

代码示例

复制代码
class Enemy
{
public:
    ~Enemy() { cout << "怪物部件删除。" << endl; }
};
class Goblin :public Enemy
{
public:
    ~Goblin() { cout << "哥布林死掉了。" << endl; }
};
int main()
{
    Enemy *enemy_A = new Goblin();
    delete enemy_A;
        return 0;
}
复制代码

输出结果为

怪物部件删除。

 

通过输出结果我们会发现,没有调动子类Goblin类的析构函数。

一般来说析构函数里面会用来释放内存,而不被调用则意味着对象没有正常这些析构函数,甚至可能意味着没有正常删除,这代表会产生内存泄露。如果在析构函数中写了其他功能的话,那些功能将也不会正常作用。

于是这个时候我们可以尝试使用虚析构函数。

 

代码示例

class Enemy
{
public:
    virtual ~Enemy() { cout << "怪物部件删除。" << endl; }
};

输出结果为

哥布林死掉了。

怪物部件删除。

 

工作情况为派生类的析构函数最先被调用,然后基类的析构函数被调用。

类析构顺序:派生类本身的析构函数->对象成员的析构函数->基类析构函数

这样派生类的析构函数便正常运作了。

 

不要滥用虚析构函数

因此通过上面的例子我们可以得知:使用虚析构函数是为了当用一个基类的指针删除一个派生类的对象时,派生类的析构函数能被正常调用。但这并不意味着可以滥用虚析构函数,使用虚函数的同时,编译器会给类添加一个虚函数表,里面来存放虚函数指针,这样就会增加类的存储空间,增加开销。当这个类不准备作为基类使用时,使用虚析构函数一般是个坏主意。

 

额外的。

什么是内存泄露?

内存泄漏(Memory Leak)是指你向系统申请分配内存进行使用(new/malloc),然后系统在堆内存中给这个对象申请一块内存空间,但当我们使用完了却没有归系统(delete),导致这个不使用的对象一直占据内存单元,造成系统将不能再把它分配给需要的程序,导致程序运行速度减慢甚至系统崩溃等严重后果。
内存泄漏缺陷具有隐蔽性、积累性的特征,比其他内存非法访问错误更难检测。因为内存泄漏的产生原因是内存块未被释放,属于遗漏型缺陷而不是过错型缺陷。但内存泄漏通常不会直接产生可观察的错误症状,而是逐渐积累,降低系统整体性能,尤其是常发性内存泄漏,发生内存泄漏的代码会被多次执行到,每次被执行时都会导致一块内存泄漏,可能导致内存溢出,最后使系统崩溃。
 
posted @   anesu  阅读(257)  评论(0编辑  收藏  举报
编辑推荐:
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· 写一个简单的SQL生成工具
点击右上角即可分享
微信分享提示