六、C++的繼承與多型——深入掌握OOP最強大的機制
這一部分內容可以直接看《C++ primer》第十五章,這裡講的基本上都是重複的。第十五章的最後一個小節還有一個綜合性的程式碼案例,包含操作符過載、繼承、多型等等。第十五章的筆記可以看我的另一篇隨筆第十五章 面向物件程式設計
繼承的基本意義
繼承的本質(好處):
-
程式碼的複用;
-
在基類中給所有派生類提供統一的虛擬函式介面,讓派生類進行重寫,然後就能使用多型了。
類和類之間的關係:
- 組合 一部分的關係
- 繼承 一種的關係
總結:1.外部只能訪問物件public的成員,protected和private成員無法直接訪問;2、在整合結構中,派生類從基類可以繼承過來private的成員,但是派生類缺無法直接訪問;3、protected和private的區別?在基類中定義的成員,想被派生類訪問,但是不想被外部訪問,那麼就在基類中把相關成員定義為protected的;如果派生類和外部都不打算訪問,那麼在基類中就把相關成員定義為private的。
預設的繼承方式是什麼?要看派生類使用class定義的還是struct定義的。如果是class定義的派生類,預設繼承方式是private的;如果是struct定義的派生類,預設繼承方式是public的。
派生類的構造過程
派生類怎麼初始化從基類繼承來的成員變數呢?
- 派生類從基類可以繼承來所有的成員(變數和方法),但是不包含建構函式和解構函式
- 通過呼叫基類相應的建構函式來初始化
過載、覆蓋、隱藏
過載關係:
- 一組函式要過載必須處在同一個作用域當中,且函式名字相同,引數列表不同
在基類和派生類中的函式不能被過載,因為作用域不同。如果在基類中定義了過載函式,在派生類中可以直接呼叫,但是如果派生類中定義了與基類同名的函式,在呼叫派生類的時候只會產生派生類中的過載,不會與基類中的同名函式發生過載
隱藏關係:
- 在繼承結構中,派生類的同名成員會把基類的同名成員隱藏起來,
基類和派生類的 型別轉化
只能訪問藍色部分的記憶體,後面紅色部分的記憶體是不存在的,訪問基類指標會報記憶體非法訪問
在繼承結構中,進行上下的型別轉換,預設只支援從下到上的型別轉換。
覆蓋關係:
基類和派生類的方法、返回值、函式名以及引數列表都相同,而且基類的方法是虛擬函式,那麼派生類的方法就自動處理成虛擬函式,他們之間稱為覆蓋關係。
虛擬函式、靜態繫結和動態繫結
虛擬函式virtual。RTTI(run-time type information)執行時的型別資訊。
總結1.如果類裡面定義了虛擬函式,編譯階段編譯器給這個類型別產生一個唯一的vftable虛擬函式表,虛擬函式表中主要儲存的內容就是RTTI指標和虛擬函式的地址。當程式執行時,每一張虛擬函式表都會載入到記憶體的.rodata區。
總結2.一個類裡定義了虛擬函式,那麼這個類定義的物件執行時,記憶體中的開始部分會多儲存一個vfptr虛擬函式指標,指向相應型別的虛擬函式表vftable。一個的型別定義的n個物件,他們的vfptr指向的都是同一張虛擬函式表。
在這個例子裡物件的大小是8個位元組
總結3.一個類裡面虛擬函式的個數不影響物件記憶體大小(Vfptr),影響的是虛擬函式表的大小
class Base{
public:
Base(int data=10): ma(data){}
//虛擬函式 virtual
virtual void show(){ cout<<"Base::show()"<<endl;}
virtual void show(int)(cout<<"Base::show(int)"<<endl;)
protected:
int ma;
};
總結4.如果派生類中的方法和基類繼承來的某個方法,返回值、函式名、引數列表都相同,而且基類的方法是virtual虛擬函式,那麼派生類的這個方法會自動處理成虛擬函式。
派生類中的虛擬函式表,如果派生類中有與基類中重複的函式,會在派生類的虛擬函式表中覆蓋原來基類中的方法地址。
類中存在虛擬函式就會發生動態繫結。
靜態繫結在彙編程式碼中會call具體的方法地址,而動態繫結call一個暫存器,暫存器會在執行時找到虛擬函式表中相應的方法的地址。
虛解構函式
問題1:那些函式不能實現成虛擬函式?
-
虛擬函式依賴:
- 虛擬函式能產生地址,儲存在vftable中
- 物件必須存在(vfptr->vftable->虛擬函式地址)物件存在才有vfptr,才能找到vftable,才有虛擬函式地址
-
建構函式不能成為虛擬函式。建構函式中呼叫虛擬函式,不會發生動態繫結。建構函式呼叫的任何函式都是靜態繫結的
-
靜態成員方法也不能實現成虛擬函式
問題2:虛解構函式
- 解構函式呼叫的時候物件是存在的。
- 基類的解構函式是虛解構函式,派生類的解構函式會自動定義為虛解構函式
什麼時候必須把基類的解構函式必須實現成虛擬函式?
基類的指標(引用)指向堆上new出來的派生類物件的時候,delete pb(基類的指標),他呼叫解構函式的時候,必須發生動態繫結,否則會導致派生類的解構函式無法呼叫
為什麼要使用虛解構函式?
解釋這個問題使用的程式碼:
//
// Created by 26685 on 2022-05-17 19:41.
// Description:ClassDerive.h 學習繼承和多型
//
#ifndef C___CLASSDERIVE_H
#define C___CLASSDERIVE_H
#include <iostream>
using namespace std;
class Base{
public:
Base(int data=10):ma(data){
cout<<"Base()"<<endl;
}
~Base(){
cout<<"~Base()"<<endl;
}
virtual void show(){
cout<<"Base::show()"<<endl;
}
private:
int ma;
};
class Derive:public Base{
public:
Derive(int data=10): Base(data),mb(data),ptr(new int(data)){
cout<<"Derive()"<<endl;
}
~Derive(){
cout<<"~Derive()"<<endl;
}
void show() override{
cout<<"Derive::show()"<<endl;
}
private:
int mb;
int* ptr;//會指向額外的空間,必須由解構函式釋放
};
#endif //C___CLASSDERIVE_H
如果在堆上開闢記憶體存放派生類會發生不呼叫派生類的解構函式的情況,程式碼如下:
int main(){
Base *pb=new Derive;
pb->show();
delete pb;
return 0;
}
輸出的結果只有基類的解構函式:
這裡的pb是Base型別,會去Base類中找解構函式,此時的繫結就是靜態繫結。
基類的解構函式如果是虛擬函式,派生類中的解構函式會自動生成為虛解構函式。所以要將解構函式定義為虛解構函式。
virtual Base::~Base(){
cout<<"~Base()"<<endl;
}
Derive::~Derive(){
cout<<"~Derive()"<<endl;
delete ptr;
}
此時就會正確的析構:
再談虛擬函式和動態繫結
問題:是不是虛擬函式的呼叫一定就是動態繫結? 答:不是的
在類的建構函式當中呼叫虛擬函式是靜態繫結。
用物件本身呼叫虛擬函式是靜態繫結
動態繫結發生在指標呼叫虛擬函式的情況下:
理解多型是什麼
面試中常見的問題:如何解釋多型
動態的多型:
靜態的多型:
函式的過載
理解抽象類
擁有純虛擬函式的類叫做抽象類,抽象類不能再例項化物件,但是可以定義指標和引用變數
virtual void 函式名()=0;
定義純虛擬函式
一般把什麼類設計為抽象類?
基類