1. C++多態

多態就是多種形態,C++的多態分爲靜態多態與動態多態。

這裏寫圖片描述

函數重載就是一個簡單的靜態多態,靜態多態是編譯器在編譯期間完成的,編譯器會根據實參類型來選擇調用合適的函數,如果有合適的函數可以調用就調,沒有的話就會發出警告或者報錯

 

動態多態: 它是在程序運行時根據基類的引用(指針)指向的對象來確定自己具體該調用哪一個類的虛函數。

動態多態就是通過繼承重寫基類的虛函數實現的多態,因爲是在運行時確定,所以稱爲動態多態。運行時在虛函數表中尋找調用函數的地址。在基類的函數前加上virtual關鍵字,在派生類中重寫該函數,運行時將會根據對象的實際類型來調用相應的函數。

如果對象類型是子類,就調用子類的函數;如果對象類型是父類,就調用父類的函數,(即指向父類調父類,指向子類調子類)此爲多態的表現。

轉載一篇相關的好的博客:

https://blog.csdn.net/qq_36359022/article/details/81870219

 

1. 多態:zl-81

2. C和C++區別:zl-1

3. const關鍵字:

// 1.修飾變量,表示變量是常量,不希望改變其值
const int a = 10;
// 2.修飾變通函數參數,表示此參數是傳入參數,不希望在函數內改變其值,參數類型常爲指針或引用
void foo(const int * p, const int &ref);
// 3.修飾函數返回類型,表示函數返回的是常量
const int foo();
// 4.用於類成員函數聲明之後,表示該成員函數不會改變類的屬性
class Test{
public:
    ...
    void foo() const;
    ...
};
// 5.用於類特定成員函數參數,通常爲拷貝構造函數等
class Test{
public:
    Test(const Test &other);
    Test operator = (const Test &other);
    ...
};
// 6.修飾指針
const int a = 10;
int b = 20;
const int * p = &a;  // 此時p爲指向a的指針,a值不可修改,但p可以修改爲b的地址
 
const int * const p = &a;  // 此時a的值不可修改,p的指向也不可修改

     推薦一篇好的關於const的博客:https://blog.csdn.net/u011333734/article/details/81294043

4. malloc/free 和 new/delete區別:zl-22~25

      關於operator new的博客:https://blog.csdn.net/ly930156123/article/details/78855379

5. 指針和引用的區別:zl-12

6. C++中堆和棧的區別,四個階段,一些內存結構:zl

7. 關鍵字static:《C和指針》44

8. 在C++程序中調用被C語言修飾的函數,爲什麼要加extern “C”:zl-63

9. 如何防止頭文件被重複包含:zl-30

10. 什麼是內存泄漏?什麼是野指針?什麼是內存越界?如何避免?:1zl-27

 野指針,也就是指向不可用內存區域的指針。如果對野指針進行操作,將會使程序發生不可預知的錯誤,甚至可能直接引起崩潰。野指針不是NULL指針,是指向“垃圾”內存的指針。人們一般不會錯用NULL指針,因爲用if語句很容易判斷。但是野指針是很危險的,也具有很強的掩蔽性,if語句對它不起作用。

       造成野指針的常見原因有三種:

        1、指針變量沒有被初始化。任何指針變量剛被創建時不會自動成爲NULL指針。在Debug模式下,VC++編譯器會把未初始化的棧內存上的指針全部填成 0xcccccccc ,當字符串看就是 “燙燙燙燙……”;會把未初始化的堆內存上的指針全部填成 0xcdcdcdcd,當字符串看就是 “屯屯屯屯……”。把未初始化的指針自動初始化爲0xcccccccc或0xcdcdcdcd,而不是就讓取隨機值,那是爲了方便我們調試程序,使我們能夠一眼就能確定我們使用了未初始化的野指針。在Release模式下,編譯器則會將指針賦隨機值,它會亂指一氣。所以,指針變量在創建時應當被初始化,要麼將其設置爲NULL,要麼讓它指向合法的內存。

        2、指針指向的內存被釋放了,而指針本身沒有置NULL。對於堆內存操作,我們分配了一些空間(使用malloc函數、calloc函數或new操作符),使用完後釋放(使用free函數或delete操作符)。指針指向的內存被釋放了,而指針本身沒有置NULL。通常會用語句if (p != NULL)進行防錯處理。很遺憾,此時if語句起不到防錯作用。因爲即便p不是NULL指針,它也不指向合法的內存塊。所以在指針指向的內存被釋放後,應該將指針置爲NULL。

        3 、指針超過了變量的作用範圍。即在變量的作用範圍之外使用了指向變量地址的指針。這一般發生在將調用函數中的局部變量的地址傳出來引起的。這點容易被忽略,雖然代碼是很可能可以執行無誤,然而卻是極其危險的。局部變量的作用範圍雖然已經結束,內存已經被釋放,然而地址值仍是可用的,不過隨時都可能被內存管理分配給其他變量。

