C++——繼承與多態


一、C++繼承

  1、繼承

    繼承是實現代碼重用的重要手段,繼承是利用已存在的類的形式,在保持原有類特性的基礎上進行擴展,增加功能。這樣產生新的類,成為派生類。繼承的語法是:在代碼中和原來一樣給出該類的名字,但在左括號前加一個冒號和基類的名字(對於多重繼承,要給出多個基類名,用逗號隔開)。

    派生類繼承基類的數據和函數,繼承可以調用基類的函數實現自己的函數。

  •  1 class A
     2 {
     3 public:
     4     void Func1(void);
     5     void Func2(void);
     6 };
     7 
     8 class B : public A   //B繼承A
     9 {
    10 public:
    11     void Func3(void)
    12     {
    13         A::Func1();//先調用基類的函數
    14         ...        //自己的實現代碼
    15     }
    16     void Func4(void)
    17     {
    18         ...  //自己的實現代嗎
    19             A::Func2(); //后調用基類的函數
    20     }
    21 };

  2、成員訪問限定符與繼承的關系:公有派生、私有派生

公有派生是主流。

  3、派生編程的步驟:

  • 四大步:
      • 吸收基類的成員:不論是數據成員,還是函數成員,處構造函數與析構函數外全盤接受
      • 改造基類成員:聲明一個和某某類成員同名的新成員,對於成員函數參數表也應完全相同,這稱為同名覆蓋,否則是重載。,派生類中的新成員就屏蔽了基類同名成員。
      • 發展新成員:派生類新成員必須與基類成員不同名,他的加入保證派生類在功能上有所擴展。
      • 重寫構造函數與析構函數。所謂不能繼承構造函數並不是不能利用基類的構造函數,而是把基類的構造函數作為新的構造函數的一部分或者說是調用基類的構造函數。

  4、派生類構造函數的定義:

  • 派生類構造函數的定義:(類外定義)

派生類名::派生類名(參數總表):基類名1(參數名表1),基類名2(參數名表2),......,成員對象名1(成員對象參數名表1),成員對象名2(成員對象參數名表2),......

{

派生類新增成員的初始化;

}//所列出的成員全部為新增成員對象的名字

  • 在構造函數的聲明中,冒號及冒號以后的部分必須略去。
  • 構造函數不能繼承但是可以利用,把基類的構造函數作為新的類的構造函數的一部分,或者說是調用基類的構造函數。
  • 派生類構造函數各部分執行的順序
      • 首先,調用基類的構造函數,按他們在派生類中的先后順序依次調用。
      • 調用成員對象的構造函數按他們在類定義中聲明的先后順序,依次調用。
      • 派生類的構造函數體中的操作

注意:在派生類構造函數中,只要基類不是使用無參的默認構造函數都要顯示的給出基類名與參數表。

   如果基類沒有定義構造函數,則派生類也可以不定義全部采用系統給定的默認構造函數。

   如果基類定義了帶有形參表的構造函數派生類就應當定義構造函數。

  5、派生類與基類的類型兼容性原則

 

  • 子類擁有父類的所有屬性和行為,子類是一種特殊的父類
      • 子類對象可以直接當父類對象使用
      • 子類對象可以直接賦值或初始化父類對象
      • 父類對象的指針和引用可以直接指向子類對象
      • 子類對象的指針和引用不能直接指向父類對象,但可以通過強制類型轉換完成
  •  1 class Person
     2 {
     3 public:
     4     void Display()
     5     {
     6         cout << _name << endl;
     7     }
     8 protected:
     9     string _name;
    10 };
    11 
    12 class Student : public Person
    13 {
    14 public:
    15     int _num;
    16 };
    17 
    18 void Test()
    19 {
    20     Person p;
    21     Student s;
    22 
    23     //子類對象可以賦值給父類對象
    24     p = s;
    25     //父類對象不可以賦值給子類對象
    26     //s = p;//error
    27 
    28     //父類對象的指針和引用可以指向子類對象
    29     Person *p1 = &s;
    30     Person& r1 = s;
    31 
    32     //子類對象的指針和引用不能指向父類的對象(但是可以通過強制類型轉化完成)
    33     Student *p2 = (Student*)&p;
    34     Student& r2 = (Student&)p;
    35 }

  6、派生類的默認成員函數

  在繼承關系中,派生類如果沒有顯示定義這六個成員函數,編譯系統會默認合成六個成員函數,即構造函數,拷貝構造函數,析構函數,賦值操作符重載,取地址操作符重載,const修飾的取地址操作符重載。

  7、單繼承與多重繼承

      單繼承:一個子類只有一個直接父類

      多重繼承:一個子類有兩個或兩個以上直接父類

  • 多重繼承存在歧義與唯一標識符問題,將在下邊的虛基類中介紹。

  8.友元與繼承:

