C++ 三大特性 封裝,繼承,多態
封裝
定義:封裝就是將抽象得到的數據和行為相結合,形成一個有機的整體,也就是將數據與操作數據的源代碼進行有機的結合,形成類,其中數據和函數都是類的成員,目的在於將對象的使用者和設計者分開,
以提高軟件的可維護性和可修改性
特性:1. 結合性,即是將屬性和方法結合 2. 信息隱蔽性,利用接口機制隱蔽內部實現細節,只留下接口給外界調用 3. 實現代碼重用
繼承
定義:繼承就是新類從已有類那里得到已有的特性。 類的派生指的是從已有類產生新類的過程。原有的類成為基類或父類,產生的新類稱為派生類或子類,
子類繼承基類后,可以創建子類對象來調用基類函數,變量等
單一繼承:繼承一個父類,這種繼承稱為單一繼承,一般情況盡量使用單一繼承,使用多重繼承容易造成混亂易出問題
多重繼承:繼承多個父類,類與類之間要用逗號隔開,類名之前要有繼承權限,假使兩個或兩個基類都有某變量或函數,在子類中調用時需要加類名限定符如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; //如果基類沒有虛析構函數,那么就只會調用父類的析構函數,只刪除了對象內的父類部分,造成一個局部銷毀,可能導致資源泄露
} //注:只有當此類希望成為 基類時才會打算聲明一個虛析構函數,否則不必要給此類聲明一個虛函數