1. 程式人生 > 其它 >c++基礎知識點(二)

c++基礎知識點(二)

11.new/delete與malloc/free的區別

malloc/free為C的標準庫函式

new、delete則為C++的操作運算子

(1)malloc開闢空間型別大小需手動計算,new是由編譯器自己計算;

(2)malloc返回型別為void*,必須強制型別轉換對應型別指標,new則直接返回對應型別指標;

(3)malloc開闢記憶體時返回記憶體地址要檢查判空,因為若它可能開闢失敗會返回NULL;new則不用判斷,因為記憶體分配失敗時,它會丟擲異常bac_alloc,可以使用異常機制;

(4)無論釋放幾個空間大小,free只傳遞指標,多個物件時delete需加[]

malloc/free為函式只是開闢空間並釋放,new/delete則不僅會開闢空間,並呼叫建構函式和解構函式進行初始化和清理

12.記憶體洩露的情況

定義:
   簡單來說,記憶體洩漏就是程式在申請一個記憶體空間後沒有釋放,直到程式執行結束後才釋放。這樣看起來似乎沒什麼大問題,但是如果程式會持續執行很長時間(例如伺服器),並且可能在程每次呼叫某個部分的時候都會申請一個記憶體空間,那麼長久以來的後果是可想而知的:當程式希望再次申請一塊空間時,發現已經沒有free的部分了,最終導致系統崩潰。

情況:
   記憶體洩漏可能發生在如下幾種條件下:

   1. 類的構造和解構函式不合理。

顯式 --- 很容易想到的情況,使用new在堆裡建立物件後,沒有delete。

隱式 --- 在建構函式或者其他方法體內動態分配記憶體,但是在解構函式中沒有釋放    說明: 以上幾種情況可以通過自定義構造和解構函式解決。

   2. 巢狀的物件指標未清除

   3. 指向物件的指標陣列和物件陣列的區別   

物件陣列 --- 一個數組中儲存多個物件,注意釋放時新增 [] 符號。

指向物件的指標陣列 --- 僅僅釋放陣列是不夠的,陣列中的每個元素都指向一個物件,需要一次釋放元素所指物件。    說明: 2 和 3 都是定義未明確時容易出現的錯誤。

   4. 同一個記憶體地址被釋放兩次    

缺少拷貝建構函式 --- 如果類成員有指標,那麼在C++中的預設拷貝建構函式同時也會複製指標,從而導致有兩個指標指向同一個地址,那麼在釋放拷貝者物件與被拷貝者物件的時候,同一個地址會被釋放兩次,這是不允許的。需要程式設計師自己新增拷貝建構函式,防止這種問題的出現。    

缺少賦值運算子過載函式 --- 和前面一種情況類似,只不過賦值運算子過載函式呼叫的時機與拷貝建構函式不同,解決方法也是程式設計師自己新增賦值運算子過載函式。    說明: 如果一個程式語句生成了一個新的物件例項,那麼它就會呼叫拷貝建構函式;如果這條語句是賦值語句(例如 B = A;)而非初始化語句(例如 ClassB B = A;),那麼它還會呼叫賦值運算子過載函式。注意,這兩者是有可能在同一個語句中呼叫的(例如 B = f1(A);)。

   5. 基類指標指向子類物件時(例如 Parent *p = son;),如果基類的解構函式沒有宣告為虛擬函式,那麼在直接釋放指標 p 時,不會呼叫 Class Son {} 中的解構函式,也就導致派生類物件 son 沒有釋放。

13.sizeof與strlen對比

例1:char ss[100]=”0123456789”;

Sizeof(ss)結果為100,ss表示在記憶體中預分配的大小,100*1;

Strlen(ss)結果為10,它的內部實現是用一個迴圈計算字串的長度,直到”\0”為止。

例2:int ss[100]=”0123456789”;

Sizeof(ss)結果為400,ss表示在記憶體中的大小,100*4;

Strlen(ss)錯誤,strlen的引數只能是char*,且必須是以”\0”結尾的。

總結sizeof和strlen的區別

⒈sizeof操作符的結果型別是size_t,它在標頭檔案中的typedef為unsigned int 型別。該型別保證能容納實現所建立的最大物件的位元組大小。

⒉sizeof是運算子,strlen是函式。

⒊sizeof可以用型別做引數,strlen只能用char*做引數,且必須是以”\0”結尾的。sizeof還可以用函式做引數,比如: short f();

printf(“%d\n”,sizeof(f()));

輸出的結果是sizeof(short),即2。

⒋陣列做sizeof的引數不退化,傳遞給strlen就退化為指標。

⒌大部分編譯程式在編譯的時候就把sizeof計算過了,是型別或是變數的長度。這就是sizeof(x)可以用來定義陣列位數的原因。