友元關系不能繼承,基類的友元對派生類沒有特殊的訪問權限。

  •  1 //友元與繼承
     2 class Person
     3 {
     4     friend void Display(Person& p, Student& s);
     5 protected:
     6         string _name; //姓名
     7 };
     8 
     9 class Student : public Person
    10 {
    11 protected:
    12     int _stuNum; //學號
    13 };
    14 
    15 void Display(Person& p, Student& s)
    16 {
    17     cout << p._name << endl;
    18     cout << s._name << endl;
    19     cout << s._stuNum << endl;
    20 }
    21 
    22 void Test()
    23 {
    24     Person p;
    25     Student s;
    26     Display(p, s);
    27 }

     

  9、繼承與靜態成員:


  基類定義了static成員,則整個繼承體系中只有一個這樣的成員。無論派生出多少個子類,都只有一個static成員實例。即:如果我們重新定義了一個靜態成員,所有在基類中的其他重載函數會被隱藏。

    •  1 class dad
       2 {
       3 public:
       4 static int a;
       5 static int geta()
       6 {
       7 return a;
       8 }
       9 static int geta(int b)
      10 {
      11 return a + b;
      12 }
      13 };
      14 int dad::a = 99;
      15 class son :public dad
      16 {
      17 public:
      18 static int a;//基類靜態成員的屬性將會被隱藏
      19 static int geta(int b, int c)//重新定義一個函數,基類中重載的函數被隱藏
      20 {
      21 return a + b + c;
      22 }
      23 };
      24 int son::a = 66;
      25 class girl:public dad
      26 {
      27 public:
      28 static int a;//基類靜態成員的屬性將會被隱藏
      29 static void geta(int b, int c)//改變基類函數的某個特征,返回值或者參數個數,將會隱藏基類重載的函數
      30 {
      31 cout << a + b + c << endl;
      32 }
      33 };
      34 int girl::a = 44;
      35 void test()
      36 {
      37 son s;
      38 girl g;
      39 cout << s.a << endl;//輸出66
      40 cout << s.geta(1, 2) << endl;;//只能訪問son類中的geta,不能訪問父類中的geta
      41 cout << g.a << endl;//輸出44,只能訪問girl中的a
      42 g.geta(3, 4);//只能訪問girl中的geta
      43 }

 

  10、虛基類:

      1、虛基類:如果一個類派生有多個直接基類,而這些基類又有一個共同的基類,即菱形派生,則在最終的派生類中會保留該間接基類的共同基類的多份同名成員。在引用這些同名的成員時必須在派生類對象后增加直接基類,以免產生二義性,使其唯一的標識一個成員。如:c1.A::diaplay().在一個類中保留間接共同基類的多分同名成員,這種現象是人們不希望出現的。C++提供虛基類的方法,使得在繼承間接共同基類時只保留一份成員。

  • 1 class  A//聲明基類A
    2 {…};
    3 class B :virtual public  A//聲明類B是類A的公用派生類,A是B的虛基類
    4 {…};
    5 class C :virtual public  A//聲明類C是類A的公用派生類,A是C的虛基類
    6 {…};
    7 
    8 //注意:虛基類並不是在聲明基類時聲明的,而是在聲明派生類時,指定繼承方式時聲明的。因為一個基類可以在生成一個類時作為虛基類,而在生成另一個派生類時不作為虛基類。

     聲明虛基類的一般形式:class 派生類名:virtual 訪問限定符繼承方式  基類名

    • 經過這樣的聲明后,當基類通過多條派生路徑被一個派生類繼承時,該派生類只繼承該基類一次。
    • 需要注意的時:為了保證虛基類在派生類中只繼承一次,應當在該基類的所有直接派生類中聲明為虛基類。否則仍會出現對基類的多次繼承。

      2、虛基類的初始化與構造函數

 

  • 虛基類的初始化如果在虛基類中定義了帶參數的構造函數,而且沒有定義默認構造函數,則在其所有派生類中(包括直接派生和間接派生的類中),通過構造函數的初始化表對虛基類進行初始化。
  •  1 class 
     2 A//定義基類A
     3 {
     4    A(int i){ } //基類構造函數,有一個參數};
     5 class B :virtual public A 
     6 //A作為B的虛基類
     7 {
     8    B(int n):A(n){ } //B類構造函數,在初始化表中對虛基類初始化
     9 };
    10 class C 
    11 :virtual public A //A作為C的虛基類
    12 {
    13    C(int n):A(n){ } 
    14 //C類構造函數,在初始化表中對虛基類初始化
    15 };
    16 class D :public B,public C 
    17 //類D的構造函數,在初始化表中對所有基類初始化
    18 {
    19    D(int n):A(n),B(n),C(n){ }
    20 };
      • 在定義類D的構造函數時,與以往的使用方法不同。規定:
      • 在最后的派生類中,不僅要負責對直接基類進行初始化,還要負責對虛基類進行初始化。C++編譯系統只執行最后的派生類對虛基類的構造函數的調用,而忽略虛基類的其他派生對(如類B和類C)虛基類的構造函數的調用,這樣就保證虛基類的數據成員不會被多次初始化

      • 使用多重繼承時要格外小心,經常出現二義性問題,一般只有在比較簡單和不易出現二義性的情況才使用多重繼承,能用單一繼承解決的問題就不要用多重繼承。



 

二、C++多態

  C++中的虛函數的作用主要是實現了多態的機制。關於多態,簡而言之就是用父類型別的指針指向其子類的實例,然后通過父類的指針調用實際子類的成員函數。這種技術可以讓父類的指針有“多種形態”。

   1、多態的引入:

  •  1 class Parent
     2 {
     3 public:
     4     Parent(int a){
     5         this->a = a;
     6     }
     7     printP(){
     8         cout<<"Parent"<<endll;
     9     }
    10 private:
    11     int a;
    12     
    13 };
    14 
    15 class Son:public Parent{
    16 public:
    17     Son(int b):Parent(10){
    18         this->b = b;
    19     }
    20     printP(){
    21         cout<<"Son"<<endll;
    22     }
    23 private:
    24     int b;
    25 };
    26 
    27 void howtoPrint(Parent *base){
    28     base->printP();
    29 }
    30 
    31 void howtoPrint2(Parent &base){
    32     base.printP();
    33 }

      上邊定義了兩個類,並且父類與子類都有一個同名函數printP(),下面通過幾種方式測試案例


  • 定義一個基類指針,讓指針分別指向基類與子類對象,然后調用printP();
    • void main(int argc, char const *argv[])
      {
          Parent *p = NULL;
          Parent p1(20);
          Son s1(30);//
      
          p = &p1;//指針執行基類
          p1->printP();//
      
          p = &s1;//類型兼容性原則
          p->printP();
      }
      
      //兩個執行的結果都是調用基類的printP()函數
  • 定義基類的引用分別指向基類與派生類
    •  1 void main(int argc, char const *argv[])
       2 {
       3 
       4     
       5         Parent &base = p;//父類的別名
       6         base.printP();//執行父類
       7 
       8         Parent &base2 = s1;//別名
       9         base2.printP();//執行父類
      10 }
      11 //結果也都是調用父類的printP()函數
  • 定義一個函數,即上邊的howtoPrint(),函數參數為基類指針,然后定義一個指向基類的指針,讓改指針分別指向基類對象與派生類對象
    •  1 void main(int argc, char const *argv[])
       2 {
       3     Parent *p = NULL;
       4     Parent p1(20);
       5     Son s1(30);
       6 
       7     p = &p1;
       8     howtoPrint(&p1);
       9     p = &s1;
      10     howtoPrint(&s1);
      11 }
      12 //結果都是執行父類的printP()函數
  • 定義一個函數,即上邊的howtoPrint2()函數,函數參數為基類對象的引用然后分別傳入基類對象引用和子類對象引用:
    • 1 void main(int argc, char const *argv[])
      2 {
      3     Parent *p = NULL;
      4     Parent p1(20);
      5     Son s1(30);
      6 
      7     howtoPrint2(p1);
      8     howtoPrint2(s1);
      9 }

上邊的四種情形,不管我們怎么改變調用方式,始終都是調用的基類的函數,那如何才能解決,當傳入子類對象時調用子類函數,傳入基類對象時調用基類函數呢。

C++提供了多態的解決方案。

  2、多態:

多態是面向對象程序設計的關鍵技術之一。若程序語言不支持多態,不能稱之為面向對象的語言。利用多態技術,可以調用同一個函數名的函數,實現完全不同的功能。

  • C++有兩種多態:
      • 編譯時的多態:通過函數的重載與運算符的重載實現的
      • 運行時的多態:運行時的多態是指在程序運行前,無法根據函數名和參數來確定調用哪一個函數,必須在程序執行過程中,根據執行的具體情況來動態的確定。它是通過類繼承關系和虛函數來實現的。目的是建立一種通用的程序。

  3、虛函數:

  • 虛函數是一個類的成員函數,在類成員函數前添加virtual關鍵字后,該函數就被稱作虛函數。有了虛函數之后就可以根據傳入對象的不同調用不同的成員函數
  • 當在派生類中重新定義虛函數時,不必加關鍵字virtual。但重定義時不僅要同名,而且它的參數表和返回值類型必須全部與基類中的虛函數一樣,否在會出錯。

下邊看看加了虛函數實現多態的結果:

  •  1 class Parent
     2 {
     3 public:
     4     Parent(int a){
     5         this->a = a;
     6     }
     7     virtual printP(){
     8         cout<<"Parent"<<endll;
     9     }
    10 private:
    11     int a;
    12     
    13 };
    14 
    15 class Son:public Parent{
    16 public:
    17     Son(int b):Parent(10){
    18         this->b = b;
    19     }
    20     printP(){ //子類的virtual寫可不寫,只需要父類寫就可以了
    21         cout<<"Son"<<endll;
    22     }
    23 private:
    24     int b;
    25 };

    基類與派生類的的同名函數要想實現多態,基類的同名函數前必須加上virtual關鍵字 

 


  • 下邊調用上邊的四個測試函數看問題有沒有解決:
    • 測試一:
      void
      main(int argc, char const *argv[]) { Parent *p = NULL; Parent p1(20); Son s1(30); p = &p1; p1->printP();//執行父類的打印函數 p = &s1; p->printP();//執行子類的打印函數 } 測試二: void main(int argc, char const *argv[]) { Parent p1(20); Son s1(30); Parent &base = p1;//父類的別名 base.printP();//執行父類 Parent &base2 = s1;//別名 base2.printP();//執行子類 } 測試三: void howtoPrint(Parent *base){ //一個調用語句執行不同的函數 base->printP(); } void main(int argc, char const *argv[]) { Parent p1(20); Son s1(30); howtoPrint(&p1);//父類 howtoPrint(&s1);//子類 } 測試四: void howtoPrint2(Parent &base){//一個調用語句執行不同的函數 base.printP(); } void main(int argc, char const *argv[]) { Parent *p = NULL; Parent p1(20); Son s1(30); howtoPrint2(p1);//父類 howtoPrint2(s1);//子類 }

  4、多態實現的基礎:

      • 1、要有繼承
      • 2、要有虛函數重寫
      • 3、父類指針(引用)指向子類對象

  5、多態的理論基礎:

  • 靜態聯編與動態聯編
    • 聯編:只一個程序塊、代碼之間相互關聯的過程
      • 靜態聯編:指程序的匹配連接在編譯階段實現。函數重載運算符重載都是靜態聯編
      • 動態聯編:指程序聯編推遲到運行時進行,又稱遲聯編。switch,if語句都是動態聯編的例子。
  • 重載、重寫、重定義
    • 函數重載:
      • 函數重載必須在同一個類中進行
      • 子類無法重載父類函數,父類同名函數將被子類同名函數覆蓋
      • 重載是在編譯階段根據參數類型和個數決定函數調用(靜態聯編)
    • 函數重寫:
      • 函數重寫必須發生在子類與父類之間
      • 父類與子類的函數原型完全一樣
      • 使用virtual聲明之后能夠產生多態(如果不寫virtual關鍵字,稱為重定義)

      • 非虛函數重寫是重定義,虛函數重寫是多態

  6、多態實現原理---VPTR指針與虛函數表

主要來看看編譯器在何處動了手腳,從而支持了多態:

從下面的代碼來分析:

  • class Parent{
        public:
            Parent(int a=0){
                this->a = a;}
            virtual void print(){ //編譯器可能動手腳的地方1  
                cout<<"parent"<<endl;}  
        private:
            int a;
    };
    
    class Son:public Parent{
        public:
           Son(int a=0,int b=0):Parent(a){
               this->b = b;}
           void print(){
               cout<<"Son"<<endl;}
        private:
            int b;
        };
    
      void play(Parent *p){ //編譯器可能動手腳的地方2  
            p->print();}
    
        void main(int argc, char const *argv[])
        {
            Parent p; //編譯器可能動手腳的地方3  
            Son s;
            play(&s)
            return 0;
        }
        • 真正綁定關系的地方就是上面代碼的地方3處,就是創建對象的時候。這時候C++編譯器會偷偷地給對象添加一個vptr指針。

 

多態的實現原理詳見下一篇博客

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM