1. 程式人生 > >C++語言特性(長期更新)

C++語言特性(長期更新)

C++是一門需要不斷實踐的語言,因為他的各種特性,程式設計技巧實在太多了,多到幾乎難以完全掌握。可以說,學的越深入,用的越多,越發現自己的無知。所以,僅此記錄自己在學習C++過程中或疑惑不解,或認知出錯,或驚為天人的一些語言特性、程式設計技巧與底層內涵!

  • 擁有虛擬函式的類會有一個虛表,而且這個虛表存放在類定義模組的資料段中。模組的資料段通常存放定義在該模組的全域性資料和靜態資料,這樣我們可以把虛表看作是模組的全域性資料或者靜態資料,類的虛表會被這個類的所有物件所共享。類的物件可以有很多,但是他們的虛表指標都指向同一個虛表,從這個意義上說,我們可以把虛表簡單理解為類的靜態資料成員。值得注意的是,雖然虛表是共享的,但是虛表指標並不是,類的每一個物件有一個屬於它自己的虛表指標。虛表中存放的是虛擬函式的地址。
  • 表格中的virtual functions地址是如何被建構起來的?在C++中,virtual functions(可經由其class object被呼叫)可以在編譯時期獲知。此外,這一組地址是固定不變的,執行期不可能新增或替換之。由於程式執行時,表格的大小和內容都不會改變,所以其建構和存取皆可以由編譯器完全掌控,不需要執行期的任何介入。
  • 虛表指標則是在進入建構函式主體前被初始化的,(這個工作是編譯器做的,對程式設計師來說是透明的),我們可以把建構函式的呼叫過程細分為兩個階段,即:1.進入到建構函式體之間。在這個階段如果存在虛擬函式的話,虛表指標被初始化。如果存在建構函式的初始化列表的話,初始化列表也會被執行。2.進入到建構函式體內。這一階段是我們通常意義上說的建構函式。
  • 由於虛擬函式的呼叫需要靠虛表指標獲取虛擬函式地址,因此如果將建構函式宣告為虛擬函式,會導致在虛表指標未初始化前即試圖使用,從而產生錯誤。而解構函式則一般要宣告為虛擬函式,在子類構造時,會先執行父類預設建構函式,再執行子類建構函式,而析構則相反,如果解構函式不為虛擬函式,則在多型(父類指標指向子類物件)析構時,會因無法動態繫結而只調用父類解構函式而不呼叫子類解構函式。
  • C++在公有繼承時,子類會繼承父類的所有成員變數與方法(包括私有變數與私有成員函式),只是對於父類的私有成員,子類並無訪問許可權,因此在子類中這部分被隱藏了,這可以用一個空類繼承來驗證,通過檢視sizeof(繼承的空類的值)可以判斷子類是否繼承了父類的私有變數。
  • sizeof(非繼承的空類) = 1,這是因為例項化的原因(空類同樣可以被例項化),每個例項在記憶體中都有一個獨一無二的地址,為了達到這個目的,編譯器往往會給一個空類隱含的加一個位元組,這樣空類在例項化後在記憶體得到了獨一無二的地址,所以空類所佔的記憶體大小是1個位元組。而當類非空時,使用sizeof計算將不會將這一個隱藏位元組計入結果中。
  • 對於delete與delete [] 常理中的認識是單個變數使用delete陣列則使用delete [],但實際上,對於基本型別(如float,int,double)delete與delete []的效果是一樣的:
int *p = new int[10];
delete p;   //is same to delete [] p

單對於非基本型別,如物件陣列,如果僅僅只是delete就可能會產生問題,因為對於物件的delete還需要不同於基本型別的一步——呼叫解構函式,因此如果只是delete將只會呼叫陣列中第一個元素的解構函式,這樣就有可能造成記憶體洩漏。

  • 如果在C++類構造造函式中使用了初始化列表的語法,那麼初始化的順序與成員變數宣告順序相同,而與初始化列表的順序無關,比如下圖的初始化順序為n2,n1而非n1,n2
