析构函数为什么是virtual类型的

本文深入探讨了MFC中为何将析构函数设计为虚拟函数的原因,以及这一设计对避免内存泄露的重要性。通过实例分析,解释了析构函数虚拟性如何确保派生类对象在撤销时正确调用基类析构函数,防止内存资源的不当释放。
class CObject
{
public:
// Object model (types, destruction, allocation)

virtual CRuntimeClass* GetRuntimeClass() const;
virtual ~CObject(); //virtual destructors are necessary

...
...
};

为什么MFC的编写者认为virtual destructors are necessary
(虚拟的析构函数是必要的)?

在著名的VC教程 "精通Visual C++ for Windows 95/NT"(电子工业版,
1997年5月版,胡俭,丘宗明等著)第99页中有这样一段话:

“如果CObject的析构函数不是虚拟的,派生类就不会自动地得到虚拟的
析构函数,当对象撤消时就会带来问题——只有当前类的析构函数得到
调用而基类的析构函数就得不到调用。...”

我认为这段解释是这本很不错的书中一个不应出现的严重错误。其意思是说:
若:
class CBase
{
public:
~CBase() { ... };
...
};

class CChild : public CBase
{
public:
~CChild() { ... };
...
};

main()
{
Child c;
...
return 0;
}

上段代码在运行时,当栈框中的自动对象 c 被撤消时,只调用~CChild(),
而不调用~CBase()。

我想但凡对C++继承性理论有所了解的人都会立刻指出这是错误的。

由于在生成CChild对象c时,实际上在调用CChild类的构造函数之前必须首先
调用其基类CBase的构造函数,所以当撤消c时,也会在调用CChild类析构函数
之后,调用CBase类的析构函数(析构函数调用顺序与构造函数相反)。也就是说,

无论析构函数是不是虚函数,派生类对象被撤消时,肯定会依次上调其基类的
析构函数。

那么为什么CObject类要搞一个虚的析构函数呢?

仍以上面代码为例,如果main()中有如下代码:
...

CBase * pBase;
CChild c;
pBase = &c;

...

那么在、当pBase指针被撤消时,调用的是CBase的析构函数还是CChild的呢?
显然是CBase的(静态联编)。但如果把CBase类的析构函数改成virtual型,当
pBase指针被撤消时,就会先调用CChild类构造函数,再调用CBase类构造函数。

在这个例子里,所有对象都存在于栈框中,当离开其所处的作用域时,该对象
会被自动撤消,似乎看不出什么大问题。但是试想,如果CChild类的的构造函数

在堆中分配了内存,而其析构函数又不是virtual型的,那么撤消pBase时,将不

调用CChild::~CChild(), 从而不会释放CChild::CChild()占据的内存,造成内存

泄露。

而将CObject的析构函数设为virtual型,则所有CObject类的派生类的析构函数都

