● 在创建一个派生类对象时我们会首先调用基类的构造函数, 然后调用派生类的构造函数, 一般情况下, 在使用虚函数的时候,我们一般会把基类的指针指向派生类对象, 那么我们删除指向派生类对象的指针时会发生什么情况呢?
下面看一个程序代码示例:
class Base
{
public:
~Base()
{
cout << "基类中的析构函数被调用!" << endl;
}
Base()
{
cout << "基类的构造函数被调用!" << endl;
}
virtual void func()
{
cout << "基类中的func函数被调用!" << endl;
}
};
class Derived :public Base
{
public:
Derived()
{
cout << "派生类的构造函数被调用!" << endl;
m_pointer = new int[20];
}
~Derived()
{
cout << "派生类的析构函数被调用" << endl;
delete[] m_pointer;
}
virtual void func()
{
cout << "派生类中的func函数被调用!" << endl;
}
private:
int *m_pointer;
};
int main()
{
{
//重点在这段代码中
Base *pp = new Derived;
pp->func();
pp->Base::func();
delete pp;
}
{
cout << endl;
Derived myDerived;
Base *ppp = &myDerived;
ppp->func();
ppp->Base::func();
}
{
cout << endl;
Derived d;
Base &b = d;
b.func();
b.Base::func();
}
system("pause");
return 0;
}
输出结果为:
基类的构造函数被调用!
派生类的构造函数被调用!
派生类中的func函数被调用!
基类中的func函数被调用!
基类中的析构函数被调用!
基类的构造函数被调用!
派生类的构造函数被调用!
派生类中的func函数被调用!
基类中的func函数被调用!
派生类的析构函数被调用
基类中的析构函数被调用!
基类的构造函数被调用!
派生类的构造函数被调用!
派生类中的func函数被调用!
基类中的func函数被调用!
派生类的析构函数被调用
在该示例中, 基类的析构函数不是虚函数, 从输出结果可以看出, 在主函数中第一个 { } 中,我们通过基类指针删除派生类对象时调用的是基类的析构函数, 派生类的析构函数并没有被执行。 因此派生类对象中动态分配的内存空间没有得到释放, 造成了内存泄漏。
也就是说, 派生类对象成员 m_pointer 所指向的内存空间在对象消失后既不能被本程序继续使用, 也没有被释放。
对于内存的需求量大、长期连续运行的程序来说, 如果持续发生这样的错误是很危险的, 最终将导致因内存不足而引起的程序异常。
那为什么派生类的对象没有被销毁?
所有问题都源于该对象是在堆上动态创建的, 程序在销毁那两个对象时都调用了错误的析构函数。
出现这种情况的原因是析构函数的链接是在编译时静态解析的。 对自动创建的对象而言,这样做没有任何问题,编译器知道它们是什么, 也能够安排调用正确的析构函数—— 比如说 上面程序主函数中的 第二个 { } 和 第三个 { } 中的代码,它们都正确的调用了虚函数, 从输出结果中,可以看出, 当该对象销毁时, 基类的析构函数 和 派生类的析构函数都被调用了。
但对于动态创建并通过指针访问的对象来说, 情况就不同了。 当执行delete 操作时, 编译器知道的唯一信息是该指针的类型“ 指向基类的指针”。 编译器不知道该指针实际指向的对象类型, 因为这是程序执行时确定的。 因此, 编译器只能确保使 delete 操作调用基类的析构函数。 在实际的应用程序中,会造成严重的内存泄漏, 解决方式很简单, 只需要在程序执行时动态解析对析构函数的调用。 通过在类中使用虚析构函数, 就可以让编译器使用这样的解析方式。
不过要注意的是: 只要是 在堆上创建派生类对象,并且用基类指针指向它,如果基类中的析构函数不是虚函数, 当我们delete 基类指针时, 只会调用基类的析构函数, 派生类的析构函数不会被调用。 跟 基类中是否有其它虚函数没有关系 。
所以说 虚析构函数是为了解决这样的一个问题: 在堆上创建派生类对象,并且用基类指针指向它, 然后并用基类的指针删除派生类对象, 此时即调用派生类析构函数 和 基类的析构函数。
如果某个类不包含虚函数, 那么一般是表示它将不作为一个基类来使用。 当一个类不作为基类使用时, 使析构函数为虚不是一个好主意。 因为它会为类增加一个虚函数表, 使的对象的体积翻倍, 还有可能降低其可移植性。 所以基本的一条原则是: 无故的声明虚析构函数 和永远不去声明一样是错误的。
那么只要我们把上面的程序的基类中的析构函数加上 virtual ,那么当 delete 基类指针时。那么系统就会获得对象运行时的类型并调用正确的析构函数。 看下图显示:
从输出结果可以看出, 当我们在基类中的析构函数加上virtual 时, delete 基类指针时, 首先调用的是派生类的析构函数, 然后基类析构函数。
在使用虚析构函数时,要注意以下几点:
只要基类的析构函数被声明为虚函数, 则派生类的析构函数, 无论是否使用virtual 关键字进行声明, 都自动成为虚函数。
一个虚函数无论被继承多少次, 仍然保持其虚函数的特性, 与继承的次数无关。
如果基类的析构函数为虚函数, 则当派生类未定义析构函数时, 编译器自动生成的析构函数也为虚函数。
delete 与析构函数一起工作, 当使用delete 删除一个对象或者指针时, 也会默认地调用析构函数。 如果该析构函数是虚函数, 那么这个调用的过程将是动态的。
构造函数不能声明为虚函数, 这是因为在执行构造函数时类对象还未完成建立, 当然谈不上函数与类对象的关联。
建议:
当使用继承时, 总是将基类中的析构函数声明为虚函数是个好主意。 这将使所有派生类的析构函数自动成为虚函数。 这样, 如果程序中 delete 运算符 删除的对象是指向派生类对象的基类指针, 系统就会调用相应类的析构函数; 否则系统只会执行基类的析构函数, 而不执行派生类的析构函数, 从而导致异常。
所以专业编程人员一般都习惯声明为虚析构函数, 即使基类并不需要使用虚析构函数, 也显示地定义一个函数体为空的虚析构函数, 以保证在撤销动态存储空间时能够得到正确的处理。