阅读本文之前,读者需要掌握 C++ 虚函数的基本用法,以及了解 C++ 的虚函数是怎么实现的,此为基础内容,不在本文的讨论范围。
在上次实习生面试中,面试官了我C++虚函数是怎样实现的问题。我想读过 Inside the C++ Object Model 这本书的人对这点都是比较熟悉的,在解释过程中,他又问了我纯虚函数是什么,用来做什么。我在回答的过程中简单提了下“C++ 的纯虚函数在特殊情况下是有可能会被调用的,具体的行为由 C++ 的标准库的实现决定”,后来回想起这句话,想了好久没想到具体的被调用的情况,幸好面试官没追问这个问题,否则我真得语塞了(当时几乎整个过程都是我在滔滔不绝的回答,面试官就一直嗯嗯嗯的状态)。趁现在比较闲又不想复习考试,就顺便写写代码,针对这个问题总结出一篇博客文章与大家交流。
首先,必须清楚的是纯虚函数本身是不应该被调用的!因为纯虚函数是用来定义接口的,有时候基类自己找不到一个合情合理的实现,所以用虚函数的形式声明,让他的子类去做具体的实现。因此,如果纯虚函数被调用了,那一定是你的程序里出现了逻辑上的错误,这是我们在工作中需要了解和避免的,这也就是本文讨论的目的之一啦。
你知道,虚函数是通过指针或者引用来调用的,调用的具体函数由指针/引用的实际决定。而这个实际的对象,是可以由这个对象的内存块中的第一个值,vptr,指向虚函数表的指针来确定的。纯虚函数是属于基类的,所以要调用纯虚函数,这个指针所指的对象必须是基类。但是呢,抽象类(包含了纯虚函数的类)的对象是不允许被用户定义的,唔,这个规定看似严谨,C++ 怎么可能让你去调用纯虚函数,看本文的你也在好奇这个问题吧。不允许用户定义抽象类的对象,是的,不代表这种对象不能被构造!记得对象的构造过程,是先调用基类的构造函数,再调用子类的构造函数,也就是先构造基类对象,再构造子类对象,对象的析构我就不提了哈。也就是说,在基类的构造函数里调用的任何虚函数,都是调用基类自己的虚函数,而不是子类的虚函数,噢!漏洞就在这里!
当然,如果你尝试写下这样的代码:
class Base{ public: virtual void foo()= 0; Base() { foo(); } // 调用纯虚函数 }; class Derived: Base{ void foo() { } }; int main() { Derived d; }
很幸运的,编译器能发现错误并向你吐槽,以下是我使用CodeBlocks(自带MinGw的,含基于gcc 4.7.1的编译器)编译得到的一个警告和一个链接错误:
编译警告:warning: pure virtual 'virtual void Base::foo()' called from constructor
链接错误:undefined reference to `Base::foo()'
但是很不幸的,实际的应用中代码往往复杂得多,使得编译器无法在编译的时候发现问题。修改上面的代码如下,就可以成功的调用到虚函数了:
class Base{ public: virtual void foo()= 0; Base() { call_foo();} void call_foo() { foo(); } }; class Derived: Base{ void foo() { } }; int main() { Derived d; }
运行后得到的结果为:
哈哈好奇心终于得到了满足了!总结起来,其实还是 "Item 9: Never call virtual functions during construction or destruction." ( The third edition of Scott Meyers' popular book, Effective C++).
最后顺便提一下,在 C++ 11 的标准文档【ISO/IEC 14882:2011(E)】中,也有相关的描述:
(10.4.6)" Member functions can be called from a constructor (or destructor) of an abstract class; the effect of making a virtual call (10.3) to a pure virtual function directly or indirectly for the object being created (or destroyed) from such a constructor (or destructor) is undefined."
也就是说,纯虚函数调用的行为是未定义的,而 gcc 的默认实现则是终止程序,输出相关的信息。
参考链接:
参考书籍:
Effective C++, 3rd Edition, Scott Meyers
ISO/IEC 14882:2011(E)
Pony279原创博文,转载请注明出处