C++ 虛擬函式的預設引數問題
前些日子,有個同學問我一個關於虛擬函式的預設引數問題。他是從某個論壇上看到的,但是自己沒想通,便來找我。現在分享一下這個問題。先看一小段程式碼:
#include <iostream> using namespace std; class A { public: virtual void Fun(int number = 10) { cout << "A::Fun with number " << number; } }; class B: public A { public: virtual void Fun(int number = 20) { cout << "B::Fun with number " << number << endl; } }; int main() { B b; A &a = b; a.Fun(); return 0; }
問題是,這段程式碼輸出什麼?正確答案是:B::Fun with number 10
這個問題並不難,關鍵要看你對C++瞭解多少。我瞭解得不多,但是這個小問題恰好能答上來。很明顯,這段程式碼的輸出結果依賴於C++的多型。什麼是多型?在C++中,多型表現為指向父類物件的指標(或引用)指向子類物件,然後利用父類指標(或引用)呼叫它實際指向的子類的成員函式。這些成員函式由virtual關鍵字定義,也就是所謂的虛擬函式。
如果你知道C++的多型是怎麼回事,那麼這道題目你至少能答對前半部分。也就是說,輸出的前半部分應該是這樣的:B::Fun with number。疑點在於,究竟number等於10還是20?
這就涉及到C++的靜態繫結和動態繫結問題。說到靜態繫結和動態繫結,就不能不談“靜態型別”和“動態型別”。何為靜態型別呢?
來看看C++ Primer(第4版,15.2.4)怎麼說的吧:“基類型別引用和指標的關鍵點在於靜態型別(static type,在編譯時可知的引用型別或指標型別)和動態型別(dynamic type,指標或引用所繫結的物件的型別,這是僅在執行時可知的)可能不同”。在該書第15章的最後,對靜態型別和動態型別做了一個總結:靜態型別是指“編譯時型別。物件的靜態型別與動態型別相同。引用或指標所引用的物件的動態型別可以不同於引用或指標的靜態型別”;動態型別是指“執行時型別。基類型別的指標和引用可以繫結到派生型別的物件,在這種情況下,靜態型別是基類引用(或指標),但動態型別是派生類引用(或指標)”。
這下就明白多了,靜態型別是編譯期就能確定的型別,簡單地說,當你宣告一個變數時,為該變數指定的型別就是它的靜態型別。動態型別是在程式執行時才能確定的型別,典型例子就是父類物件指標指向子類物件,這時,父類指標的動態型別就變成了子類指標。正如上述C++標準中所舉的例子,假設p原本是一個B型別的指標,如果現在讓p指向D物件,而D恰好是B的派生類,那麼p的動態型別就是D型別的指標。聽上去有點繞,為了方便說明,我還是拿出C++標準上的一個例子來分析:
struct A {
virtual void f(int a = 7);
};
struct B : public A {
void f(int a);
};
void m()
{
B* pb = new B;
A* pa = pb;
pa->f(); // OK, calls pa->B::f(7)
pb->f(); // error: wrong number of arguments for B::f()
}
這段程式碼中,pb的靜態型別是B型別指標,它的動態型別也是B型別指標。pa的靜態型別是A型別指標,而它的的動態型別卻是B型別指標。
一旦明白了靜態型別和動態型別的概念,靜態繫結和動態繫結也就好理解了。按照C++ Primer的說法,動態繫結是指“延遲到執行時才選擇執行哪個函式。在C++中,動態繫結指的是在執行時基於引用或指標繫結的物件的基礎型別而選擇執行哪個virtual函式”。顯然,動態繫結與虛擬函式是息息相關的。與此對應,靜態繫結就簡單多了:如果一個型別的成員函式不是虛擬函式,那也就沒什麼好選擇的了,通過指標或引用呼叫成員函式時,直接繫結到指標或引用的基礎型別即可。比如,在上面的程式碼中,pa->f(),這裡呼叫的實際上是B的成員函式f(),也就是說,被呼叫的是與pa的動態型別相對應的函式,這就是所謂的“動態繫結”。
說了這麼多,來解釋本文一開始給出的問題。在C++中,雖然虛擬函式的呼叫是通過動態繫結來確定的,但是虛擬函式的預設引數卻是通過靜態繫結確定的。(就這麼規定的,據說是為了提高效率)顯然,a的靜態型別是A的引用,而動態型別是B的引用,因此,當a呼叫虛擬函式Fun()時,根據動態繫結規則,它呼叫的是B的成員函式Fun();而對於虛擬函式的預設引數,根據靜態繫結規則,它將number確定為A中給出的預設值10。
再簡單說一下本文給出的第二段程式碼。這是C++標準中給出的一個例子,而且也給了說明:“A virtual function call (10.3) uses the default arguments in the declaration of the virtual function determined by the static type of the pointer or reference denoting the object. An overriding function in a derived class does not acquire default arguments from the function it overrides.” 我來翻譯一下吧:“呼叫虛擬函式時使用的預設引數在虛擬函式宣告中給出,這些預設引數由指示物件的指標或引用的靜態型別確定。派生類中的重寫函式無法獲得它所重寫的函式的預設引數。”