c++ 繼承與多態, 虛基類,虛函數,純虛函數

繼承與多態

繼承與派生

c++通過類派生來支持繼承,被繼承的類型稱爲基類(baseclass)或超類(superclass),而產生的類爲派生類(derived class)或子類(subclass)。基類和派生類的集合稱作類的層次結構(hierarchy).

定義:

class 派生類名 : 訪問限定符 基類名1<,訪問限定符 基類名2, ... , 訪問限定符 基類名n>{

//成員表  新增的或替代父類的成員
}

同一個派生類可以同時繼承多個基類,稱爲多重繼承(multi-inheritance)

單繼承(single-inheritance): 一個派生類只繼承一個基類

先討論單繼承:

編寫派生類的4個步驟:

  1. 吸收基類的成員除構造函數,析構函數,運算符重載函數,友元函數外所有的數據成員和函數成員全都成爲派生類的成員
  2. 改造基類成員,當有的基類成員在新的應用中不合適時,可以進行改造,如果派生類聲明一個和某個基類成員同名的成員,派生類中的成員會屏蔽基類同名的成員,類似函數中的局部變量屏蔽全局變量如果是成員函數,參數表和返回值完全一樣時,稱爲同名覆蓋(override),否則爲重載
  3. 發展新成員,增加新的數據成員和函數成員,更適合新的應用
  4. 重寫構造函數和析構函數
繼承方式

繼承方式分爲三種,公有方式public,保護方式protected,私有方式private,如不顯示的給出繼承方式關鍵字,則默認爲私有繼承

在一層繼承關係中,private和protected在行爲上完全相同,但是當有兩層或多層繼承時,新保護派生的派生類可訪問底層基類的公有和保護成員,而私有繼承不可訪問。

簡單理解:會將基類中的訪問限定符使用繼承限定符進行進一步的訪問限定

  • public 繼承會保留原訪問形式
  • priotected 會將public 進一步限定爲protected
  • private 會將public 和protected進一步限定爲private
派生方式 基類中的訪問限定符 在派生類中對基類的訪問限定 在派生類對象外訪問派生類對象的基類成員
公有派生 public public public (可直接訪問) 可直接訪問
公有派生 public protected protected(可直接訪問) 不可直接訪問
公有派生 public private 不可直接訪問 不可直接訪問
私有派生 private public private(可直接訪問) 不可直接訪問
私有派生 private protected private(可直接訪問) 不可直接訪問
私有派生 private private 不可直接訪問 不可直接訪問
構造和析構函數

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