charstr[20]=”0123456789”;

inta=strlen(str); //a=10;

intb=sizeof(str); //b=20;

⒍strlen的結果要在執行的時候才能計算出來,用來計算字串的長度,而不是型別佔記憶體的大小。

14、指標與引用的區別

相同點:
指標與引用都是地址的概念,指標指向的是一塊記憶體,它的內容是這塊記憶體的地址;引用是某塊記憶體的別名。

區別:
1.指標是一個實體,而引用僅僅只是一個別名。
2.引用使用時不需要解引用‘*’,指標使用時需要解引用。
3.引用只能在定義的時候被初始化一次,之後不能再改變作為別的變數的別名,而指標可以在任意時候改變它的指向(const指標除外)。
4.引用的物件必須是有效的一個變數,不能為空;指標可以有空指標。
5.sizeof引用得到的是引用物件的大小,而sizeof指標是指標所指向物件的地址的大小,在32位平臺下指標的大小為4個位元組,在64位平臺下指標的大小為8個位元組。
6.指標與引用++的含義不一樣,引用++是將引用的物件的值++,指標是在該地址上往後指標指向物件大小的空間,比如該物件是int,則往後4個位元組的空間。

15、野指標產生與避免

1.定義

指向非法的記憶體地址指標叫作野指標(Wild Pointer).

2.出現野指標的常見情形

  2.1使用未初始化的指標

  出現野指標最典型的情形就是在定義指標變數之後沒有對它進行初始化

  2.2指標所指的物件已經消亡

  指標指向某個物件之後,當這個物件的生命週期已經結束,物件已經消亡後,仍使用指標訪問該物件,將出現執行時錯誤。

  2.3指標釋放後之後未置空

  指標p被free或者delete之後,沒有置為NULL,讓人誤以為p是個合法的指標。對指標進行free和delete,只是把指標所指的記憶體空間給釋放掉,但並沒有把指標本身置空,此時指標指向的就是“垃圾”記憶體。    釋放後的指標應立即將指標置為NULL,防止產生野指標。

3.如何避免野指標的出現

野指標有時比較隱蔽,編譯器不能發現,為了防止野指標帶來的危害,開發人員應該注意以下幾點。
(1)C++引入了引用機制,如果使用引用可以達到程式設計目的,就可以不必使用指標。因為引用在定義的時候,必須初始化,所以可以避免野指標的出現。

(2)如果一定要使用指標,那麼需要在定義指標變數的同時對它進行初始化操作。定義時將其置位NULL或者指向一個有名變數。

16、多型:動態多型、靜態多型

動態多型

動態多型的設計思想:對於相關的物件型別,確定它們之間的一個共同功能集,然後在基類中,把這些共同的功能宣告為多個公共的虛擬函式介面。各個子類重寫這些虛擬函式,以完成具體的功能。客戶端的程式碼(操作函式)通過指向基類的引用或指標來操作這些物件,對虛擬函式的呼叫會自動繫結到實際提供的子類物件上去。

從上面的定義也可以看出,由於有了虛擬函式,因此動態多型是在執行時完成的,也可以叫做執行期多型,這造就了動態多型機制在處理異質物件集合時的強大威力(當然,也有了一點點效能損失)。

靜態多型

靜態多型本質上就是模板的具現化。靜態多型中的介面呼叫也叫做隱式介面,相對於顯示介面由函式的簽名式(也就是函式名稱、引數型別、返回型別)構成,隱式介面通常由有效表示式組成。

17、虛擬函式實現動態多型的原理、虛擬函式與純虛擬函式的區別

C++的多型性用一句話概括就是:

在基類的函式前加上virtual關鍵字,在派生類中重寫該函式,執行時將會根據物件的實際型別來呼叫相應的函式。如果物件型別是派生類,就呼叫派生類的函式;如果物件型別是基類,就呼叫基類的函式

1:用virtual關鍵字申明的函式叫做虛擬函式,虛擬函式肯定是類的成員函式。

2:存在虛擬函式的類都有一個一維的虛擬函式表叫做虛表,類的物件有一個指向虛表開始的虛指標。虛表是和類對應的,虛表指標是和物件對應的。

3:多型性是一個介面多種實現,是面向物件的核心,分為類的多型性和函式的多型性。

4:多型用虛擬函式來實現,結合動態繫結.

5:純虛擬函式是虛擬函式再加上 = 0;

6:抽象類是指包括至少一個純虛擬函式的類。

