简单总结就是:构造函数不可以是虚函数,而析构函数可以且常常是虚函数。
构造函数不能是虚函数
1.从vptr角度解释
虚函数的调用是通过虚函数表来查找的,而虚函数表由类的实例化对象的vptr指针(vptr可以参考C++的虚函数表指针vptr)指向,该指针存放在对象的内部空间中,需要调用构造函数完成初始化。如果构造函数是虚函数,那么调用构造函数就需要去找vptr,但此时vptr还没有初始化!
2.从多态角度解释
虚函数主要是实现多态,在运行时才可以明确调用对象,根据传入的对象类型来调用函数,例如通过父类的指针或者引用来调用它的时候可以变成调用子类的那个成员函数。而构造函数是在创建对象时自己主动调用的,不可能通过父类的指针或者引用去调用,因此使用虚函数也没有实际意义。并且构造函数的作用是提供初始化,在对象生命期仅仅运行一次,不是对象的动态行为,没有必要成为虚函数。
析构函数可以且常常是虚函数
此时 vtable 已经初始化了,完全可以把析构函数放在虚函数表里面来调用。
C++类有继承时,基类的析构函数必须为虚函数。如果不是虚函数,则使用时可能存在内存泄漏的问题。
假设我们有这样一种继承关系:
如果我们以这种方式创建对象:
SubClass* pObj = new SubClass();
delete pObj;
不管析构函数是否是虚函数(即是否加virtual关键词),delete时基类和子类都会被释放;
如果我们要实现多态,令基类指针指向子类,即以这种方式创建对象:
BaseClass* pObj = new SubClass();
delete pObj;
若析构函数是虚函数(即加上virtual关键词),delete时基类和子类都会被释放;
若析构函数不是虚函数(即不加virtual关键词),delete时只释放基类,不释放子类,会造成内存泄漏问题。
构造函数或者析构函数中调用虚函数会怎样
简要结论:
- 从语法上讲,调用完全没有问题。
- 但是从效果上看,往往不能达到需要的目的。
- 如果在基类中声明了纯虚函数,并且在基类的析构函数中调用之,编译器会发生错误。
Effective 的解释是:
- 由于类的构造次序是由基类到派生类,所以在构造函数中调用虚函数,派生类还没有完全构造,虚函数是不会呈现出多态的。
- 类的析构是从派生类到基类,当调用继承层次中某一层次的类的析构函数时意味着其派生类部分已经析构掉,所以也不会呈现多态。
例子:
#include <iostream>
#include <string>
using namespace std;
class BaseClass {
public:
BaseClass() {
std::cout << "初始化父类,地址:" << this << std::endl;
std::cout << "父类虚函数表地址:" << *((intptr_t **) (this)) << std::endl;
showme();
}
virtual void showme() {
std::cout << "==BaseClass::shome()==" << std::endl;
};
};
class DeriveClass : public BaseClass {
public:
DeriveClass() {
std::cout << "初始化子类,地址:" << this << std::endl;
std::cout << "子类虚函数表地址:" << *((intptr_t **) (this)) << std::endl;
}
virtual void showme() {
std::cout << "直接调用BaseClass::showme() --> ";
BaseClass::showme();
std::cout << "==DeriveClass::shome()==" << std::endl;
}
};
int main() {
DeriveClass dc = DeriveClass();
dc->showme();
return 0;
}
执行结果:
初始化父类,地址:0x7ffee0673780
父类虚函数表地址:0x10f58e080
==BaseClass::shome()==
初始化子类,地址:0x7ffee0673780
子类虚函数表地址:0x10f58e040
直接调用BaseClass::showme() --> ==BaseClass::shome()==
==DeriveClass::shome()==
在基类的构造函数中,调用了showme()的虚函数。但是我们创建的是DeriveClass这个派生类,在创建派生类之前会首先调用基类的构造函数来创建基类的数据,如果此时进入了基类构造函数中,那么当前对象指向的虚函数表地址为基类的虚函数表,并非派生类的虚函数表,从上面的执行结果就可以看出,当初始化好基类的构造函数之后,回到派生类的构造函数中时,获取的虚函数表地址已经变为了派生类的虚函数表地址,已经非基类的虚函数表地址。
派生类与基类的构造函数初始化时,虚函数表是不一样的,意味着构造函数中调用虚函数是当前类的虚函数,无法多态调用。即使允许多态调用,如果在基类中调用派生类的虚函数,由于派生类还未开始初始化,如果访问了还未初始化的数据,那就有很大的问题了。
内联函数不能是虚函数
inline是在编译器将函数类容替换到函数调用处,是静态编译的。而虚函数是动态调用的,在编译器并不知道需要调用的是父 类还是子类的虚函数,所以不能够inline声明展开,所以编译器会忽略
static成员函数不能是虚函数
静态成员函数没有this指针。虚函数依靠vptr和vtable来处理。vptr是一个指针,在类的构造函数中创建生成,并且只能用this指针来访问它。对于静态成员函数,它没有this指针,所以无法访问vptr. 这就是为何static函数不能为virtual.