派生類名:: 派生類 (參數總表): 基類名1(參數名錶)<, 基類名2(參數名錶), ..., 基類名2(參數名錶)><,成員對象名1(成員對象參數名錶)1, ... >){
// 新增成員的初始化}

在類體的聲明中不需要寫":"後面的部分。
派生類構造函數各部分的執行次序:

  1. 調用基類構造函數,按他們在派生類定義中的先後順序依次調用, 若未顯示聲明則默認調用基類中的無參構造函數
  2. 調用新增成員對象的構造函數,按他們在類定義中排列的先後順序依次調用
  3. 派生類的構造函數體中的初始化操作

在析構函數中只要處理好新增的成員就好。對於新增的成員對象和基類中的成員,系統會調用成員對象和基類的析構函數。析構函數各部分的執行次序與構造函數相反。

在實際中,成員對象的使用或者聚合是一種完善的封裝,推薦將數據,數據的操作,資源的動態分配與釋放封裝在一個完善的子系統中,就像string。

虛基類

對於多繼承(環狀繼承),A->D, B->D, C->(A,B),例如:

class D{......};
class B: public D{......};
class A: public D{......};
class C: public B, public A{.....};

這個繼承會使D創建兩個對象,要解決上面問題就要用虛繼承方式:
將class D這個共同基類設置爲虛基類,這樣從不同路徑中繼承來的同名數據成員在內存中就合併爲1個。
格式:class 類名: virtual 繼承方式 父類名
其中,virtual關鍵字支隊緊隨其後的基類名起作用。

class D{......};
class B: virtual public D{......};
class A: virtual public D{......};
class C: public B, public A{.....};

在虛基類對象的創建中,步驟如下:

  1. 虛基類的構造函數
  2. 非虛基類的構造函數,按照聲明順序
  3. 成員對象的構造函數
  4. 派生類自己的構造函數
    實例:
#include <iostream>

using namespace std;
//基類

class D
{
public:
    D(){cout<<"D()"<<endl;}
    ~D(){cout<<"~D()"<<endl;}
protected:
    int d;
};

class B:virtual public D
{
public:
    B(){cout<<"B()"<<endl;}
    ~B(){cout<<"~B()"<<endl;}
protected:
    int b;
};

class A:virtual public D
{
public:
    A(){cout<<"A()"<<endl;}
    ~A(){cout<<"~A()"<<endl;}
protected:
    int a;
};

class C:public B, public A
{
public:
    C(){cout<<"C()"<<endl;}
    ~C(){cout<<"~C()"<<endl;}
protected:
    int c;
};

int main()
{
    cout << "Hello World!" << endl;
    C c;   //D, B, A ,C
    cout<<sizeof(c)<<endl; //一共有4個int 值,字節爲24 .
    system("pause");
    return 0;
}

派生類的應用討論

  1. 賦值兼容規則
    對於公有派生,派生類所有的訪問限定和基類一樣,其接口也全盤接受。這樣只要基類能解決的問題,公有派生類都可以解決。在任何需要基類對象的地方都可以用公有派生類的對象來代替,這一規則稱爲賦值兼容規則。包括以下情況:
    1. 派生類的對象可以賦值給基類的對象,這是把派生類從對象基類中繼承來的成員賦值給基類對象。 反過來不行
    2. 可以將一個派生類對象的地址賦值給其基類的指針變量,但只能訪問派生類中有基類繼承而來的成員,不能訪問派生類中的新成員。 反過來也不行
    3. 派生類對象可以初始化基類對象的引用。

賦值兼容規則下的自定義賦值構造函數:
調用基類的賦值構造函數,在對新增成員完成賦值

Person:: Person(Person &ps){
IdPerson = ps.IdPerson;
Sex=ps.Sex
}

Student:: Student(Student &Std): Person(Std){
NoStudent=Std.NoStudent;
}

同樣的,對於重載的複製賦值操作符,也實在定義函數體中先調用基類的複製賦值操作符

Person& Person:: operator= (Person &ps){
IdPerson = ps.IdPerson;
Sex=ps.Sex
}

Student& Student:: operator= (Student &Std){
this->Person::operator=(Std);
NoStudent=Std.NoStudent;
}

注意:一定要將資源的動態分配和釋放封裝在對象中,這樣按語義進行賦值是完全可以的。否則回引起資源的污染和重複析構,這涉及到指針的深複製和淺複製的問題。

多態性與虛函數

多態性:

  • 靜態的多態性: 函數的重載和運算符的重載
  • 運行時的多態性:以虛函數爲基礎,考慮在不同層次的類(繼承)中同名的成員函數之間的關係問題

運行時的動態性:在程序執行前,無法根據函數名和參數來確定調用哪一個函數,必須在執行過程中根據執行的具體情況來動態的確定

虛函數的定義:
virtual 返回類型 函數名(參數列表){...}

虛函數在該類中派生的所有類中都保持虛函數的特性,在派生類中重新定義該虛函數時,可以不加關鍵字virtual,但重新定義時不僅要同名,而且參數列表和返回值類型全部與基類中的虛函數一樣。

虛函數:與同名覆蓋不同的是在基類中多了一個virtual關鍵字
同名覆蓋:都相同
重載:參數列表不同

通過對象訪問時虛函數的行爲與同名覆蓋完全相同,不同的是當使用基類的指針或引用訪問時(基類指針指向派生類對象), 此時調用虛函數,執行的是派生類中的定義。


class father
{
public:
    virtual void foo()
    {
        cout<<"father::foo() is called"<<endl;
    }
};
class son:public father
{
public:
    void foo()
    {
        cout<<"son::foo() is called"<<endl;
    }
};
int main(void)
{
    father *a = new son();
    father->foo();   // 在這裏,a雖然是指向A的指針,但是被調用的函數(foo)卻是B的!
    return 0;
}

純虛函數
  1. 純虛函數是在基類中聲明的虛函數,它在基類中沒有定義,但要求任何派生類都要定義自己的實現方法。在基類中實現純虛函數的方法是在虛函數原型後加"=0"
    格式:
    virtual 返回類型 函數名(參數表)=0

2、引入原因
  1、爲了方便使用多態特性,需要在基類中定義虛函數。
  2、在很多情況下,基類本身生成對象是不合情理的。例如,動物作爲一個基類可以派生出老虎、孔雀等子類,但動物本身生成對象明顯不合常理。
  
  爲了解決上述問題,引入了純虛函數的概念,將函數定義爲純虛函數,則編譯器要求在派生類中必須予以重寫以實現多態性。同時含有純虛擬函數的類稱爲抽象類它不能生成對象。使派生類僅僅只是繼承函數的接口。
  
聲明瞭純虛函數的類是一個抽象類。用戶不能創建類的實例,只能創建它的派生類的實例。
純虛函數最顯著的特徵是:它們必須在繼承類中重新聲明函數(不要後面的=0,否則該派生類也不能實例化),而且它們在抽象類中沒有定義。

class A{
    public:
          virtual void f() = 0;
          void g(){ this->f(); }
          A(){}
};
class B:public A{
      public:
          void f(){ cout<<"B:f()"<<endl;}
};
int main(){
    B b;
    b.g();
    return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章