覆蓋 多態 重載 隱藏

先介紹下動態綁定,多態性,函數覆蓋,這3個概念是一致的,不同的書上有不同的叫法,但是他們的本質是一樣的。都體現了面向對象編程語言的一個重要的特點。          

        識記關鍵詞:(動態綁定,多態性,函數覆蓋),基類,派生類,基類指針(引用),虛函數實現手段:在C++中,通過基類的引用(或指針)調用虛函數時,發生動態綁定。引用(或指針)既可以指向基類對象也可以指向派生類對象,這一事實是動態綁定的關鍵。用引用(或指針)調用的虛函數在運行時確定,被調用的函數是引用(或指針)所指對象的實際類型所定義的。

        與函數重載的區別:不少初學者,甚至有些學了一段時間的,都會把它兩搞混,首先函數重載在我們學習類類型前就學過了,對於類來說,函數重載只是在同一個類作用域中定義相同函數名,但是不同參數類型的函數。它和類的繼承層次沒有關係,有沒有virtualok。

        注:重載 是在同一個類中定義的相同函數名,但是參數不同的函數,與類的繼承層次沒有關係。抓住重載與覆蓋所指的作用域不同!!!重載是發生在同一類或空間內,而覆蓋和隱藏發生於父類和子類繼承時。

        而函數覆蓋(多態)是特指在類的繼承層次中提出來的一個重要概念。基類必須有virtual指明要覆蓋的函數。

        這兩個區別開了,還有一個概念比較困擾初學者,就是函數隱藏。

        隱藏 是指派生類的函數屏蔽了與其同名的基類函數,既不是同一個類中的 重載 也不是類之間的 覆蓋(雖然隱藏也是類之間的產生的)。兩種情況如下:

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

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

 

重載與覆蓋:
        成員函數被重載的特徵:
               (1)相同的範圍(在同一個類中)
               (2)函數名字相同
               (3)參數不同(類型、順序或數目不同、包括const和非const參數)
               (4)virtual關鍵字可有可無(與virtual沒關係)
        覆蓋是指派生類函數覆蓋基類函數,特徵是:
               (1)不同的範圍(分別位於派生類與基類)
               (2)函數名字相同
               (3)參數相同
               (4)基類函數必須有virtual關鍵字(基類必須是虛函數)

*****************************************************************************************************************************************************

實例分析:

 

  1. #include <iostream>  
  2. using namespace std;  
  3. struct foo  
  4. {  
  5.        void func(int x) {  
  6.               cout <<"foo::func(int)"<<endl;  
  7.        }  
  8.        void func(float x) {  
  9.               cout <<"foo::func(float)"<<endl;  
  10.        }  
  11.        void func(double x) {  
  12.               cout <<"foo::func(double)"<<endl;  
  13.        }  
  14. };  
  15.    
  16. struct a  
  17. {  
  18.        void func(int x) {  
  19.               cout <<"struct a :: fun(int x)"<<endl;  
  20.        }  
  21. };  
  22. struct b : public a   
  23. {  
  24.        void func(float x) {  
  25.               cout <<"struct b::fun(float x)"<<endl;  
  26.        }  
  27. };  
  28. struct c : public b  
  29. {  
  30.        //using a::func;  
  31.        //using b::func;  
  32.        void func(double x) {  
  33.               cout <<"struct c::func(double x)"<<endl;  
  34.        }  
  35. };  
  36.    
  37. int main()  
  38. {  
  39.        foo  fo;  
  40.        fo.func(1);  
  41.        fo.func((float)1);  
  42.    
  43.        c  oc;  
  44.        oc.func(1);  
  45.        oc.func((float)1);  
  46. }  


運行結果是:

foo::func(int)

foo::func(float)

struct c::func(double x)

struct c::func(double x)

 
說明前面是重載,而後面則完全被覆蓋了, C++中有一條規則:派生類和基類是處於不同的兩個名字空間中。因此,拆開後的代碼應該不算是重載。如果要進行重載,需要 using 聲明:

struct c: public a

{   using a::func;

    using b::func;

    void func(double x){  }

};

 
        那麼,如果不用 using 聲明,拆分後代碼中的函數 func 不是重載應該是什麼呢?是隱藏(hide)。

        下面是關於重載、覆蓋和隱藏的區別: 這幾個概念都有一個共同點——函數名稱相同,所以不免讓人混淆,大致的區別如下:

        重載(overload):必須在一個域中,函數名稱相同但是函數參數不同,重載的作用就是同一個函數有不同的行爲,因此不是在一個域中的函數是無法構成重載的,這個是重載的重要特徵。

        覆蓋(override):覆蓋指的是派生類的虛擬函數覆蓋了基類的同名且參數相同的函數,既然是和虛擬函數掛鉤,說明了這個是一個多態支持的特性,所謂的覆蓋指的是用基類對象的指針或者引用時訪問虛擬函數的時候會根據實際的類型決定所調用的函數,因此此時派生類的成員函數可以“覆蓋”掉基類的成員函數。注意唯有同名且參數相同還有帶有virtual關鍵字並且分別在派生類和基類的函數才能構成虛擬函數,這個也是派生類的重要特徵。而且,由於是和多態掛鉤的,所以只有在使用類對象指針或者引用的時候才能使用上。總之一句話:覆蓋函數都是虛函數,反之不然~~ (如果基類和繼承類的函數名稱,產生返回值都是一樣的[如果返回值不同應該無法編譯],如果基類用到了virtual,那麼無論繼承類的實現中是否加入virtual 這個keyword ,還是會構成 override 的關係)。

        隱藏(hide):指的是派生類的成員函數隱藏了基類函數的成員函數。隱藏一詞可以這麼理解:在調用一個類的成員函數的時候,編譯器會沿着類的繼承鏈逐級的向上查找函數的定義,如果找到了那麼就停止查找了,所以如果一個派生類和一個基類都有同一個同名(暫且不論參數是否相同)的函數,而編譯器最終選擇了在派生類中的函數,那麼我們就說這個派生類的成員函數“隱藏”了基類的成員函數,也就是說它阻止了編譯器繼續向上查找函數的定義....

        回到隱藏的定義中,前面已經說了有virtual關鍵字並且分別位於派生類和基類的同名,同參數函數構成覆蓋的關係,因此隱藏的關係只有如下的可能:

                1)必須分別位於派生類和基類中

                2)必須同名

                3)參數不同的時候本身已經不構成覆蓋關係了,所以此時是否是virtual函數已經不重要了。當參數相同的時候就要看時候有virtual關鍵字了,有的話就是覆蓋關係,沒有的時候就是隱藏關係了。很多人分辨不清隱藏和覆蓋的區別,因爲他們都是發生在基類和派生類之中的,但是它們之間最爲重要的區別就是:覆蓋的函數是多態的,是存在於vtbl之中的函數才能構成“覆蓋”的關係,而隱藏的函數都是一般的函數,不支持多態,在編譯階段就已經確定下來了。

