1. 程式人生 > >c++中被忽視的隱藏

c++中被忽視的隱藏

稍微懂得點oop的人都知道過載,那是多型性的重要體現!可是在c++中你能分清成員函式的過載、覆蓋嗎?這個好像也不難,過載存在與同一個類中,而覆蓋存在於派生類於基類中!可是如果再加上隱藏呢?說實話,以前我從來沒有聽說過這個概念!也不知道自己曾經捏造的程式,出了多少問題!看看林銳在《高質量 c++程式設計指南》中的解釋。


    成員函式的過載、覆蓋(override)與隱藏很容易混淆,C++程式設計師必須要搞清楚概念,否則錯誤將防不勝防。

8.2.1 過載與覆蓋

    成員函式被過載的特徵:

(1)相同的範圍(在同一個類中);

(2)函式名字相同;

(3)引數不同;

(4)virtual關鍵字可有可無。

    覆蓋是指派生類函式覆蓋基類函式,特徵是:

(1)不同的範圍(分別位於派生類與基類);

(2)函式名字相同;

(3)引數相同;

(4)基類函式必須有virtual關鍵字。

    示例8-2-1中,函式Base::f(int)與Base::f(float)相互過載,而Base::g(void)被Derived::g(void)覆蓋。

#include <iostream.h>

    class Base

{

public:

             void f(int x){ cout << "Base::f(int) " << x << endl; }

void f(float x){ cout << "Base::f(float) " << x << endl; }

     virtual void g(void){ cout << "Base::g(void)" << endl;}

};


 
 
    class Derived : public Base

{

public:

     virtual void g(void){ cout << "Derived::g(void)" << endl;}

};


 
 
    void main(void)

    {

     Derived d;

     Base *pb = &d;

     pb->f(42);        // Base::f(int) 42

     pb->f(3.14f);     // Base::f(float) 3.14

     pb->g();          // Derived::g(void)


 

示例8-2-1成員函式的過載和覆蓋

8.2.2 令人迷惑的隱藏規則

    本來僅僅區別過載與覆蓋並不算困難,但是C++的隱藏規則使問題複雜性陡然增加。這裡“隱藏”是指派生類的函式遮蔽了與其同名的基類函式,規則如下:

(1)如果派生類的函式與基類的函式同名,但是引數不同。此時,不論有無virtual關鍵字,基類的函式將被隱藏(注意別與過載混淆)。

(2)如果派生類的函式與基類的函式同名,並且引數也相同,但是基類函式沒有virtual關鍵字。此時,基類的函式被隱藏(注意別與覆蓋混淆)。

    示例程式8-2-2(a)中:

(1)函式Derived::f(float)覆蓋了Base::f(float)。

(2)函式Derived::g(int)隱藏了Base::g(float),而不是過載。

(3)函式Derived::h(float)隱藏了Base::h(float),而不是覆蓋。

#include <iostream.h>

    class Base

{

public:

    virtual void f(float x){ cout << "Base::f(float) " << x << endl; }

void g(float x){ cout << "Base::g(float) " << x << endl; }

            void h(float x){ cout << "Base::h(float) " << x << endl; }

}; 
 
    class Derived : public Base

{

public:

    virtual void f(float x){ cout << "Derived::f(float) " << x << endl; }

void g(int x){ cout << "Derived::g(int) " << x << endl; }

            void h(float x){ cout << "Derived::h(float) " << x << endl; }

}; 
 

示例8-2-2(a)成員函式的過載、覆蓋和隱藏

    據作者考察,很多C++程式設計師沒有意識到有“隱藏”這回事。由於認識不夠深刻,“隱藏”的發生可謂神出鬼沒,常常產生令人迷惑的結果。

示例8-2-2(b)中,pb和pd指向同一地址,按理說執行結果應該是相同的,可事實並非這樣。

void main(void)

{

Derived d;

Base *pb = &d;

Derived *pd = &d;

// Good : behavior depends solely on type of the object

pb->f(3.14f); // Derived::f(float) 3.14

pd->f(3.14f); // Derived::f(float) 3.14

// Bad : behavior depends on type of the pointer

pb->g(3.14f); // Base::g(float) 3.14

pd->g(3.14f); // Derived::g(int) 3        (surprise!)

// Bad : behavior depends on type of the pointer

pb->h(3.14f); // Base::h(float) 3.14      (surprise!)

pd->h(3.14f); // Derived::h(float) 3.14


 

示例8-2-2(b) 過載、覆蓋和隱藏的比較

8.2.3 擺脫隱藏

    隱藏規則引起了不少麻煩。示例8-2-3程式中,語句pd->f(10)的本意是想呼叫函式Base::f(int),但是Base::f(int)不幸被Derived::f(char *)隱藏了。由於數字10不能被隱式地轉化為字串,所以在編譯時出錯。

class Base

{

public:

void f(int x);

}; 
 
class Derived : public Base

{

public:

void f(char *str);

}; 
 
void Test(void)

{

Derived *pd = new Derived;

pd->f(10);    // error


 

示例8-2-3 由於隱藏而導致錯誤

    從示例8-2-3看來,隱藏規則似乎很愚蠢。但是隱藏規則至少有兩個存在的理由:

u       寫語句pd->f(10)的人可能真的想呼叫Derived::f(char *)函式,只是他誤將引數寫錯了。有了隱藏規則,編譯器就可以明確指出錯誤,這未必不是好事。否則,編譯器會靜悄悄地將錯就錯,程式設計師將很難發現這個錯誤,流下禍根。

u       假如類Derived有多個基類(多重繼承),有時搞不清楚哪些基類定義了函式f。如果沒有隱藏規則,那麼pd->f(10)可能會呼叫一個出乎意料的基類函式f。儘管隱藏規則看起來不怎麼有道理,但它的確能消滅這些意外。

示例8-2-3中,如果語句pd->f(10)一定要呼叫函式Base::f(int),那麼將類Derived修改為如下即可。

class Derived : public Base

{

public:

void f(char *str);

void f(int x) { Base::f(x); }

};

2

過載與覆蓋的區別
1、方法的覆蓋是子類和父類之間的關係,是垂直關係;方法的過載是同一個類中方法之間的關係,是水平關係。
2、覆蓋只能由一個方法,或只能由一對方法產生關係;方法的過載是多個方法之間的關係。
3、覆蓋要求引數列表相同;過載要求引數列表不同。
4、覆蓋關係中,呼叫那個方法體,是根據物件的型別(物件對應儲存空間型別)來決定;過載關係,是根據呼叫時的實參表與形參表來選擇方法體的。
override可以翻譯為覆蓋,從字面就可以知道,它是覆蓋了一個方法並且對其重寫,以求達到不同的作用。對我們來說最熟悉的覆蓋就是對介面方法的實現,在介面中一般只是對方法進行了宣告,而我們在實現時,就需要實現介面宣告的所有方法。除了這個典型的用法以外,我們在繼承中也可能會在子類覆蓋父類中的方法。在覆蓋要注意以下的幾點:
    1、覆蓋的方法的標誌必須要和被覆蓋的方法的標誌完全匹配,才能達到覆蓋的效果;
    2、覆蓋的方法的返回值必須和被覆蓋的方法的返回一致;
    3、覆蓋的方法所丟擲的異常必須和被覆蓋方法的所丟擲的異常一致,或者是其子類;
    4、被覆蓋的方法不能為private,否則在其子類中只是新定義了一個方法,並沒有對其進行覆蓋。
     overload對我們來說可能比較熟悉,可以翻譯為過載,它是指我們可以定義一些名稱相同的方法,通過定義不同的輸入引數來區分這些方法,然後再呼叫時,VM就會根據不同的引數樣式,來選擇合適的方法執行。在使用過載要注意以下的幾點:
    1、在使用過載時只能通過不同的引數樣式。例如,不同的引數型別,不同的引數個數,不同的引數順序(當然,同一方法內的幾個引數型別必須不一樣,例如可以是fun(int, float), 但是不能為fun(int, int));
    2、不能通過訪問許可權、返回型別、丟擲的異常進行過載;
    3、方法的異常型別和數目不會對過載造成影響;
   overload編譯時的多型   
   override執行時的多型
面向物件程式設計中的另外一個重要概念是多型性。在執行時,可以通過指向基類的指標,來呼叫實現派生類中的方法。可以把一組物件放到一個數組中,然後呼叫它們的方法,在這種場合下,多型性作
用就體現出來了,這些物件不必是相同型別的物件。當然,如果它們都繼承自某個類,你可以把這些派生類,都放到一個數組中。如果這些物件都有同名方法,就可以呼叫每個物件的同名方法。
同一操作作用於不同的物件,可以有不同的解釋,產生不同的執行結果,這就是多型性。多型性通過派生類過載基類中的虛擬函式型方法來實現。
在面向物件的系統中,多型性是一個非常重要的概念,它允許客戶對一個物件進行操作,由物件來完成一系列的動作,具體實現哪個動作、如何實現由系統負責解釋。
“多型性”一詞最早用於生物學,指同一種族的生物體具有相同的特性。在C#中,多型性的定義是:同一操作作用於不同的類的例項,不同的類將進行不同的解釋,最後產生不同的執行結果。C#支援兩種型別的多型性:
● 編譯時的多型性
編譯時的多型性是通過過載來實現的。對於非虛的成員來說,系統在編譯時,根據傳遞的引數、返回的型別等資訊決定實現何種操作。
● 執行時的多型性
執行時的多型性就是指直到系統執行時,才根據實際情況決定實現何種操作。C#中,執行時的多型性通過虛成員實現。
編譯時的多型性為我們提供了執行速度快的特點,而執行時的多型性則帶來了高度靈活和抽象的特點。
舉個簡單的例子:   
   void    test(CBase    *pBase)   
   {   
       pBase->VirtualFun();   
   }   
    
   這段程式編譯的時刻並不知道執行時刻要呼叫那個子類的函式,所以編譯的時刻並不會選擇跳轉到那個函式去!如果不是虛擬函式,那麼跳轉的偽彙編程式碼應該是call    VirtuallFun!但當是虛擬函式的時候,就不能這樣了,而是變成了call    pBase->虛擬函式表裡的一個變數,不同的子類在這個變數含有不同的函式地址,這就是所謂的執行時刻了。但事實上    pBase->虛擬函式表裡的一個變數    也是在編譯時刻就產生的的,它是固定的。    所以執行時刻,還是編譯時刻事實上也並不嚴密,重要的還是理解它的實質!
虛擬函式只是一個函式指標表,具體呼叫哪個類的相關函式,要看執行是,物件指標或引用所指的真實型別,由於一個基類的指標或引用可以指向不同的派生類,所以,當用基類指標或引用呼叫虛擬函式時,結果是由執行時物件的型別決定的

###################################################################################

“overload”翻譯過來就是:超載,過載,過載,超出標準負荷;“override”翻譯過來是:重置,覆蓋,使原來的失去效果。

先來說說過載的含義,在日常生活中我們經常要清洗一些東西,比如洗車、洗衣服。儘管我們說話的時候並沒有明確地說用洗車的方式來洗車,或者用洗衣服的方式來洗一件衣服,但是誰也不會用洗衣服的方式來洗一輛車,否則等洗完時車早就散架了。我們並不要那麼明確地指出來就心知肚明,這就有過載的意思了。在同一可訪問區內被聲名的幾個具有不同引數列的(引數的型別、個數、順序不同)同名函式,程式會根據不同的引數列來確定具體呼叫哪個函式,這種機制叫過載,過載不關心函式的返回值型別。這裡,“過載”的“重”的意思不同於“輕重”的“重”,它是“重複”、“重疊”的意思。例如在同一可訪問區內有:

① double calculate(double);

② double calculate(double,double);

③ double calculate(double, int);

④ double calculate(int, double);

⑤ double calculate(int);

⑥ float calculate(float);

⑦ float calculate(double);

六個同名函式calculate,①②③④⑤⑥中任兩個均構成過載,⑥和⑦也能構成過載,而①和⑦卻不能構成過載,因為①和⑦的引數相同。

覆蓋是指派生類中存在重新定義的函式,其函式名、引數列、返回值型別必須同父類中的相對應被覆蓋的函式嚴格一致,覆蓋函式和被覆蓋函式只有函式體(花括號中的部分)不同,當派生類物件呼叫子類中該同名函式時會自動呼叫子類中的覆蓋版本,而不是父類中的被覆蓋函式版本,這種機制就叫做覆蓋。