繼承與多態
繼承與派生
c++通過類派生來支持繼承,被繼承的類型稱爲基類(baseclass)或超類(superclass),而產生的類爲派生類(derived class)或子類(subclass)。基類和派生類的集合稱作類的層次結構(hierarchy).
定義:
class 派生類名 : 訪問限定符 基類名1<,訪問限定符 基類名2, ... , 訪問限定符 基類名n>{
//成員表 新增的或替代父類的成員
}
同一個派生類可以同時繼承多個基類,稱爲多重繼承(multi-inheritance)
單繼承(single-inheritance): 一個派生類只繼承一個基類
先討論單繼承:
編寫派生類的4個步驟:
- 吸收基類的成員, 除構造函數,析構函數,運算符重載函數,友元函數外所有的數據成員和函數成員全都成爲派生類的成員
- 改造基類成員,當有的基類成員在新的應用中不合適時,可以進行改造,如果派生類聲明一個和某個基類成員同名的成員,派生類中的成員會屏蔽基類同名的成員,類似函數中的局部變量屏蔽全局變量。 如果是成員函數,參數表和返回值完全一樣時,稱爲同名覆蓋(override),否則爲重載
- 發展新成員,增加新的數據成員和函數成員,更適合新的應用
- 重寫構造函數和析構函數
繼承方式
繼承方式分爲三種,公有方式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, ... >){
// 新增成員的初始化}
在類體的聲明中不需要寫":"後面的部分。
派生類構造函數各部分的執行次序:
- 調用基類構造函數,按他們在派生類定義中的先後順序依次調用, 若未顯示聲明則默認調用基類中的無參構造函數
- 調用新增成員對象的構造函數,按他們在類定義中排列的先後順序依次調用
- 派生類的構造函數體中的初始化操作
在析構函數中只要處理好新增的成員就好。對於新增的成員對象和基類中的成員,系統會調用成員對象和基類的析構函數。析構函數各部分的執行次序與構造函數相反。
在實際中,成員對象的使用或者聚合是一種完善的封裝,推薦將數據,數據的操作,資源的動態分配與釋放封裝在一個完善的子系統中,就像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{.....};
在虛基類對象的創建中,步驟如下:
- 虛基類的構造函數
- 非虛基類的構造函數,按照聲明順序
- 成員對象的構造函數
- 派生類自己的構造函數
實例:
#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;
}
派生類的應用討論
- 賦值兼容規則
對於公有派生,派生類所有的訪問限定和基類一樣,其接口也全盤接受。這樣只要基類能解決的問題,公有派生類都可以解決。在任何需要基類對象的地方都可以用公有派生類的對象來代替,這一規則稱爲賦值兼容規則。包括以下情況:- 派生類的對象可以賦值給基類的對象,這是把派生類從對象基類中繼承來的成員賦值給基類對象。 反過來不行
- 可以將一個派生類對象的地址賦值給其基類的指針變量,但只能訪問派生類中有基類繼承而來的成員,不能訪問派生類中的新成員。 反過來也不行
- 派生類對象可以初始化基類對象的引用。
賦值兼容規則下的自定義賦值構造函數:
調用基類的賦值構造函數,在對新增成員完成賦值
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;
}
純虛函數
- 純虛函數是在基類中聲明的虛函數,它在基類中沒有定義,但要求任何派生類都要定義自己的實現方法。在基類中實現純虛函數的方法是在虛函數原型後加"=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;
}