指針懸掛:如果一個地方指針既不爲空,也沒有指向一個已知的對象,這樣的指針稱爲懸掛指針。它指向了一塊沒有分配給用戶使用的內存。

int *p1 = new int(10);

int *p2 = new int(10);

... ...

p1 = p2;  //將p2賦值給p1,即p1也指向了p2所指的內存單元

... ...

delete p1;  //釋放的是p2指向的內存單元,p1開始指向的地址再也找不回來了,因此無法釋放,產生了指針懸掛問題

deletep2;  //再次釋放p2指向的內存單元,將會產生嚴重錯誤

--------------------------------------------------------------------------------------------------------------------------------------

11. 描述一下封裝、繼承、多態:

封裝:封裝就是將抽象得到的數據和行爲相結合,形成一個有機的整體,也就是將數據與操作數據的源代碼進行有機的結合,形成類,其中數據和函數都是類的成員,目的在於將對象的使用者和設計者分開,以提高軟件的可維護性和可修改性

  特性:1. 結合性,即是將屬性和方法結合    

                  2. 信息隱蔽性,利用接口機制隱蔽內部實現細節,只留下接口給外界調用  

                  3. 實現代碼重用。

繼承:就是新類從已有類那裏得到已有的特性。 類的派生指的是從已有類產生新類的過程。原有的類成爲基類或父類,產生的新類稱爲派生類或子類,子類繼承基類後,可以創建子類對象來調用基類函數,變量等

    單一繼承:繼承一個父類,這種繼承稱爲單一繼承,一般情況儘量使用單一繼承,使用多重繼承容易造成混亂易出問題

    多重繼承:繼承多個父類,類與類之間要用逗號隔開,類名之前要有繼承權限,假使兩個或兩個基類都有某變量或函數,在子類中調用時需要加類名限定符如c.a::i = 1;

    菱形繼承:多重繼承摻雜隔代繼承1-n-1模式,此時需要用到虛繼承,例如 B,C虛擬繼承於A,D再多重繼承B,C,否則會出錯。

    繼承權限:繼承方式規定了如何訪問繼承的基類的成員。繼承方式指定了派生類成員以及類外對象對於從基類繼承來的成員的訪問權限;繼承權限:子類繼承基類除構造和析構函數以外的所有成員,繼承可以擴展已存在的代碼,目的也是爲了代碼重用

   繼承也分爲接口繼承和實現繼承,如下

   普通成員函數的接口總是會被繼承: 子類繼承一份接口和一份強制實現

   普通虛函數被子類重寫     :  子類繼承一份接口和一份缺省實現

   純虛函數只能被子類繼承接口  :  子類繼承一份接口,沒有繼承實現

   訪問權限圖如下:

         

多態:可以簡單概括爲“一個接口,多種方法”,即用的是同一個接口,但是效果各不相同,多態有兩種形式的多態,一種是靜態多態,一種是動態多態。

動態多態:    是指在程序運行時才能確定函數和實現的鏈接,此時才能確定調用哪個函數,父類指針或者引用能夠指向子類對象,調用子類的函數,所以在編譯時是無法確定調用哪個函數。使用時在父類中寫一個虛函數,在子類中分別重寫,用這個父類指針調用這個虛函數,它實際上會調用各自子類重寫的虛函數。

運行期多態的設計思想要歸結到類繼承體系的設計上去。對於有相關功能的對象集合,我們總希望能夠抽象出它們共有的功能集合,在基類中將這些功能聲明爲虛接口(虛函數),然後由子類繼承基類去重寫這些虛接口,以實現子類特有的具體功能。

運行期多態的實現依賴於虛函數機制。當某個類聲明瞭虛函數時,編譯器將爲該類對象安插一個虛函數表指針,併爲該類設置一張唯一的虛函數表,虛函數表中存放的是該類虛函數地址。運行期間通過虛函數表指針與虛函數表去確定該類虛函數的真正實現。

 優點: 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時會先調用子類的析構函數,然後再調用基類的析構函數,成功刪除

 delete p;       

  //如果基類沒有虛析構函數,那麼就只會調用父類的析構函數,只刪除了對象內的父類部分,

     造成一個局部銷燬,可能導致資源泄露    

 return 0;            

} //注:只有當此類希望成爲 基類時纔會打算聲明一個虛析構函數,否則不必要給此類聲明一個虛函數。

--------------------------------------------------------------------------------------------------------------------------------------

12.  如何理解智能指針,什麼時候改變引用計數:zl-71

幾篇好的博文:

https://blog.csdn.net/qq_27717921/article/details/82940519

https://blog.csdn.net/qq_27717921/article/details/83043278

https://blog.csdn.net/shaosunrise/article/details/85228823?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task

https://blog.csdn.net/solstice/article/details/8547547

https://www.cnblogs.com/J1ac/p/9826591.html

http://c.biancheng.net/view/1480.html

13. share_ptr與weak_ptr的區別與聯繫:zl-71

https://blog.csdn.net/weixin_41066529/article/details/89480260

--------------------------------------------------------------------------------------------------------------------------------------

 

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