C++ 三大特性 封裝,繼承,多態編程
封裝函數
定義:封裝就是將抽象獲得的數據和行爲相結合,造成一個有機的總體,也就是將數據與操做數據的源代碼進行有機的結合,造成類,其中數據和函數都是類的成員,目的在於將對象的使用者和設計者分開,優化
以提升軟件的可維護性和可修改性ui
特性:1. 結合性,便是將屬性和方法結合 2. 信息隱蔽性,利用接口機制隱蔽內部實現細節,只留下接口給外界調用 3. 實現代碼重用this
繼承 spa
定義:繼承就是新類從已有類那裏獲得已有的特性。 類的派生指的是從已有類產生新類的過程。原有的類成爲基類或父類,產生的新類稱爲派生類或子類,設計
子類繼承基類後,能夠建立子類對象來調用基類函數,變量等指針
單一繼承:繼承一個父類,這種繼承稱爲單一繼承,通常狀況儘可能使用單一繼承,使用多重繼承容易形成混亂易出問題調試
多重繼承:繼承多個父類,類與類之間要用逗號隔開,類名以前要有繼承權限,假使兩個或兩個基類都有某變量或函數,在子類中調用時須要加類名限定符如c.a::i = 1;對象
菱形繼承:多重繼承摻雜隔代繼承1-n-1模式,此時須要用到虛繼承,例如 B,C虛擬繼承於A,D再多重繼承B,C,不然會出錯
繼承權限:繼承方式規定了如何訪問繼承的基類的成員。繼承方式指定了派生類成員以及類外對象對於從基類繼承來的成員的訪問權限
繼承權限:子類繼承基類除構造和析構函數之外的全部成員
繼承能夠擴展已存在的代碼,目的也是爲了代碼重用
繼承也分爲接口繼承和實現繼承:
普通成員函數的接口老是會被繼承: 子類繼承一份接口和一份強制實現
普通虛函數被子類重寫 : 子類繼承一份接口和一份缺省實現
純虛函數只能被子類繼承接口 : 子類繼承一份接口,沒有繼承實現
訪問權限圖以下:
爲了便於理解,僞代碼以下,注意這個例子編譯是不過的,僅是爲了能夠更簡潔的說明繼承權限的做用:
class Animal //父類
{
public:
void eat(){
cout<<"animal eat"<<endl;
}
protected:
void sleep(){
cout<<"animal sleep"<<endl;
}
private:
void breathe(){
cout<<"animal breathe"<<endl;
}
};
class Fish:public Animal //子類
{
public:
void test() {
eat(); //此時eat()的訪問權限爲public,在類內部可以訪問
sleep(); //此時sleep()的訪問權限爲protected,在類內部可以訪問
breathe(); //此時breathe()的訪問權限爲no access,在類內部不可以訪問
}
};
int main(void) {
Fish f;
f.eat(); //此時eat()的訪問權限爲public,在類外部可以訪問
f.sleep(); //此時sleep()的訪問權限爲protected,在類外部不可以訪問
f.breathe() //此時breathe()的訪問權限爲no access,在類外部不可以訪問
}
多態
定義:能夠簡單歸納爲「一個接口,多種方法」,即用的是同一個接口,可是效果各不相同,多態有兩種形式的多態,一種是靜態多態,一種是動態多態
動態多態: 是指在程序運行時才能肯定函數和實現的連接,此時才能肯定調用哪一個函數,父類指針或者引用可以指向子類對象,調用子類的函數,因此在編譯時是沒法肯定調用哪一個函數
使用時在父類中寫一個虛函數,在子類中分別重寫,用這個父類指針調用這個虛函數,它實際上會調用各自子類重寫的虛函數。
運行期多態的設計思想要歸結到類繼承體系的設計上去。對於有相關功能的對象集合,咱們總但願可以抽象出它們共有的功能集合,在基類中將這些功能聲明爲虛接口(虛函數),
而後由子類繼承基類去重寫這些虛接口,以實現子類特有的具體功能。
運行期多態的實現依賴於虛函數機制。當某個類聲明瞭虛函數時,編譯器將爲該類對象安插一個虛函數表指針,併爲該類設置一張惟一的虛函數表,虛函數表中存放的是該類虛函數地址。
運行期間經過虛函數表指針與虛函數表去肯定該類虛函數的真正實現。
優勢: OO設計重要的特性,對客觀世界直覺認識; 可以處理同一個繼承體系下的異質類集合
vector<Animal*>anims;
Animal * anim1 = new Dog;
Animal * anim2 = new Cat;
//處理異質類集合
anims.push_back(anim1);
anims.push_back(anim2);
缺點:運行期間進行虛函數綁定,提升了程序運行開銷;龐大的類繼承層次,對接口的修改易影響類繼承層次;因爲虛函數在運行期才綁定,因此編譯器沒法對虛函數進行優化
虛函數
定義:用virtual關鍵字修飾的函數,本質:由虛指針和虛表控制,虛指針指向虛表中的某個函數入口地址,就實現了多態,做用:實現了多態,虛函數能夠被子類重寫,虛函數地址存儲在虛表中
虛表:虛表中主要是一個類的虛函數的地址表,這張表解決了繼承,覆蓋的問題,保證其真實反應實際的函數,當咱們用父類指針來指向一個子類對象的時候,虛表指明瞭實際所應調用的函數
基類有一個虛表,能夠被子類繼承,(當類中有虛函數時該類纔會有虛表,該類的對象纔有虛指針,子類繼承時也會繼承基類的虛表),子類若是重寫了基類的某虛函數,那麼子類繼承於基類的虛表中該虛函數的地址也會相應改變,指向子類
自身的該虛函數實現,若是子類有本身的虛函數,那麼子類的虛表中就會增長該項,編譯器爲每一個類對象定義了一個虛指針,來定位虛表,因此雖然是父類指針指向子類對象,但由於此時子類
重寫了該虛函數,該虛函數地址在子類虛表中的地址已經被改變了,因此它實際調用的是子類的重寫後的函數,正是因爲每一個對象調用的虛函數都是經過虛表指針來索引的,也就決定了虛表指針的
正確初始化是很是重要的,便是說,在虛表指針沒有正確初始化以前,咱們是不能調用虛函數的,由於生成一個對象是構造函數的工做,因此設置虛指針也是構造函數的工做,編譯器在構造函數
的開頭部分祕密插入能初始化虛指針的代碼, 在構造函數中進行虛表的建立和虛指針的初始化
一但虛指針被初始化爲指向相應的虛表,對象就「知道」它本身是什麼類型,但只有當虛函數被調用時這種自我認知纔有用
類中若沒有虛函數,類對象的大小正好是數據成員的大小,包含有一個或者多個虛函數的類對象。編譯器會向裏面插入一個虛指針,指向虛表,這些都是編譯器爲咱們作的,咱們徹底沒必要關心
這些,全部有虛函數的類對象的大小是數據成員的大小加一個虛指針的大小;對於虛繼承,若子類也有本身的虛函數,則它自己須要有一個虛指針,指向本身的虛表,另外子類繼承基類時,
首先要經過加入一個虛指針來指向基類,所以可能會有兩個或多個虛指針(多重繼承會多個),其餘狀況通常是一個虛指針,一張虛表
每個帶有virtual函數的類都有一個相應的虛表,當對象調用某一virtual函數時,實際被調用的函數取決於該對象的虛指針所指向的那個虛表-編譯器在其中尋找適當的函數指針。
效率漏洞:咱們必須明白,編譯器正在插入隱藏代碼到咱們的構造函數中,這些隱藏代碼不只必須初始化虛指針,並且還必須檢查this的值(以避免operator new返回零)和調用基類構造函數。放在一塊兒,
這些代碼能夠影響咱們認爲是一個小內聯函數的調用,特別是,構造函數的規模會抵消函數調用代價的減小,若是作大量的內聯函數調用,代碼長度就會增加,而在速度上沒有任何好處,
固然,也許不會當即把全部這些小構造函數都變成非內聯,由於它們更容易寫爲內聯構造函數,可是,當咱們正在調整咱們的代碼時,請務必去掉這些內聯構造函數
虛函數使用:將函數聲明爲虛函數會下降效率,通常函數在編譯期其相對地址是肯定的,編譯器能夠直接生成imp/invoke指令,若是是虛函數,那麼函數的地址是動態的,譬如取到的地址在eax寄存
器裏,則在call eax以後的那些已經被預取到流水線的全部指令都將失效, 流水線越長,那麼一次分支預測失敗的代價越大,建議若不打算讓某類成爲基類,那麼類中最好不要出現虛函數,
純虛函數:含有至少一個純虛函數的類叫抽象類,由於抽象類含有純虛函數,因此其虛表是不健全的,在虛表不健全的狀況下是不能實例化對象的,子類繼承抽象基類後必須重寫基類的全部純虛函數
不然子類仍爲純虛函數子類將抽象基類的純虛函數所有重寫後會將虛表完善,此時子類才能實例化對象,純虛函數只聲明不定義,形如 virtual void print() = 0
靜態多態:是在編譯期就把函數連接起來,此時便可肯定調用哪一個函數或模板,靜態多態是由模板和重載實現的,在宏多態中,是經過定義變量,編譯時直接把變量替換,實現宏多態
優勢: 帶來了泛型編程的概念,使得C++擁有泛型編程與STL這樣的武器; 在編譯期完成多態,提升運行期效率; 具備很強的適配性和鬆耦合性,(耦合性指的是兩個功能模塊之間的依賴關係)
缺點: 程序可讀性下降,代碼調試帶來困難;沒法實現模板的分離編譯,當工程很大時,編譯時間不可小覷 ;沒法處理異質對象集合
調用基類指針建立子類對象,那麼基類應該有虛析構函數,由於若是基類沒有虛析構函數,那麼在刪除這個子類對象的時候會調用錯誤的析構函數而致使刪除失敗產生不明確行爲,
int main() {
Base *p = new Derive(); //調用基類指針建立子類對象,那麼基類應有虛析構函數,否則當刪除的時候會調用錯誤的析構函數而致使刪除失敗產生不明確行爲,
delete p; //刪除子類對象時,若是基類有虛析構函數,那麼delete時會先調用子類的析構函數,而後再調用基類的析構函數,成功刪除
return 0; //若是基類沒有虛析構函數,那麼就只會調用父類的析構函數,只刪除了對象內的父類部分,形成一個局部銷燬,可能致使資源泄露
} //注:只有當此類但願成爲 基類時纔會打算聲明一個虛析構函數,不然沒必要要給此類聲明一個虛函數