1. 程式人生 > >C++發哥筆記(4):類的繼承

C++發哥筆記(4):類的繼承

rcp 繼承 特點 face 系列 表示 導致 編譯 代碼

繼承
在C++裏,有繼承的語法來表示is kind of的關系

class Tutorial
{
};
class VideoTutorial : public Tutorial
{
};

語法:class B : public A {}
表示類B繼承於類A,把A稱為父類(基類),把B稱為子類(派生類)



當B繼承於A時,則自動地將父類中的所有public成員繼承。
例如,
class Tutorial
{
public:
	char name[32];
	char author[16];
public:
	void ShowInfo();
};
則在VideoTutorial類中也具有了這些成員,而不必顯式寫出。   
VideoTutorial cpp_guide; strcpy(cpp_guide.name, "C/C++學習指南"); strcpy(cpp_guide.author, "邵發"); cpp_guide.ShowInfo(); 註:可以直接在VC下查看該變量的成員 子類只需要把自己的獨有的那部分特性寫出來,父類已有的不需要再顯示了,只需要顯示自己的獨特部分即可,這是重點核心部分,不能忽略這部分。 例如, class VideoTutorial : public Tutorial { public: void Play(); // 播放 public: char url[128]; // 在線觀看的URL地址 int visits; // 播放量 }; 訪問修飾符 protected 在描述繼承關系時,新增一種訪問修飾符 protected(受保護的) 當一個類成員被修飾為protected的時候,有以下規則成立: 1.該成員不能被外部訪問,同private 2.該成員可以被子類繼承,同public 所以,public和protected的成員都能夠被子類繼承
例如,將父類的成員變量聲明為protected
class Tutorial { protected: char name[32]; char author[16]; public: void ShowInfo(); }; 問題 在內存上描述父類和子類的關系: 子類對象的前半部分就是父類對象。 class Parent { public: int a; }; class Child : public Parent { public: int b; }; (1)用sizeof驗證 (2)在內存窗口中直接觀測 問題:父類的private成員變量也會出現在內存中嗎? 是的,父類的所有成員變量都在子類對象中,只是編譯器限制了訪問。 小結 用class B : public A {}表示B繼承於A 當B繼承於A後,父類的所有protected/public成員都被繼承。
什麽叫被繼承?就是這些父類的成員就如同直接寫在子類裏一般。 代碼上可以簡化 虛擬繼承,virtual的用法 函數的重寫 子類可以重寫從父類繼承而來的函數 (overwriting) class Parent { public: void Test(); }; class Child : public Parent { public: void Test(); }; 則 Child ch; ch.Test(); // 調用的是子類的Test()函數 如果重寫的時候,還是要嵌入調用一下父類的函數,怎麽辦? void Child::Test() { Parent::Test(); // 顯式地調用父類的函數 } 父類指針指向子類對象 可以將父類指針指向一個子類的對象,這是完全允許的。 例如, // 左側為Tree*,右側為AppleTree* Tree* p = new AppleTree(); 從普通的邏輯來講,蘋果樹是一種樹,因而可以把AppleTree*視為一種Tree* 從語法本質上講,子類對象的前半部分就是父類,因而可以將子類對象的指針直接轉化為父類。 有父類和子類: class Parent { public: int a; }; class Child : public Parent { public: int b; }; int main() { Child ch; ch.a = 0x11111111; ch.b = 0x22222222; Parent* p = &ch; // p指向的對象是Child* printf("Parent::a = %d \n", p->a); return 0; } 所以,從直觀感覺到內在機理都允許這麽做 問題:考慮以下情況, Parent* p = new Child(); p->Test(); 那麽,此時調用的Test()是父類的、還是子類的? 指針p的類型是Parent* 指針p指向的對象是Child* 調用者的初衷:因為p指向的是對象是子類對象,所以應該調用子類的Test()。 虛擬繼承: virtual 當一個成員函數需要子類重寫,那麽在父類應該將其聲明為virtual。 (有時將聲明為virtual的函數為“虛函數”) 例如 class Parent { public: virtual void Test(); }; virtual本身表明該函數即將被子類重寫。 加virtual關鍵字是必要的。 考慮以下情況, Parent* obj = new Child(); // 語法允許,合乎情理 obj->Test(); 此時,如果Test()在父類中被聲明為virtual,是調用的是子類的Test()。 這解釋了virtual的作用:根據對象的實際類型,調用相應類型的函數。 註意: (1)只需要在父類中將函數聲明為virtual,子類自動地就是virtual了。 (2)即將被重寫的函數添加virtual,是一條應該遵守的編碼習慣。 小結 介紹繼承關系中,對函數重寫後的結果 介紹virtual關鍵字的作用和必要性(父類指針指向子類對象) 繼承:構造與析構 有Child類繼承於 Parent類 class Child : public Parent {} 那麽,當創建一個子類對象時:(編譯器默認動作) 子類對象構造時,先調用父類的構造函數,再調用子類的構造函數。 子類對象析構時,先調用子類的析構函數,再調用父類的構造函數。 在VC中演示: 子類的構造 子類的析構 當父類有多個構造函數,可以顯式的調用其中的一個構造函數。 如果沒有顯式調用,則調用了父類的“默認構造函數” 記住調用方法: Parent(1,1) virtual 析構函數 當一個類被繼承時,應該將父類的析構函數聲明為virtual, 否則會有潛在的問題。 class Parent { virtual ~Parent(){} // 聲明為virtual }; 考慮以下場景: Parent* p = new Child(); delete p; // 此時,調用的是誰的析構函數? 如果析構函數沒有標識為virtual,則有潛在的隱患,並有可能直接導致程序崩潰。(資源沒有被釋放,並引申一系列問題) 類的大小,與 virtual關鍵字的影響 (1) 類的大小由成員變量決定。(這struct的原理相同) 類的大小成員函數的個數無關,即使一個類有10000個成員函數,對它所占的內存空間是沒有影響的。 (2) 但是,如果有一個成員函數被聲明為virtual,那類的大小會有些微的變化。(這個變化由編譯器決定,一般是增加了4個字節) 小結 1.介紹繼承關系中,父類的構造函數和析構函數將被調用。 2.當一個類被別的類繼承時,應該將父類的析構函數聲明為virtual。 (註:如果這個類在設計的時候,已經明確它不會被繼承,則不需要聲明為virtual) 3. 構造函數不能加 virtual 多重繼承 (註:初學者可以跳過這一集,或者聽一下有個印象就行) 定義這個語法的本意:一個孩子有父有母,可以從父母處各自繼承一些特點。 語法: 用Father, Mother表示二個類 class Child : public Father, public Mother { }; 表示Child繼承於Father,Mother 在寫法上,以冒號引導,每個父類用逗號隔開 多重繼承的結果:從所有父類中,繼承他們所有可以被繼承的成員(public/protected) 在VC中展示 多重繼承的問題 多重繼承的問題:很明顯,當多個父類有相同的成員時,會影響沖突。 所以,C++的抽象世界和現實世界是不一樣的。 實際上,多重繼承的理念一般是不會用到的。問題頗多。 (多重繼承的有一個有用的地方,在下一集“純虛函數”中介紹) 小結 1.可以多重繼承,但多重繼承一般是不使用的。(只有一種常見應用場景,在下一章) 2.我們要記住什麽:只需要記住它是怎麽寫的 class Child : public Parent1, public Parent2 { }; 純虛函數,抽象類 什麽是純虛函數 這次課的地位:很重要,設計模式中的概念:接口 但初學者第一次學習時只需要有個印象,等學完了全書再回頭專門學習。 純虛函數的語法: 將成員函數聲明為virtual 後面加上 = 0 該函數沒有函數體 例如, class CmdHandler { public: virtual void OnCommand(char* cmdline) = 0; }; 含有純虛函數的類,稱為抽象類(Abstract Class)(或稱純虛類)。 例如,CmdHandler中有一個純虛函數OnCommand(),因此,它是純虛類。 抽象類不能夠被實例化,即無法創建該對象。 CmdHandler ch; // 編譯錯誤!! CmdHandler* p = new CmdHandler(); // 編譯錯誤! 問題:不能被實例化,還定義這個類做什麽用??? 抽象類的實際作用 抽象類/純虛函數的實際用途:充當的“接口規範” (相當於Java中的interface語法) (用於替代C中的回調函數的用法) 接口規範:凡是遵循此規範的類,都必須實現指定的函數接口。通常是一系列接口。 比如, class CmdHandler { public: virtual void OnCommand(const char* cmdline) = 0; }; 可以理解為:凡是遵循CmdHandler規範的類,都必須實現指定的函數接口:OnCommand() 實例演示 項目需求:用戶輸入一行命令,按回車完成輸入。要求解析命令輸入,並且處理。 設計: CmdInput:用於接收用戶輸入 CmdHandler: 規定一系列函數接口 MyParser: 接口的實現,實際用於解析處理的類 ////////// main.cpp ////////// #include "CmdInput.h" #include "MyParser.h" int main() { CmdInput input; MyParser parser; input.SetHandler(&parser); input.Run(); return 0; } 小結 1.如何定義一個純虛函數 2抽象類的實質作用: 接口規範 因為它只代表了一個規範,並沒有具體實現,所以它不能被實例化。 3. 抽象類通常被多重繼承 比如,一個普通的類,實現了多套接口規範,又繼承於原有的父類。 4. 抽象類的析構函數應該聲明為virtual,因為它是被設計用於繼承的。

C++發哥筆記(4):類的繼承