*********************************************************************************************************************************************************

深入分析“類”:

繼承(基類,父類,超類),派生類,子類


一:繼承中的訪問權限關係。
1.基類,父類,超類是指被繼承的類,派生類,子類是指繼承於基類的類.
2.在C++中使用:冒號表示繼承,如class A:public B;表示派生類A從基類B繼承而來
3.派生類包含基類的所有成員,而且還包括自已特有的成員,派生類和派生類對象訪問基類中的成員就像訪問自已的成員一樣,可以直接使用,不需加任何操作符,但派生類仍然無法訪問基類中的私有成員.
4.在C++中派生類可以同時從多個基類繼承,Java 不充許這種多重繼承,當繼承多個基類時,使用逗號將基類隔開.
5.基類訪問控制符,class A:public B 基類以公有方式被繼承,A:private B 基類以私有方式被繼承,A:protected B 基類以受保護方式被繼承,如果沒有訪問控制符則默認爲私有繼承。
6. protected 受保護的訪問權限:使用protected 保護權限表明這個成員是私有的,但在派生類中可以訪問基類中的受保護成員。派生類的對象就不能訪問受保護的成員了。
7. 如果基類以public 公有方式被繼承,則基類的所有公有成員都會成爲派生類的公有成員.受保護的基類成員成爲派生類的受保護成員
7.如果基類以private 私有被繼承,則基類的所有公有成員都會成爲派生類的私有成員.基類的受保護成員成爲派生類的私有成員.
8.如果基類以protected 受保護方式被繼承,那麼基類的所有公有和受保護成員都會變成派生類的受保護成員.
9.不管基類以何種方式被繼承,基類的私有成員,仍然保有其私有性,被派生的子類不能訪問基類的私有成員.
例:繼承中的訪問權限關係
class A {int a; protected:int b; public:int c; A(){a=b=c=1;} };
//類B以公有方式從基類A繼承
class B:public A
{public: int d;
B(){//a=2; //錯誤,不能訪問基類中的私有成員
b=2;   //正確,可以在類中訪問基類中的受保護成員,但類的對象不能訪問,基類中的受保護成員b在類B中仍然是受保護成員
c=d=2;} }; //基類中的公有成員c在類B中仍然是公有成員
//以受保護和私有方式從基類A繼承。
class C:protected A {public: int e; C(){//a=3; //錯誤,不能訪問基類中的私有成員
b=c=e=3; }};//這裏基類受保護成員b和公有成員c都成爲類C中的受保護成員。
class D:private A  {public: D(){b=c=4;} };//基類中的公有和受保護成員都成爲了類D中的私有成員。
//驗證受保護和私有方式繼承的訪問權限。
class C1:public C
{public: C1(){b=c=e=4;} };//正確;類A中的成員b和c在類C中是以受保護方式被繼承的,b和c都成爲了類C中的受保護成員。
class D1:public D
{public:D1(){//b=5; //錯誤,在A中受保護的成員b在類D中是以私有方式繼承的,這樣b就成爲了類D中的私有成員,所以無法訪問。
//c=5; //錯誤,在A中公有的成員c在類D中是以私有方式繼承的,這樣c就成爲了類D中的私有成員,所以無法訪問。} };
int main()
{A m1; B m2; C m3; D m4;
//cout<<m1.b<<m2.b<<m3.b<<m4.b;  //錯誤;不能用類的對象訪問受保護的成員,只有在類中才能訪問。
cout<<m1.c; cout<<m2.c;
//cout<<m3.c; //錯誤,類C是以受保護的方式從A繼承的,基類中的變量c在類C中就是受保護的,所以類的對象不能訪問
//cout<<m4.c; //錯誤,類C是以私有的方式從A繼承的,基類中的變量c在類C中就是私有的,所以類的對象不能訪問}
二:覆蓋和隱藏基類成員變量或成員函數。
1. 基類的成員變量或函數被覆蓋:如果派生類覆蓋了基類中的成員函數或成員變量,則當派生類的對象調用該函數或變量時是調用的派生類中的版本,當用基類對象調用該函數或變量時是調用的基類中的版本。
2. 隱藏基類成員函數的情況:如果在派生類中定義了一個與基類同名的函數,不管這個函數的參數列表是不是與基類中的函數相同,則這個同名的函數就會把基類中的所有這個同名的函數的所有重載版本都隱藏了,這時並不是在派生類中重載基類的同名成員函數,而是隱藏,比如類A中有函數f(int i,intj)和f(int i)兩個版本,當在從A派生出的類
B中定義了基類的f()函數版本時,這時基類中的f(int i)和f(int i,int j)就被隱藏了,也就是說由類B創建的對象比如爲m,不能直接訪問類A中的f(int i)版本,即使用語句m.f(2)時會發生錯誤。
3. 怎樣使用派生類的對象訪問基類中被派生類覆蓋或隱藏了的函數或變量:
3.1. 方法1 使用作用域運算符::,在使用對象調用基類中的函數或變量時使用作用域運算符即語句m.A::f(2),這時就能訪問基類中的函數或變量版本。注意,訪問基類中被派生類覆蓋了的成員變量只能用這種方法
3.2. 方法2 使用using:該方法只適用於被隱藏或覆蓋的基類函數,在派生類的類定義中使用語句using 把基類的名字包含進來,比如using A::f;就是將基類中的函數f()的所有重載版本包含進來,重載版本被包含到子類之後,這些重載的函數版本就相當於是子類的一部分,這時就可以用派生類的對象直接調用被派生類隱藏了的基類版本,比如m.f(2),但是使用這種語句還是沒法調用基類在派生類中被覆蓋了的基類的函數,比如m.f()調用的是派生類中定義的函數f,要調用被覆蓋的基類中的版本要使用語句m.A::f()纔行。
4. 在派生類的函數中調用基類中的成員變量和函數的方法:就是在函數中使用的被派生類覆蓋的基類成員變量或函數前用作域解析符加上基類的類名,即a::f()就是在派生類的函數中調用基類中被派生類覆蓋了的函數f()的方法。
5. 派生類以私有方式被繼承時改變基類中的公有成員爲公有的方法:
5.1.使用::作用域運算符,不提倡用這種方法,在派生類的public 後面用作用域運算符把基類的公有成員包函進來,這樣基類的成員就會成爲派生類中的公有成員了,注意如果是函數的話後面不能加括號,如A::f;如果f是函數的話不能有括號。
5.2.使用using語句,現在一般用這種方法,也是在派生類的public使用using把基類成員包函進來,如using A::f。
例:隱藏或覆蓋基類中的成員,使用::作用域運算符訪問
class A {int a; protected:int b; public:int c,d; void f(int i){cout<<"class A"<<"\n";} A(){a=b=c=d=1;} };
class B:public A {public:int d;  //覆蓋基類中的成員變量d。
B(){b=c=d=2;  //這裏是給子類B中的成員變量d賦值,而不是基類中的d
A::d=3; } //給基類中被覆蓋的成員d賦值,注意在類中訪問的被覆蓋成員的方式。
void f(){cout<<"class B"<<"\n";//在子類中重定義基類中的同名函數,雖然參數列表不一樣,但同樣會隱藏基類中的同名函數
A::f(1);   //在函數中調用基類中被隱藏了的同名函數的方法,使用作用域解析運算符。
//f(1);  //錯誤,因爲基類中的函數被子類中的同名函數隱藏了,在這裏子類不知道有一個帶參數的函數f。} };
int main()
{B m; cout<<m.d<<"\n";    //輸出子類中的成員變量d的值,注意派生類中覆蓋了基類成員d.
cout<<m.A::d<<"\n"; //輸出基類中的成員變量d的值,注意這是使用對象訪問被覆蓋的基類成員的方式
m.f();            //調用子類中的不帶參數的函數f。
//m.f(2);  //錯誤,因爲基類中的帶一個參數的函數f被子類中的同名函數隱藏掉了,不能這樣訪問,須用作用域解析運算符來訪問。
m.A::f(1); }  //使用子類對象訪問基類中被隱藏的函數的方法。
例:使用using 語句以便訪問基類中被隱藏的函數
class A{int a; protected:int b; public:int c,d; void f(){cout<<"Amoren"<<"\n";}
void f(int i){cout<<"class A"<<"\n";} A(){a=b=c=d=1;} };
class B:public A {public:int d;  //覆蓋基類中的成員變量d。
B(){b=c=d=2;  //這裏是給類B中的成員變量d賦值,而不是基類中的d
A::d=3; } //給基類中被覆蓋的成員d賦值,注意在類中訪問的被覆蓋成員的方式。
using A::f;  //使用語句using把類A中的函數f包含進來,以便以後可以直接訪問基類被隱藏了的函數,注意函數f沒有括號
void f(){cout<<"class B"<<"\n";//在子類中覆蓋基類中的同名函數,注意這裏是覆蓋,同時會隱藏基類中的其他同名重載函數
f(1);  //正確,因爲使用了using語句,所以可以在類中直接使用基類中f函數的重載版本。
A::f(2) ;//正確,雖然使用了using語句,但同樣可以按這種方法訪問基類中的函數。
A ma; 
ma.f();  //正確,在子類中創建的基類對象,可以直接用對象名調用基類中被子類覆蓋或隱藏了的函數,因爲這時不會出現二義性。
ma.f(1);}  //正確,在子類中創建的基類對象,可以直接用對象名調用基類中被子類覆蓋或隱藏了的函數,因爲這時不會出現二義性。
void g(){cout<<"this g"<<"\n";  f();  //正確,但該語句訪問的是子類中的不帶參數函數f,雖然在類中使用了using語句,但直接調用
被子類覆蓋了的基類函數時不能使用這種方法
A::f();  }};//正確,調用被子類覆蓋了的基類中的函數f,注意,雖然使用了using但要訪問被子類覆蓋了的函數,只能這樣訪問。
int main()
{B m; m.f();   //調用子類中的不帶參數的函數,這裏不會調用基類中的不帶參數的被覆蓋的函數f。
m.A::f(); //調用基類中被子類覆蓋了的函數f,雖然子類使用了using語句,但要訪問基類中被覆蓋的方法只能像這樣使用。
m.f(1);  //調用基類重載的f函數,注意這裏可以不用::運算符,因爲在子類中使用了using,只要子類沒有覆蓋基類中的方法,都可以這
樣直接調用。
m.A::f(2); } //當然,使用了using後,也可以使用這種方法
例:派生類以私有方式被繼承時改變基類中的公有成員爲公有的方法
class A {public: int a,b; void f(){cout<<"f"<<"\n";}  void g(){cout<<"g"<<"\n";} };
class B:private A
{public:  A::f; A::a; //使用::運算符使基類中的成員成爲公有的。注意函數名後不能有括號。
using A::g; }; //使用using語句使基類中的成員函數g成爲類B中的公有成員,注意函數名後不能有括號。
int main()
{ B m;
//m.b=1;  //錯誤,因爲類B是以私有方式繼承的,類A中的成員在類B中是私有的,這裏不能訪問私有成員。
m.f(); m.g(); m.a=1;}
三:繼承時的構造函數和析構函數問題
1. 在繼承中,基類的構造函數構建對象的基類部分,派生類的構造函數構建對象的派生類部分。
2. 當創建派生類對象時先用派生類的構造函數調用基類的構造函數構建基類,然後再執行派生類構造函數構造派生類。即先構造基類再構造派生類的順序。執行析構函數的順序與此相反。
3. 調用基類帶參數的構造函數的方法:在派生類的構造函數中使用初始化列表的形式就可以調用基類帶參數的構造函數初始化基類成員,如B():A(int i){},類B是類A的派生類。
4. 派生類的構造函數調用基類的構造函數的方法爲:
4.1 如果派生類沒有 顯示用初始化列表調用基類的構造函數時,這時就會用派生類的構造函數調用基類的默認構造函數,構造完基類後,纔會執行派生類的構造函數函數體,以保證先執行基類構造函數再執行派生類構造函數的順序,如果基類沒有默認構造函數就會出錯。
4.2 如果派生類用 顯示的初始化列表調用基類的構造函數時,這時就會檢測派生類的初始化列表,當檢測到顯示調用基類的構造函數時,就調用基類的構造函數構造基類,然後再構造派生類,以保證先執行基類構造函數再執行派生類構造函數的順序,如果基類沒有定義派生類構造函數初始化列表調用的構造函數版本就會出錯。
6. 如果在基類中沒有定義默認構造函數,但定義了其他構造函數版本,這時派生類中定義了幾個構造函數的不同版本,這時只要派生類有一個構造函數沒有顯示調用基類中定義的構造函數版本就會發生錯誤,因爲編譯器會首先檢查派生類構造函數調用基類構造函數的匹配情況,如果發現不匹配就會出錯,即使沒有創建任何類的對象都會出錯,而
不管這個派生類的對象有沒有調用派生類的這個構造函數。比如:基類有一個構造函數版本A(int i)而沒有定義默認構造函數,派生類B,有這幾個版本的構造函數B():A(4){},B(int i):A(5){},再有語句B(int i, int j){}沒有顯示調用基類定義的構造函數而是調用基類的默認構造函數,如果創建了B m和語句B m(1)時都會提示沒有可用的基類默認構造函數可用的錯誤,雖然這時類B的對象m沒有調用派生類B的帶有兩個形參的構造函數,但同樣會出錯。
7. 同樣的道理,如果基類中定義了默認構造函數,卻沒有其他版本的構造函數,而這時派生類卻顯示調用了基類構造函數的其他版本,這時就會出錯,不管你有沒有創建類的對象,因爲編譯器會先在創建對象前就檢查構造函數的匹配問題。
8. 派生類只能初始化他的直接基類。比如類C是類B的子類,而類B又是類A的子類,這時class C:public B{public:B():A(){} };將會出錯,該語句試圖顯示調用類B的基類類A的構造函數,這時會出現類A不是類C的基類的錯誤。
9. 繼承中的複製構造函數和構造函數一樣,基類的複製構造函數複製基類部分,派生類的複製構造函數複製派生類部分。
10.派生類複製構造函數調用基類複製構造函數的方法爲:A(const A& m):B(m){}其中B是基類,A是派生類。
11.如果在派生類中定義了複製構造函數而沒有用初始化列表顯示調用基類的複製構造函數,這時不管基類是否定義了複製構造函數,這時出現派生類對象的複製初始化情況時就將調用基類中的默認構造函數初始化基類的成員變量,注意是默認構造函數不是默認複製構造函數,如果基類沒有默認構造函數就會出錯。也就是說派生類的複製構造函
數的默認隱藏形式是B(const B& j):A(){}這裏B是A的派生類,也就是說如果不顯示用初始化列表形式調用基類的複製構告函數時,默認情況下是用初始化列表的形式調用的是基類的默認構造函數。
12.當在派生類中定義了複製構造函數且顯示調用了基類的複製構造函數,而基類卻沒有定義基類的複製構造函數時,這時出現派生類對象的複製初始化情況就將調用基類中的默認複製構造函數初始化基類部分,調用派生類的複製構造函數初始化派生類部分,因爲複製構造函數只有一種形式,即A(const A& m){},比如當出現調用時A(const A&
m):B(m){}如果這時基類B沒有定義複製構造函數,則該語句將會調用派生類A的默認複製構造函數。
12.如果基類定義了複製構造函數,而派生類沒有定義時,則會調用基類的複製構造函數初始化基類部分,調用派生類的默認複製構造函數初始化派生類部分。
例:基類不定義默認構造函數,派生類不定義複製構造函數而基類定義複製構造函數的情形。
//類A不定義默認構造函數
class A {public: int a,a1; A(int i){a=a1=11;cout<<"goucaoA1"<<"\n";} A(const AEFGHIJKLMFJNKLMOPQRSS"fugouA"<<"\n";}
TUVHIOPQRSS"hahaA"<<"\n";} };
//類B不定義複製構造函數
class B:public A
{public: int b,b1;
//B(){b=b1=2; cout<<"goucaoB"<<"\n";}    //錯誤,此語句會調用基類中的默認構造函數,而基類沒有默認構造函數。
//B(int i,int_HI`K`NKaMbFFcc錯誤,同上,此語句沒有顯示調用基類構造函數,就會調用基類的默認構造函數,而這時基類沒有默認構造
函數。
B(int i):A(2){b=b1=3;cout<<"goucaoB1"<<"\n";} jkVHIOPQRSS"hahaB"<<"\n";}};//顯示調用基類帶一個形參的構造函數,注意語法
int main()
{ B m(rHMFFOPQRS<m.a<<m.b<<"\n"; //輸出113
B m1(m);  cout<<m1.a<<m1.b;} //輸出43,注意,此語句將調用基類定義的複製構造函數,然後調用派生類的默認複製構造函數。
例:派生類B使用默認的B(const B& K):A(){}形式,而不使用初始化列表顯示調用基類A中的複製構造函
數的情況
class A
{public: int a,a1; A(){a=a1=2;cout<<"goucaoA"<<"\n";} A(int i){a=a1=11;cout<<"goucaoA1"<<"\n";}
A(const AxFGHIJKLMFJNKLMOPQRSS"fugouA"<<"\n";} yUVHIOPQRSS"hahaA"<<"\n";} };
//類中B不使用初始化列表調用基類中的複製構造函數的情況
class B:public A
{public: int b,b1; B(){b=b1=2; cout<<"goucaoB"<<"\n";}   
//B(int i,intzH{UVL|LHI`K`NKaMbFFcc錯誤,調用了基類沒有定義的帶兩個參數的構造函數。
B(int i):A(2){b=b1=3;cout<<"goucaoB1"<<"\n";}
B(const BF.HI`K`NK.MOPQRSS"fugouB"<<"\n";} //注意,這裏沒有顯示調用基類的複製構造函數,而是用默認的B(const B.F.H{UVHIb的
形式調用的基類中的默認構造函數,注意是默認構造函數而不是默認複製構造函數。
.kVHIOPQRSS"hahaB"<<"\n";}};
int main()
{ B m(.HMFFOPQRSS..JSS..`SS"\n";// 輸出113。
B m1(m); cout<<m1.a<<m1.b;}//輸出25,注意此語句將調用基類的默認構造函數將基類中的成員初始化爲,再調用派生類的複製構造函
數將類B中的成員b初始化爲。
14.1.6 多重繼承與虛基類
1. C++允許一個派生類從多個基類繼承,這種繼承方式稱爲多重繼承,當從多個基類繼承時每個基類之間用逗號隔開,比如class A:public B, public C{}就表示派生類A從基類B和C繼承而來。
2. 多重繼承的構造函數和析構函數:多重繼承中初始化的次序是按繼承的次序來調用構造函數的而不是按初始化列表的次序,比如有class A:public B, public C{}那麼在定義類A的對象A m時將首先由類A的構造函數調用類B的構造函數初始化B,然後調用類C的構造函數初始化C,最後再初始化對象A,這與在類A中的初始化列表次序無關。
3. 多重繼承中的二義性問題:
3.1. 成員名重複:比如類A從類B和C繼承而來,而類B和C中都包含有一個名字爲f的成員函數,這時派生類A創建一個對象,比如A m; 語句m.f()將調用類B中的f函數呢還是類C中的f函數呢?
3.2. 多個基類副本:比如類C和B都從類D繼承而來,這時class A:public B, public C{}類A從類C和類B同時繼承而來,這時類A中就有兩個類D的副本,一個是由類B繼承而來的,一個是由類C繼承而來的,當類A的對象比如A m;要訪問類D中的成員函數f時,語句m.f()就會出現二義性,這個f函數是調用的類B繼承來的f還是訪問類C繼承來的函數f呢。
3.3. 在第2種情況下還有種情況,語句classA:public B,public C{},因爲類A首先使用類B的構造函數調用共同基類D的構造函數構造第一個類D的副本,然後再使用類C的構造函數調用共同基類D的構造函數構造第二個類D的副本。類A的對象m總共有兩個共享基類D的副本,這時如果類D中有一個公共成員變量d,則語句m.B::d和m.D::d都是訪問的同一變量,類B和類D都共享同一個副本,既如果有語句m.D::d=3 則m.B::d也將是3。這時m.C::d的值不受影響而是原來的值。爲什麼會這樣呢?因爲類A的對象m總共只有兩個類D的副本,所以類A的對象m就會從A繼承來的兩個直接基類B和C中,把從共同基類D 中最先構造的第一個副本作爲類A的副本,即類B構造的D的副本。因爲class A:public B,public C{}最先使用B的構造函數調用共同基類類D創造D的第一個副本,所以類B和類D共享同一個副本。
3.4. 解決方法:對於第1 和第2 種情況都可以使用作用域運算符::來限定要訪問的類名來解決二義性。但對於第二種情況一般不允許出現兩個基類的副本,這時可以使用虛基類來解決這個問題,一旦定義了虛基類,就只會有一個基類的副本。
例:多重繼承及其二義性
class A {public:int a; A(int i){a=i;cout<<"A";}}; //共同的基類A
class B:public A {public: int b; B():A(4){cout<<"B";}};
class C:public A{public: int c; C():A(5){cout<<"C";}};
class D:public B,public C
{public: int d;D():C(),B(){cout<<"D";}}; //先調用類B的構造函數而不會先調用類C的構造函數,初始化的順序與初始化列表順序無關
int main()
{D m;  //輸出ABACD,調用構造函數的順序爲類ABACD,注意這裏構造了兩個類A的副本,調用了兩次類A的構造函數
// m.a=1; //錯誤,出現二義性語句,因爲類D的對象m有兩個公共基類A的副本,這裏不知道是調用由類B繼承來的A的副本還是由類C繼承
來的A的副本
m.B::a=1;cout<<m.B::a; //輸出1。
m.A::a=3; cout<<m.B::a<<m.A::a;//輸出33,類B和類A共享相同的副本,調用類A中的a和類B繼承來的a是一樣的,因此最後a的值爲3。
m.C::a=2;  cout<<m.C::a;} //輸出2,類C和類A類B的副本是彼此獨立的兩個副本,因此,這裏不會改變類B和類A的副本中的a的值。
4. 虛基類:方法是使用virtual關見字,比如class B:public virtual D{},class C:virtual public D{}注意關見字virtual的次序不關緊要。類B和類C以虛基類的方式從類D 繼承,這樣的話從類B和類C同時繼承的類時就會只創見一個類D的副本,比如class A:public B, public C{}這時類A的對象就只會有一個類D的副本,類A類B類C類D四個類都共享一個類D的副本,比如類D 有一個公有成員變量d,則m.d和m.A::d,m.B::d,m.C::d,m.D::d 都將訪問的是同一個變量。這樣類A的對象調用類D中的成員時就不會出現二義性了。
5. 虛基類的構造函數:比如class B:public virtual D{};class C:virtual public D{}; class A:public B,public C{};這時當創建類A的對象A m時初始化虛基類D將會使用類A的構造函數直接調用虛基類的構造函數初始化虛基類部分,而不會使用類B或者類C的構造函數調用虛基類的構造函數初始化虛基類部分,這樣就保證了只有一個虛基類的副本。但是當創建一個類B和類C的對象時仍然會使用類B和類C中的構造函數調用虛基類的構造函數初始化虛基類。
例:虛基類及其構造函數
class A {public:int a; A(){cout<<"moA"<<"\n";} A(int i){a=i;cout<<"A";} };
class B:public virtual A {public: int b; B(int i):A(4){cout<<"B";}  }; //以虛基類的方式繼承
class C:public virtual A {public: int c;  C():A(5){cout<<"C";} };
class D:public B, public C {public: int d; D():A(4),C(),B(2){cout<<"D";}};
//因爲類D是虛基類,所以類D會直接調用虛基類的構告函數構造虛基類部分,而不會使用類B或者類C的構造函數來調用虛基類的構造函數初始化虛基類部分。要調用虛基類中的帶參數的構造函數必須在這裏顯示調用,如果不顯示調用就將調用虛基類的默認構造函數。
int main()
{D m;  //輸出ABCD,注意這裏沒有重複的基類副本A。
C m1;  //輸出AC,雖然D是虛基類,但當創建類C的對象時仍然會使用類C的構造函數調用虛基類的構造函數初始化虛基類部分。
cout<<m.a;  //輸出4,因爲使用了虛基類所以這裏就不存在二義性問題。
m.B::a=1;
cout<<m.a<<m.A::a<<m.B::a<<m.C::a;} //輸出1111,類A,類B,類C,類D共享的是同一個虛基類的副本,所以這裏輸出四個1。
15 虛函數(virtual關見字)和多態性
一:繼承中的指針問題。
1. 指向基類的指針可以指向派生類對象,當基類指針指向派生類對象時,這種指針只能訪問派生對象從基類繼承而來的那些成員,不能訪問子類特有的元素,除非應用強類型轉換,例如有基類B和從B派生的子類D,則B *p;D dd;p=&dd;是可以的,指針p 只能訪問從基類派生而來的成員,不能訪問派生類D特有的成員.因爲基類不知道派生類
中的這些成員。
2. 不能使派生類指針指向基類對象.
3. 如果派生類中覆蓋了基類中的成員變量或函數,則當聲明一個基類指針指向派生類對象時,這個基類指針只能訪問基類中的成員變量或函數。例如:基類B和派生類D都定義了函數f,則B *p; D m; p=&m; m.f()將調用基類中的函數f()而不會調用派生類中的函數f()。
4. 如果基類指針指向派生類對象,則當對其進行增減運算時,它將指向它所認爲的基類的下一個對象,而不會指向派生類的下一個對象,因此,應該認爲對這種指針進行的增減操作是無效的.
二:虛函數
1. 爲什麼要使用虛函數:正如上面第1 和3 點所講的,當聲明一個基類指針指向派生類對象時,這個基類指針只能訪問基類中的成員函數,不能訪問派生類中特有的成員變量或函數。如果使用虛函數就能使這個指向派生類對象的基類指針訪問派生類中的成員函數,而不是基類中的成員函數,基於這一點派生類中的這個成員函數就必須和基類中的虛函數的形式完全相同,不然基類指針就找不到派生類中的這個成員函數。使用虛函數就實現了一個接口多種方法。
2. 注意不能把成員變量聲明爲虛有的,也就是說virtual關見字不能用在成員變量前面。
3. 正如上面所介紹的,一般應使用基類指針來調用虛函數,如果用點運算符來調用虛函數就失去了它的意義.
4. 如果基類含有虛函數則當聲明瞭一個基類的指針時,當基類指針指向不同的派生類時,它就會調用相應派生類中定義的虛函數版本.這種調用方法是在運行時決定的,例如在類B中聲明瞭虛函數,C,D,E 都從B繼承而來且都實現了自已的虛函數版本,那麼當定義了一個B類的指針P時,當P指向子類C時就會調用子類C中定義的虛函數,當
P指向子類D時就會調用子類D中定義的虛函數,當P指向子類E時就會調用子類E中定義的虛函數.
5. 虛函數須在基類中用virtual 關見字聲明也可以在基類中定義虛函數,並在一個或多個子類中重新定義.重定義虛函數時不需再使用virtual關見字,當然也可以繼續標明virtual關見字,以便程序更好理解。
6. 包括虛函數的類被稱爲多態類.C++使用虛函數支持多態性.
7. 在子類中重定義虛函數時,虛函數必須有與基類虛函數的聲明完全相同的參數類型和數量,這和重載是不同的.如果不相同,則是函數重載,就失去了虛函數的本質.
8. 虛函數不能是聲明它的類的友元函數,必須是聲明它的類的成員函數,不過虛函數可以是另一個類的友元.
9. 一旦將函數聲明爲虛函數,則不管它通過多少層繼承,它都是虛函數,例如D和B繼承,而E又從D繼承,那麼在
B中聲明的虛函數,在類E中仍然是虛函數.
10.隱藏虛函數:如果基類定義了一個虛函數,但派生類中卻定義了一個虛函數的重載板本,則派生類的這個版本就會把基類的虛函數隱藏掉,當使用基類指針調用該函數時只能調用基類的虛函數,而不能調用派生類的重載版本,當用派生類的對象調用基類的虛函數時就會出現錯誤了,因爲基類的虛函數被派生類的重載版本隱藏了。
11.帶默認形參的虛函數:當基類的虛函數帶有默認形參時,則派生類中對基類虛函數的重定義也必須有相同數量的形參,但形參可以有默認值也可以沒有,如果派生類中的形參數量和基類中的不一樣多,則是對基類的虛函數的重載。對虛函數的重定義也就意味着,當用指向派生類的基類指針調用該虛函數時就會調用基類中的虛函數版本。比如基
類定義virtual void f(int i=1, int j=2){}則派生類中必須定義帶有兩個形參的函數f纔是對基類虛函數f的重定義,不然就是函數f的重載版本,比如派生類中定義的void f(),void f(int i),void f(int i=2)都是對函數f的重載,不是對f的重定義。而void f(int i, int j),void f( int i, int j=3),void f(int i=4, int j=5)都是對虛函數f的重定義。
12.如果虛函數形參有默認值,那麼派生類中的虛數的形參不論有無默認值,當用指針調用派生類中的虛函數時就會被基類的默認值覆蓋,即派生類的默認值不起作用。但用派生類的對象調用該函數時,就不會出現這種情況。
13.當用指向派生類的基類指針調用虛函數時是以基類中的虛函數的形參爲標準的,也就是隻要調用的形式符合基類中定義的虛函數的標準就行了。比如基類中定義virtual void f(int i=1,int j=2){}派生類中重定義爲void f(int i, int j=3){}這時如果用派生類的對象調用這個派生類中的虛函數f 時必須至少要有一個實參,但是用指向派生類的基類指針調
用該虛函數時就可以不用任何形參就能調用派生類中的這個函數f,比如語句p->f()就會調用派生類中的虛函數版本。當用指向派生類的基類指針調用虛函數時是以基類中的虛函數的形參爲標準的,也就是隻要調用的形式符合基類中定義的虛函數的標準就行了。
14.析構函數可以是虛函數,但構造函數不能.
15.純虛函數聲明形式爲virtual 類型函數名(參數列表)=0;注意後面的等於0;
16.如果類至少有一個純虛函數,則這個類就是抽象的。
17.如果基類只是聲明虛函數而不定義虛函數則此虛函數是純虛函數.任何派生類都必須實現純虛函數的自已的版本.如果不實現純虛函數那麼該類也是抽象類。
18.抽象類不能有對象,抽象類只能用作其它類的基類,因爲抽象類中的一個或多個函數沒有定義,所以不能用抽象類聲明對象,
19.仍然可以用抽象類聲明一個指針,這個指針指向派生類對象.
20.如果派生類中未定義虛函數,則會使用基類中定義的函數.
21.虛函數虛擬特性是以層次結構的方式來繼承的,例如C從B派生而且C中重定義了B中的虛函數,而D又從C派生且未重定義B中的虛函數,這時聲明一個基類指針P,當P指向類D,並調用D中的虛函數時,由於D中未重定義虛函數他會調用基類中的虛函數版本,這時他會調用類C中的虛函數而不是類B中的虛函數,因爲類C比類B更接
近於類D.
例:虛函數的應用
class A
{public:int a; virtual void f(){cout<<"èéêQ.<<"\n";} virtual void h(int i=1,int ìKíHIOPQRSS".éêQ..<<"\n";}
eUVHIOPQRSS".éU.<<"\n";}//virtual int b;  //錯誤,不能把成員變量聲明爲虛有的。};
class B:public A
{public: int b;
void f(int i){cout<<"paif()"<<"\n";} //重載虛函數f。
void f(){cout<<"pai.Q.<<"\n";} //在派生類中重定義虛函數f
void h(){int b;b=5; cout<<"paifu"<<b<<"\n";}   //重載虛函數h的版本。注意這裏不是對基類虛函數的重定義。
void h(int i,int úKaHIint b; b=.MOPQRSS"paiüQ..<<b<<"\n";}//當基類中的虛函數有默認形參時,派生類中重定義基類中的虛函數的版
本必須有相同數量的形參,形參可以有默認值,也可以沒有。如果形參數量不一樣多則是對虛函數的重載。
@ABCDEFGHII"JKAL<<"\n";}};
int main()
{B m; A MNOPQR
//pSTUOVRWX/錯誤,指向派生類的基類指針不能調用派生類中的成員,只能調用基類中的成員,除非該成員是虛函數。
pijkBCRWWW//調用派生類中的函數f,輸出pailG
//pmnkBoCRWXX錯誤,注意這裏不是在調用派生類中帶一個形參的f函數,因爲帶一個參數的f函數不是虛函數,用指向派生類的基類指針時
不會調用派生類中的函數,除非這個函數是虛函數。這裏基類中沒有定義這種帶一個形參的f函數,所以這時會出現錯誤。
pxyz{{kBCRW//調用基類的虛函數f,輸出}KJG,可以用作用域運算符使用指向派生類的基類指針調用基類的虛函數
p...BCR//調用派生類中的虛函數版本h輸出pai.G..,用指向派生類的基類指針調用虛函數時派生類中的虛函數的默認值在這裏不起作用。
雖然派生類中的虛函數需要一個參數,但這裏不給參數仍是調用的派生類的帶兩個參數的虛函數h,而不是調用派生類中的不帶參數的h函數
//使用派生類對象調用成員
m.h();  //調用派生類中不帶參數的h函數,如果要用對象調用派生類中帶兩個形參的h函數,在本例中必須使用一個實參值。
m.h(1); //調用派生類中帶兩個形參的h函數,輸出pai.G.V,用對象調用派生類中的虛函數時函數的默認值不受基類虛函數默認值的影響
m.A::h();} // 調用基類中的虛函數h.
13.1.8 虛析構函數
1. 爲什麼需要虛析構函數:當使用new運算符動態分配內存時,基類的析構函數就應該定義爲虛析構函數,不然就會出問題。比如類B由類A繼承而來,則有語句A *p= new A;delete p; 這時沒有問題,調用類A的析構函數釋放類A的資源。但果再把類B的內存動態分配給指針p時如p= new B; delete p;如果基類的析構函數不是虛析構函數的話就會只調用基類A中的析構函數釋放資源,而不會調用派生類B的析構函數,這時派生類B的資源沒有被釋放。
2. 解決這個問題的方法是把基類的析構函數聲明爲虛析構函數,即在析構函數前加virtual 關見字,定義爲虛析構函數時當用delete釋放派生類的資源時就會根據基類的析構函數自動調用派生類中的析構函數釋放派生類的資源。
3. 只要基類中的析構函數是虛析構函數,則該基類的派生類中的析構函數自動爲虛析構函數,雖然派生類中的析構函數前沒有virtual關見字,析構函數名字也不一樣,但派生類中的析構函數被自動繼承爲虛析構函數。
4. 如果要使用new運算符分配內存,最好將析構函數定義爲虛析構函數。
例:使用new 分配內存,但不定義爲虛析構函數的情形
class A {public: int a;  .zBCDEFGHII".KzL<<"\n"; }};
class B:public A {public: int b; .ABCDEFGHII".KAL<<"\n"; }};
class C:public B{public: int c; ¢£BCDEFGHII"¤K£L<<"\n";}};
int main()
{A ¥NOne| A; delete p; //輸出§KzR
//B m; p=¨QRWWWXX此語句沒有錯,但是將使指針p指向一個靜態分配的內存地址,這時不能用delete語句釋放指針p的資源。
//delete p;  //錯誤,指針p現在指向的內容不是動態分配的內存,而是靜態內存,delete只能釋放動態分配的內存。
p=neá B;  //動態分配派生類B的內存,並把地址賦給指針p。
delete p; //輸出èKzR在這裏沒有調用派生類的析構函數釋放動態分配的派生類的內存資源。
B êN.OWneì B; delete p1; //輸出íKA.JKz.
p1=neD C; delete p1; //輸出.KA.JKz注意,這裏沒有釋放掉子類C的資源。
}
例:使用new 分配內存,且基類定義爲虛析構函數的情形
class A {public: int a; virtual .zBCDEFGHII"×KzL<<"\n";}  };//基類定義爲虛析構函數
class B:public A {public: int b; úABCDEFGHII".KAL<<"\n";} }; //派生類B自動繼承爲虛析構函數
class C:public B {public: int c;  à£BCDEFGHII"áK£L<<"\n";}}; //派生類C也自動繼承爲虛析構函數
int main()
{A .NOne. A; delete p;  //輸出.Kz
p=ne. B;
delete p; //輸出.KA.JKz因爲基類定義的是虛析構函數,所以在這裏調用派生類的析構函數釋放動態分配的派生類的內存資源,並調用基類的析構函數釋放基類的資源
B ìN1= neí B; delete p1; //輸出.KA.JKz
p1=ne. C;delete p1; } //輸出eK£.JKA.JKz這裏因爲類B的析構函數被自動繼承爲虛析構函數,所以這裏釋放了子類C的釋源。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章