天天看點

C/C++面向對象程式設計之繼承

C/C++面向對象程式設計

    • 目錄:
    • 1、什麼是繼承與派生
    • 2、繼承時的對象記憶體模型
    • 3、C語言運用繼承的思想

目錄:

C/C++面向對象程式設計之封裝

C/C++面向對象程式設計之繼承

C/C++面向對象程式設計之多态

1、什麼是繼承與派生

繼承(Inheritance) 可以了解為一個類從另一個類擷取成員變量和成員函數的過程。例如類 B 繼承于類 A,那麼 B 就擁有 A 的成員變量和成員函數。

派生(Derive) 和繼承是一個概念,隻是站的角度不同。繼承是兒子接收父親的産業,派生是父親把産業傳承給兒子。

被繼承的類稱為父類或基類,繼承的類稱為子類或派生類。“子類”和“父類”通常放在一起稱呼,“基類”和“派生類”通常放在一起稱呼。

派生類除了擁有基類的成員,還可以定義自己的新成員,以增強類的功能。

以下是兩種典型的使用繼承的場景:

  1. 當你建立的新類與現有的類相似,隻是多出若幹成員變量或成員函數時,可以使用繼承,這樣不但會減少代碼量,而且新類會擁有基類的所有功能。
  2. 當你需要建立多個類,它們擁有很多相似的成員變量或成員函數時,也可以使用繼承。可以将這些類的共同成員提取出來,定義為基類,然後從基類繼承,既可以節省代碼,也友善後續修改成員

2、繼承時的對象記憶體模型

以C++為例:

沒有繼承時對象記憶體的分布情況。這時的記憶體模型很簡單,成員變量和成員函數會分開存儲:

  • 對象的記憶體中隻包含成員變量,存儲在棧區或堆區(使用 new 建立對象);
  • 成員函數與對象記憶體分離,存儲在代碼區。

有繼承關系時,派生類的記憶體模型可以看成是基類成員變量和新增成員變量的總和,而所有成員函數仍然存儲在另外一個區域——代碼區,由所有對象共享。

請看下面的代碼:

#include <cstdio>
//基類A
class A{
public:
    A(int a, int b);
public:
    void display();
protected:
    int m_a;
    int m_b;
};
A::A(int a, int b): m_a(a), m_b(b){}
void A::display(){
    printf("m_a=%d, m_b=%d\n", m_a, m_b);
}

//派生類B
class B: public A{
public:
    B(int a, int b, int c);
    void display();
private:
    int m_c;
};
B::B(int a, int b, int c): A(a, b), m_c(c){ }
void B::display(){
    printf("m_a=%d, m_b=%d, m_c=%d\n", m_a, m_b, m_c);
}

int main(){
    A obj_a(99, 10);
    B obj_b(84, 23, 95);
    obj_a.display();
    obj_b.display();
	obj_b.A::display();//通路基類的同名函數
    return 0;
}

/*輸出:
m_a=99, m_b=10
m_a=84, m_b=23, m_c=95
m_a=84, m_b=23
*/
           

注意到派生類中B的成員和基類A中的成員有重名函數display(),這在C++裡叫名字遮蔽,所謂遮蔽,就是在派生類中使用該成員(包括在定義派生類時使用,也包括通過派生類對象通路該成員)時,實際上使用的是派生類新增的成員,而不是從基類繼承來的。

派生類的對象如果通路一個名字在派生類的成員内無法找到,編譯器會智能的繼續到基類作用域中查找該名字的定義;如果派生類的對象想通路基類的同名名字,需要加上類名和域解析符。

記憶體模型:

obj_a 是基類對象,obj_b 是派生類對象。假設 obj_a 的起始位址為 0X1000,那麼它的記憶體分布如下圖所示:

C/C++面向對象程式設計之繼承

假設 obj_b 的起始位址為 0X1100,那麼它的記憶體分布如下圖所示:

C/C++面向對象程式設計之繼承

在派生類的對象模型中,會包含所有基類的成員變量。這種設計方案的優點是通路效率高,能夠在派生類對象中直接通路基類變量,無需經過好幾層間接計算;所有成員函數仍然存儲在另外一個區域——代碼區,由所有對象共享。

以C語言為例:

在C語言的世界裡裡結構體不能包含函數體,每個功能函數的名字必須不同,是以自然沒有像C++那樣函數重載的功能和繼承時的名字遮蔽問題。C語言裡結構體隻能包含變量,在派生類的對象模型中,會包含所有基類的成員變量。

C語言沒有C++那樣智能,C語言如果想通路基類成員,必須加上基類的對象名。

請看下面的代碼:

#include <stdio.h>
//基類A
struct A{
    int m_a;
    int m_b;
    void (*display)(struct A *a);
};
void a_display(struct A *a){
    printf("m_a=%d, m_b=%d\n", a->m_a, a->m_b);
}
//派生類B
struct B{
	struct A parent;
    int m_c;
    void (*display)(struct B *b);
};
void b_display(struct B *b){
    printf("m_a=%d, m_b=%d, m_c=%d\n", b->parent.m_a, b->parent.m_b, b->m_c);
}

int main(){
    struct A obj_a={99, 10,a_display};
    struct B obj_b={99, 10, a_display,95,b_display};
    obj_a.display(&obj_a);
    obj_b.display(&obj_b);
	obj_b.parent.display(&obj_a);
    return 0;
}
/*輸出:
m_a=99, m_b=10
m_a=99, m_b=10, m_c=95
m_a=99, m_b=10
*/
           

C語言的記憶體模型比較簡單,去除掉C++的函數體,就是C語言的記憶體模型。

3、C語言運用繼承的思想

在C語言中,可以利用“結構在記憶體中的布局與結構的聲明具有一緻的順序”這一事實實作繼承。利用繼承的思想,C語言很容易就能實作較為複雜的代碼。

RT-Thread 采用核心對象管理系統來通路 / 管理所有核心對象,核心對象包含了核心中絕大部分設施,這些核心對象可以是靜态配置設定的靜态對象,也可以是從系統記憶體堆中配置設定的動态對象。

RT-Thread 核心對象包括:線程,信号量,互斥量,事件,郵箱,消息隊列和定時器,記憶體池,裝置驅動等。對象容器中包含了每類核心對象的資訊,包括對象類型,大小等。對象容器給每類核心對象配置設定了一個連結清單,所有的核心對象都被連結到該連結清單上,如圖 RT-Thread 的核心對象容器及連結清單如下圖所示:

C/C++面向對象程式設計之繼承

從面向對象的觀點,可以認為每一種具體對象是抽象對象的派生,繼承了基本對象的屬性并在此基礎上擴充了與自己相關的屬性。下圖則顯示了 RT-Thread 中各類核心對象的派生和繼承關系:

C/C++面向對象程式設計之繼承

通過這種核心對象的設計方式,RT-Thread 做到了不依賴于具體的記憶體配置設定方式,系統的靈活性得到極大的提高。

RT-Thread的核心架構請看下邊的文章:

https://blog.csdn.net/sinat_31039061/article/details/104121771

參考資料:http://c.biancheng.net/cplus/

聯系作者:

歡迎關注本人公衆号:

C/C++面向對象程式設計之繼承

繼續閱讀