C/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,那麼它的內存分佈如下圖所示:
在這裏插入圖片描述
假設 obj_b 的起始地址爲 0X1100,那麼它的內存分佈如下圖所示:

在這裏插入圖片描述
在派生類的對象模型中,會包含所有基類的成員變量。這種設計方案的優點是訪問效率高,能夠在派生類對象中直接訪問基類變量,無需經過好幾層間接計算;所有成員函數仍然存儲在另外一個區域——代碼區,由所有對象共享。
以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 的內核對象容器及鏈表如下圖所示:
在這裏插入圖片描述
從面向對象的觀點,可以認爲每一種具體對象是抽象對象的派生,繼承了基本對象的屬性並在此基礎上擴展了與自己相關的屬性。下圖則顯示了 RT-Thread 中各類內核對象的派生和繼承關係:
在這裏插入圖片描述
通過這種內核對象的設計方式,RT-Thread 做到了不依賴於具體的內存分配方式,系統的靈活性得到極大的提高。
RT-Thread的內核架構請看下邊的文章:
https://blog.csdn.net/sinat_31039061/article/details/104121771

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

聯繫作者:
歡迎關注本人公衆號:

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章