虚析构函数问题:为什么要将基类的的析构函数设成虚的?

本文探讨了C++中析构函数为何需要声明为虚函数,尤其是在多态上下文中。通过具体实例,解释了若基类的析构函数不是虚函数时可能引发的内存泄漏和异常问题,并给出了正确的解决方案。

 

优快云网友问:

class A
{
public:
   ~A() 
   { 
      cout < <"A::~A" < <endl; 
   }
};


class B:public A
{
public: 
   virtual ~B() 
   { 
      cout < <"B::~B" < <endl; 
   }
};


class C:public B
{
public: 
   ~C() 
   { 
      cout < <"C::~C" < <endl; 
   }
};

 
int main()

   A *a=new A(); 
   B *b=new B(); 
   C *c=new C(); 
   A *d=new B(); 
   A *e=new C(); 
   B *f=new C(); 

   delete a; 
   cout < <endl; 
   delete b; 
   cout < <endl; 
   delete c; 
   cout < <endl; 
   delete d; 
   cout < <endl; 
   delete e; 
   cout < <endl; 
   delete f; 
   cout < <endl; 

   system("Pause");
}
这段程序运行时有错,当时考的时候题目直接说写出运行结果,我就稀里糊涂得写下来,回来编下发现有错,请教下错在哪,最好告诉我为什么?

 

  

玄机逸士回答:

1. 一般来说,如果一个类要被另外一个类继承,而且用其指针指向其子类对象时,如题目中的A* d = new B();(假定A是基类,B是从A继承而来的派生类),那么其(A)析构函数必须是虚的,否则在delete d时,B类的析构函数将不会被调用,因而会产生内存泄漏和异常;

2. 在构造一个类的对象时,先构造其基类子对象,即调用其基类的构造函数,然后调用本类的构造函数;销毁对象时,先调用本类的析构函数,然后再调用其基类的构造函数;

3. 题目给出的代码是可以编译的,但会出现运行时错误。错误出现在delete d;这一句。为讨论方便,我们不妨将A类和B类改写如下:

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

class B : public A
{
public:
   
int b;
   
virtual ~B()
    {
        cout <<
"B::~B" << endl;
    }
};

那么A* d = new B();这一句的左边所产生B的对象的内存结构如下:

A对象的内存结构如下:

可见d只能找到aA类的析构函数,而无法找到B对象的析构函数,所以当delete d;的时候,B对象所占用的内存就此被泄漏掉了,也从而产生了异常。

 

如果将 A 类的析构函数设为虚的,那么 A 类对象的内存结构将为:

 

B类对象的内存结构为:

此时通过A* d = new B();A对象的内存结构中的vfptr,即虚函数表指针,就是B对象的vfptr(B对象的vfptrbitwise copy,即浅拷贝到A对象的vfptr。如B是从A虚继承而来的,则需要加一个offset,情况要更复杂,见

http://blog.youkuaiyun.com/pathuang68/archive/2009/04/24/4105902.aspx),因此,A对象的vfptr所指向的是B对象的虚函数表,而B的析构函数位于书函数表0的位置,因此,这样就可以通过A类对象的指针d,找到B类对象的析构函数,从而在delete d;时,可以销毁B对象,而不会产生内存泄漏和异常。


事实上,该题目只要将A中的析构函数设成虚的,B类中的析构函数前面的virtual关键字不管是否存在,其析构函数也一定是虚的,C类同此道理。因此,得到结论就是,只要能够保证继承关系中最高的基类的析构函数是虚的,那么就不会产生前面所谈及的问题。这就是为什么在想使用多态特性的时候,需要将基类的析构函数设成虚的真正原因。

虚析构函数在C++中用于确保当通过基类指针删除派生类对象时,能够正确调用派生类的析构函数。如果没有虚析构函数,只有基类析构函数会被调用,而派生类的析构函数将被忽略,这可能导致资源泄漏或未定义行为。 ### 虚析构函数的作用 1. **确保正确的析构顺序**:当使用多态方式(通过基类指针)删除派生类对象时,虚析构函数确保派生类的析构函数先被调用,然后是基类析构函数。 2. **避免资源泄漏**:如果派生类中包含需要显式释放的资源(如动态分配的内存),虚析构函数可以确保这些资源被正确释放。 ### 示例代码 ```cpp #include <iostream> #include <string> class Base { public: Base() { std::cout << "Base constructor called." << std::endl; } virtual ~Base() { std::cout << "Base destructor called." << std::endl; } // 虚析构函数 }; class Derived : public Base { public: Derived() { std::cout << "Derived constructor called." << std::endl; } ~Derived() override { std::cout << "Derived destructor called." << std::endl; } }; int main() { Base* obj = new Derived(); delete obj; // 调用虚析构函数,确保 Derived 的析构函数被调用 return 0; } ``` ### 解释 - **虚析构函数**:`Base` 类中的 `~Base()` 被声明为虚函数,这意味着即使通过基类指针删除对象,也会调用派生类的析构函数。 - **输出结果**: ``` Base constructor called. Derived constructor called. Derived destructor called. Base destructor called. ``` 这表明在删除对象时,派生类和基类析构函数都被正确调用了。 ### 虚析构函数与纯虚函数的关系 虚析构函数和纯虚函数都是实现多态性的重要工具,但它们有不同的用途: - **虚析构函数**:主要用于确保通过基类指针删除派生类对象时,能够正确调用派生类的析构函数。 - **纯虚函数**:用于定义接口,要求派生类必须实现该函数。 虽然纯虚函数通常出现在抽象类中,但抽象类的析构函数可以是普通的虚函数,也可以是纯虚函数。如果是纯虚析构函数,它仍然需要提供一个定义(通常是空实现),以便派生类可以正确调用它。 ### 示例代码(纯虚析构函数) ```cpp #include <iostream> class AbstractBase { public: virtual ~AbstractBase() = 0; // 纯虚析构函数 }; // 必须为纯虚析构函数提供定义 AbstractBase::~AbstractBase() {} class ConcreteDerived : public AbstractBase { public: ~ConcreteDerived() override { std::cout << "ConcreteDerived destructor called." << std::endl; } }; int main() { AbstractBase* obj = new ConcreteDerived(); delete obj; return 0; } ``` ### 解释 - **纯虚析构函数**:`AbstractBase` 类的析构函数被声明为纯虚函数,但它仍然需要提供一个定义(即使是空实现),以确保派生类的析构函数能够被正确调用。 ### 结论 虚析构函数确保通过基类指针删除派生类对象时,能够正确调用派生类的析构函数,从而避免资源泄漏。纯虚析构函数可以用于抽象类,但它仍然需要提供一个定义。
评论 17
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值