Performanced C++ 經驗規則(4):靜態和多態,亦敵亦友

jopen 8年前發布 | 8K 次閱讀 C/C++開發

很奇妙,這一組對立的概念,卻可以在C++中和平共存,時而協同工作。

老規矩,還是一小段代碼提出問題,當一個虛成員函數(多態性)在其子類中被聲明為靜態成員函數時(或相反過來),會發生什么?

1、當虛函數遭遇靜態函數

#include <iostream>
using namespace std;

class Base
{
public:
    virtual void foo(void){ cout << "Base::foo()" << endl; }
};

class Derived : public Base
{
public:
    void foo(void){ cout << "Derived::foo()" << endl; }
} ;

class DerivedAgain : public Derived
{
public:
    static void foo(void){ cout << "DerivedAgain::foo()"<< endl; }
} ;

int main(int argc, char** argv)
{
    DerivedAgain da;
    Base* pB = &da;

    da.foo();
    pB->foo(); 
    return 0;
}

上述代碼運行結果是什么?等等,你確定上述代碼能通過編譯?在筆者Ubuntu 12.04 + gcc 4.6.3的機器上,上述代碼編譯不能通過。顯示如下信息:

stawithvir.cpp:19:17: error: ‘static void DerivedAgain::foo()’ cannot be declared
stawithvir.cpp:13:10: error:   since ‘virtual void Derived::foo()’ declared in base class

很明顯,編譯不能通過的原因,是在DerivedAgain類中將虛函數聲明為static,編譯器拒絕此“靜態”與“多態”的和平共處。此時理由很簡單,static成員函數,是類級共享的,不屬于任何對象,也不會傳入this指針,不能訪問非靜態成員;然而,虛函數的要求與此正相反,需要綁定對象(this指針),進而獲得虛表,然后進行調用。如此矛盾的行為,編譯器情何以堪,因為選擇報錯來表達其不滿。我們可以暫時記住結論:不能將虛函數聲明為靜態的

接下來你可能會問,編譯都不能通過的東西,對錯不是明擺著的嗎?為什么還要拿來討論,這是因為,在某些編譯器上(可以在VC6,VC2008等嘗試),該代碼能編譯通過,并輸出結果,不可思議?不過這些編譯器同時也給出了一個警告(參與MSDN warning c4526),指出靜態函數不能用做虛函數進行調用。雖然通過了編譯,但思想與上述Gcc是一致的。

//輸出結果
DerivedAgain::foo()
Derived::foo()

da.foo()輸出DerivedAgain::foo()沒有疑問(通過對象調用方法,無論是否虛方法,本來就不會產生動態綁定,即無虛特性);而pB->foo()輸出Derived::foo()則需要解釋一下,因為pB是指針調用虛方法,產生“多態”,動態綁定時發現pB指向的對象類型為DerivedAgain,于是去查找DerivedAgain對象虛表中foo()的地址,但此時發現DerivedAgain的虛表中foo()的地址其實是Derived::foo(),因為DerivedAgain中的foo已經被聲明為static,不會更新此函數在虛表中的地址(實際上,由于DerivedAgain沒有聲明任何新的虛函數,它對象的虛表同Derived對象是完全一樣的,如果有興趣,可以通過匯編查看),所以輸出的是Derived::foo(),也從一個側面證明了:在繼承鏈中,使用最”新”的虛函數版本。

至此,這個問題已經解釋清楚,再次記住結論:靜態成員函數,不能同時也是虛函數

2、重載(overload)并非真正的多態,其本質是靜態行為

筆者曾不止一次的看到,許多書籍、資料,在談到C++多態性的時候,經常把“重載”(overload)歸入多態行為中。這種說法看似也沒什么不正確,實際上我認為十分不妥。雖然重載,通過區分特征標的不同(注意,同函數名而參數不同、或同函數名但是否是const成員函數,都是重載依據),而使相同函數名的方法調用產生了不同的行為,確實體現了“多態”的思想,但重載的本質是靜態綁定,是編譯期就能確定調用哪個方法,而非動態綁定,所以不是真正的多態。所以,頭腦要清醒,即如果兩個(或多個)方法之間的關系是“重載”(overload),那么就不會有真正的多態行為產生。

3、何時產生真正的多態?

討論重載之后,就要談到,何時產生真正的多態行為,即動態綁定呢?筆者歸納三個必要條件如下:

(1)方法是虛的;

(2)有覆蓋(override)產生;

(3)通過指針或引用調用相應的虛方法,而非通過對象調用;通過對象調用方法,無論方法是否是虛方法,均是靜態聯編行為。

條件(1)(2)很明顯,如果方法是虛的也沒有覆蓋,何來“多”的“態”?而條件(3)容易被新手忽視,因為通過對象調用,對象的類型已經確知,所以靜態綁定,不會再產生多態。而通過指針或引用調用相應虛方法,由于在編譯期不能確定指針或引用指向的具體類型,所以只能動態聯編,從而產生多態

4、不正確的代碼將阻止多態行為

好了,接下來我們看一小段代碼,來自《C++ Primer Plus》:

class Base
{
public:
    virtual void foo(void) {...}
    ...
};

class Derived : public Base
{
public:
    void foo(void) {...}
    ...
};

//版本1
void show1(const Base& b)
{
    b.foo();
}

//版本2
void show2(Base b)
{
   b.foo();
}

int main(int argc, char** argv)
{
    Derived d;
    show1(d);
    show2(d);
    return 0;
}

上述代碼有什么問題?我們看到,兩個版本的show函數唯一不同之處,就是版本1按引用傳遞對象,版本2按值傳遞對象。在main函數中,新建了一個Derived對象并傳給版本1函數,由于版本1中的參數b是引用類型,OK,沒有問題,b.foo()將按照b實際指向的對象調用,即可以正確調用Derived::foo();而版本2參數b是對象類型(b是Base(const Base&)拷貝構造創建的一個Base對象,自動向上的強制類型轉換使得基類拷貝構造函數可以引用一個子類對象),根據上述第3點,則b.foo()將按對象類型(Base)調用到Base::foo(),不產生多態行為。即,由于按值傳遞,在此處阻止了動態綁定,阻止了多態行為

說到這里的話,又是老生常談的問題,即除非必須要這樣做,否則不要按值方式傳遞參數,而應選擇指針或引用,關于這個問題,本系列后面還會再談。

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!