閱讀本文之前,讀者需要掌握 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;
}
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;
}
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原創博文,轉載請注明出處 http://www.cnblogs.com/Pony279/archive/2013/06/04/3117955.html