自动变为virtual型,这保证了在任何情况下,不会出现由于析构函数未被调用而
导致
的内存泄露。这才是MFC将CObject::~CObject()设为virtual型的真正原因。
C++中,**析构函数通常被声明为虚函数**,尤其是在涉及**继承和多态**的场景下。这是为了确保在通过**基类指针或引用删除派生类对象**时,能够正确调用派生类的析构函数,避免资源泄漏。以下是详细解释: --- ### **一、为什么需要虚析构函数?** #### **1. 基类指针删除派生类对象的问题** - **场景**:当通过基类指针指向派生类对象,并调用`delete`时: - 如果基类析构函数**不是虚函数**,只会调用基类的析构函数,**派生类的析构函数不会被调用**,导致派生类中分配的资源(如动态内存、文件句柄等)无法释放。 - 如果基类析构函数是虚函数,会通过**虚函数表(vtable)**动态绑定到派生类的析构函数,确保完整清理。 #### **2. 示例对比** ```cpp #include <iostream> using namespace std; class Base { public: ~Base() { cout << "Base destructor" << endl; } // 非虚析构函数 }; class Derived : public Base { public: ~Derived() { cout << "Derived destructor" << endl; } }; int main() { Base* b = new Derived(); delete b; // 仅调用Base::~Base(),Derived::~Derived()不会被调用! return 0; } ``` **输出**(资源泄漏): ``` Base destructor ``` **修改为虚析构函数后**: ```cpp class Base { public: virtual ~Base() { cout << "Base destructor" << endl; } // 虚析构函数 }; class Derived : public Base { public: ~Derived() override { cout << "Derived destructor" << endl; } }; int main() { Base* b = new Derived(); delete b; // 正确调用Derived::~Derived()和Base::~Base() return 0; } ``` **输出**(正确释放): ``` Derived destructor Base destructor ``` --- ### **二、虚析构函数的工作原理** 1. **虚函数表(vtable)**: - 基类声明虚析构函数后,编译器会为类生成虚函数表,其中包含析构函数的地址。 - 派生类重写析构函数时,会更新虚函数表中对应的条目。 2. **动态绑定**: - 当通过基类指针调用`delete`时,编译器会通过虚函数表找到实际对象类型(派生类)的析构函数并调用。 3. **析构顺序**: - 派生类析构函数先执行,释放派生类资源。 - 然后调用基类析构函数,释放基类资源。 --- ### **三、何时不需要虚析构函数?** 1. **类不被继承**: - 如果类明确不会被继承(如使用`final`关键字),析构函数无需声明为虚函数。 2. **不涉及动态内存管理**: - 如果类不管理资源(如仅包含基本类型成员),即使被继承,未释放资源也不会导致问题(但设计上仍建议声明为虚函数)。 --- ### **四、最佳实践** 1. **基类析构函数应声明为虚函数**: ```cpp class Base { public: virtual ~Base() = default; // 显式声明为虚函数 }; ``` 2. **使用`override`关键字(C++11起)**: - 派生类重写析构函数时,建议使用`override`明确意图: ```cpp class Derived : public Base { public: ~Derived() override { /* ... */ } }; ``` 3. **智能指针管理资源**: - 优先使用`std::unique_ptr`或`std::shared_ptr`管理对象,减少手动`delete`的需求: ```cpp std::unique_ptr<Base> b = std::make_unique<Derived>(); // 无需手动delete,智能指针会自动调用正确的析构函数 ``` --- ### **五、相关问题扩展** 1. **纯虚析构函数**: - 如果基类是抽象类(包含纯虚函数),析构函数可以声明为纯虚函数(但需提供实现): ```cpp class AbstractBase { public: virtual ~AbstractBase() = 0; // 纯虚析构函数 }; AbstractBase::~AbstractBase() {} // 必须提供实现 ``` 2. **虚析构函数与性能**: - 虚析构函数会引入少量运行时开销(虚函数表查找),但在现代CPU上影响极小,远低于资源泄漏的风险。 3. **C++11后的改进**: - C++11引入`= default`和`= delete`,可以更灵活地控制析构函数的行为: ```cpp class Base { public: virtual ~Base() = default; // 显式默认虚析构函数 }; ``` --- ### **六、总结** | 场景 | 析构函数类型 | 结果 | |-------------------------------|--------------------|----------------------------------------------------------------------| | 基类指针删除派生类对象 | 非虚析构函数 | 仅调用基类析构函数,派生类资源泄漏 | | 基类指针删除派生类对象 | 虚析构函数 | 正确调用派生类和基类析构函数,资源完整释放 | | 类不被继承 | 非虚析构函数 | 无问题(但设计上建议声明为虚函数) | **关键点**: - **只要类可能被继承,析构函数就应声明为虚函数**。 - 虚析构函数C++多态性的重要组成部分,确保资源安全释放。 - 现代C++中,结合智能指针和`override`关键字可以更安全地管理对象生命周期。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值