class A    
{    
private:    
    int n2;    
    int n1; 

public:    
    A() :n1(0), n2(0){}   
};
  • C++使用inline來宣告行內函數,這是為了在函式呼叫處能夠直接展開函式程式碼而避免函式語句較少卻因引數傳遞等代價帶來的效率損失,但要注意的是,inline的宣告只是使用者希望這個函式內聯,而編譯器有權忽略這個請求。比如當函式語句十分長時,這時候函式的引數傳遞等開銷對函式執行本身而言微不足道,如果還是在呼叫處展開會造成程式碼體積的膨脹,從而增大指令的儲存空間,得不償失,因此,對於教長的函式,編譯器可能會忽略inline的請求,事實上行內函數也不應該用於較多語句的函式。
  • 如果行內函數的宣告在標頭檔案中則標頭檔案中不僅要包含 inline 函式的宣告,而且必須包含定義,且在定義時必須加上 inline 。【關鍵字 inline 必須與函式定義體放在一起才能使函式成為內聯,僅將 inline 放在函式宣告前面不起任何作用】
  • inline 函式可以定義在原始檔中,但多個原始檔中的同名 inline 函式的實現必須相同。一般把 inline 函式的定義放在標頭檔案中更加合適。
  • 定義在Class宣告內的成員函式預設是inline函式:
class A {   public:  void Foo(int x, int y) { ... }   // 自動地成為行內函數   } 
  • 結構體記憶體對齊的3大規則:
    1.對於結構體的各個成員,第一個成員的偏移量是0,排列在後面的成員其當前偏移量必須是當前成員型別的整數倍
    2.結構體內所有資料成員各自記憶體對齊後,結構體本身還要進行一次記憶體對齊,保證整個結構體佔用記憶體大小是結構體內最大資料成員的最小整數倍
    3.如程式中有#pragma pack(n)預編譯指令,則所有成員對齊以n位元組為準(即偏移量是n的整數倍),不再考慮當前型別以及最大結構體內型別

  • C++傳遞引數是如果是按值傳遞,則會產生一個臨時的變數副本(如果傳遞一個有拷貝建構函式的物件則會呼叫拷貝建構函式來構造這個物件),如果是按引用傳遞,則直接傳遞所引用變數的地址。

  • C++返回值與傳遞類似,如果返回的是一個引用,則直接返回所引用變數的本身(可能是通過返回地址實現,不同編譯器實現可能不同),如果返回值非引用,則可能會在函式返回的函式處(比如A函式呼叫B函式,那麼B函式返回的函式為A)分配空間建立一個臨時變數(同樣會呼叫拷貝建構函式),也可能直接將返回值存入暫存器(比如對於比較簡單的基本型別)同樣不同編譯器實現不同。
  • #include<> 和 #include”” 只是最先搜尋的路經不一樣。#include<> :表示只從從標準庫檔案目錄下搜尋,對於標準庫檔案搜尋效率快。#include”” :表示首先從使用者工作目錄下開始搜尋,對於自定義檔案搜尋比較快,然後搜尋整個磁碟(標準庫)。
  • 在類中 new A與new A()都會呼叫預設建構函式,但不同的是,當沒有為類A提供預設構造或解構函式時(即類A的構造與解構函式由編譯器生成,此時認為A是POD型別),則new A()會自動對成員變數初始化(比如把int初始化為0),當為類A提供了預設建構函式,即使提供的預設建構函式什麼都不做,此時認為類A是non-POD型別,則new A不會對成員變數初始化。
  • 當一個變數被明確宣告為陣列時,比如int a[5];則sizeof(a)計算的是陣列所有元素所佔的位元組,上述例子結果為45=20 但當陣列是用指標來描述的時候,比如int *p = new int[5];則sizeof計算的是指標的位元組而非陣列所有元素所佔的位元組,此外,陣列的傳參一般也是以指標形式描述,一般有int a[],int size;int *a兩種寫法,後一種sizeof計算的依然是指標,而前一種,由於並不知道陣列a的長度,因此大部分的實現中sizeof還是將a認為是指標併產生一個warning告訴你sizeof(a)將以指標的方式計算。
  • 當類中含有其他類成員變數(這個成員類有預設建構函式)時,若這個類沒有預設建構函式,則編譯器自動產生的預設建構函式將會呼叫這個類成員變數的預設建構函式,如果使用者定義了預設建構函式卻沒有在其中顯式呼叫類成員變數的預設建構函式,則編譯器會自動插入呼叫成員類預設建構函式的程式碼段,並且是在插入在原預設建構函式體的所有執行語句之前,當有多個類成員變數(均有預設建構函式)時,插入的順序與宣告順序相同。