本文也是读者朋友面试大疆时的面试真题,据读者反馈,面试官问:构造函数和析构函数可以调用虚函数吗?事后读者朋友向我求助时,我的回答是,当然可以。这样回答绝对正确,但是这不是面试官想考察的点,如上回答可能连一面都过不了。
本题从字面中可以看到涉及到三个函数,分别是:
所以虚函数通常涉及到基类派生类,涉及到重写;同时结合构造函数和析构函数,所以本题中的虚函数调用可细分为如下的8种情况(见下图)。
浅层次的从函数调用的角度看,如上8种情况下都是可以调用虚函数的,但是如果只是这样回答,会让面试官认为知识浅薄,恐怕连一面都过不了。 要回答这个问题,需要知道面试官想要考察什么,此处提到虚函数,必定涉及到多态,那么
只是,子类构造函数、子类析构函数中调用的虚函数如同子类自身的其他普通函数一样,调用的必定是子类中重写的函数。实际到考察的是父类构造函数、父类析构函数中调用的虚函数两种情况下,函数的执行是否符合预期。要回答这两个问题,需要从继承时父类和派生类的构造函数、析构函数的执行顺序,多态的实现原理两个角度回答。
定义子类对象时,会先执行父类的构造函数,再执行子类的构造函数。销毁子类对象时,先执行子类的析构函数,再执行父类的析构函数。也就是说:,父类构造函数执行时,子类对象还未初始化;而调用到父类的析构函数时,对象的子类部分已经被销毁。
对象通过虚表指针(vptr)指向虚函数表(vtable),虚函数表中的函数指针指向虚函数的实现。所以多态的基础是对象持有的虚表指针,而虚表指针是在对象创建时确定的,即构造函数执行时确定的。
综上,当执行父类的构造函数时,对象的虚表指针指向的是父类的虚函数表(此时子类的对象还未初始化),所以父类调用虚函数执行的是父类内的函数;同理;当执行父类的析构函数时,对象的虚表指针指向的是父类的虚函数表(此时子类的对象已经被销毁),所以父类调用虚函数执行的是父类内的函数。所以调用虚函数时执行的都是父类内的函数,而不是子类中重写的函数。所以并不符合多态的预期,那也就没有必要使用虚函数了,也就是说虚函数在构造函数和析构函数中是“失效”的,不建议在构造函数和析构函数中调用虚函数。
如上是枯燥的逻辑分析,仅以如下两个代码示例,验证上述分析。
class Base {
public:
Base() {
log(); // 试图调用虚函数
}
virtual void log() {
cout << "Base::log()" << endl;
}
};
class Derived :public Base {
public:
Derived() {}
void log() override {
cout << "Derived::log()" << endl;
}
};
int main() {
Derived d; // 输出什么?
}
输出结果:
Base::log()
class Base {
public:
virtual ~Base() {
cleanup(); // 析构函数中调用虚函数
}
virtual void cleanup() {
cout << "Base::cleanup()" << endl;
}
};
class Derived :public Base {
public:
~Derived() {}
void cleanup() override {
cout << "Derived::cleanup()" << endl;
}
};
int main() {
Base* p = new Derived();
delete p; // 输出什么?
}
输出结果:
Base::cleanup()
如上从原理、实验都验证了,构造函数、析构函数中虽然可以调用虚函数,但是虚函数“失效”了,所以并不符合多态的预期,没有必要使用虚函数,所以不建议在构造函数和析构函数中调用虚函数。