總結(基類有虛擬函式的):

  1:每一個類都有虛表

  2:虛表可以繼承,如果子類沒有重寫虛擬函式,那麼子類虛表中仍然會有該函式的地址,只不過這個地址指向的是基類的虛擬函式實現,如果基類有3個虛擬函式,那麼基類的虛表中就有三項(虛擬函式地址),派生類也會虛表,至少有三項,如果重寫了相應的虛擬函式,那麼虛表中的地址就會改變,指向自身的虛擬函式實現,如果派生類有自己的虛擬函式,那麼虛表中就會新增該項。

  3:派生類的虛表中虛地址的排列順序和基類的虛表中虛擬函式地址排列順序相同。

  這就是c++中的多型性,當c++編譯器在編譯的時候,發現Father類的Say()函式是虛擬函式,這個時候c++就會採用晚繫結技術,也就是編譯時並不確定具體呼叫的函式,而是在執行時,依據物件的型別來確認呼叫的是哪一個函式,這種能力就叫做c++的多型性,我們沒有在Say()函式前加virtual關鍵字時,c++編譯器就確定了哪個函式被呼叫,這叫做早期繫結。

18、繼承時,父類的解構函式是否為虛擬函式?建構函式能不能為虛擬函式?為什麼?

1. 為什麼建構函式不能為虛擬函式?
虛擬函式的呼叫需要虛擬函式表指標,而該指標存放在物件的內容空間中;若建構函式宣告為虛擬函式,那麼由於物件還未建立,還沒有記憶體空間,更沒有虛擬函式表地址用來呼叫虛擬函式——構造函數了。

2. 為什麼解構函式可以為虛擬函式,如果不設為虛擬函式可能會存在什麼問題?
首先解構函式可以為虛擬函式,而且當要使用基類指標或引用呼叫子類時,最好將基類的解構函式宣告為虛擬函式,否則可以存在記憶體洩露的問題。

19、靜態多型:重寫、過載、模板

一:過載
過載指的是在同一個作用域內,兩函式的函式名可以相同,但是引數不能完全相同,可以是引數型別不同或者是引數個數不同,至於返回值,不影響過載。如何實現過載?

C++程式碼在編譯時會根據引數列表對函式進行重新命名,例如void Test(int a, int b)會被重新命名為_Test_int_int,void Test(int x, double y)會被重新命名為_Test_int_double。所以說函式過載從底層上看它們還是不同的函式。
二:重定義
重定義也叫隱藏,指的是在繼承關係中,子類實現了一個和父類名字一樣的函式,(只關注函式名,和引數與返回值無關)這樣的話子類的函式就把父類的同名函式隱藏了。

三:重寫
重寫指的在繼承關係中,子類中定義了一個與父類極其相似的虛擬函式。
具體怎麼相似:函式名必須相同,引數列表必須相同,返回值可以不相同,但是必須是父子關係的指標或引用。

通過重寫,可以實現動態多型,何為動態多型,就是當父類的指標或引用指向被重寫的虛擬函式時,父類的指標或引用指向誰就呼叫誰的虛擬函式,而不是說根據型別。
在這裡,如果去掉父類的虛擬函式的virtual,則構不成多型,如果去掉子類虛擬函式的virtual可以構成多型,可以理解為編譯器優化。

20、static關鍵字:修飾區域性變數、全域性變數、類中成員變數、類中成員函式

(1)修飾全域性變數隱藏。當我們同時編譯多個檔案時,所有未加static字首的全域性變數和函式都具有全域性可見性。

(2)修飾區域性變數保持變數內容的持久。儲存在靜態資料區的變數會在程式剛開始執行時就完成初始化,也是唯一的一次初始化。

(3)static修飾的變數預設初始化為0

(4)c++中的類成員宣告static

在類中宣告static變數或者函式時,初始化時使用作用域運算子來標明它所屬類,因此,靜態資料成員是類的成員,而不是物件的成員

(1)類的靜態成員函式是屬於整個類而非類的物件,所以它沒有this指標,這就導致 了它僅能訪問類的靜態資料和靜態成員函式。

(2)不能將靜態成員函式定義為虛擬函式。

(3)靜態資料成員是靜態儲存的,所以必須對它進行初始化。(程式設計師手動初始化,否則編譯時一般不會報錯,但是在Link時會報錯誤)

(4)靜態成員初始化與一般資料成員初始化不同:

初始化在類體外進行,而前面不加static,以免與一般靜態變數或物件相混淆;
初始化時不加該成員的訪問許可權控制符private,public等;
初始化時使用作用域運算子來標明它所屬類;
所以我們得出靜態資料成員初始化的格式:
<資料型別><類名>::<靜態資料成員名>=<值>

int Person::num=250

(5)為了防止父類的影響,可以在子類定義一個與父類相同的靜態變數,以遮蔽父類的影響。這裡有一點需要注意:我們說靜態成員為父類和子類共享,但我們有重複定義了靜態成員,這會不會引起錯誤呢?不會,我們的編譯器採用了一種絕妙的手法:name-mangling 用以生成唯一的標誌。