1. 程式人生 > >c++重要知識點總結

c++重要知識點總結

1、記憶體分配的方式:

分配方式有三種:

1.靜態儲存區,是在程式編譯時就已經分配好記憶體,在整個執行期間都存在,如全域性變數、常量。

2.棧上分配,函式內的區域性變數就是從棧上分配的,但分配的記憶體容量有限。

3.堆上分配,也稱動態分配,如我們用newmalloc分配記憶體,用deletefree來釋放記憶體。

2、記憶體分配的注意事項

  用newmalloc分配記憶體時,必須要對此指標賦初值。

  用deletefree釋放記憶體後,必須要將指標指向NULL

  不能修改指向常量的指標資料

3、Sizeof的問題

  C++無法知道指標所指物件的大小,指標的大小永遠為4位元組

char a[]=

Hello World!

char *p=a;

count sizeof(a) end; //12位元組

count sizeof(p) endl; //4位元組

而且,在函式中,陣列引數退化為指標,所以下面的內容永遠輸出為4

void fun(char a[1000])

{

count sizeof(a) endl; //輸出4而不是1000

}

4、關於指標

  1、 指標建立時必須被初始化

  2、 指標在free delete後必須置為NULL

  3、 指標的長度都為4位元組

  4、釋放記憶體時,如果是陣列指標,必須要釋放掉所有的記憶體,如

char *p=new char[100];

strcpy(p,

Hello World);

delete []p; //注意前面的[]

p=NULL; //置為NULL

5、關於malloc/free new /delete

malloc/free C/C+的記憶體分配符,new /deleteC++的記憶體分配符。

注意:malloc/free是庫函式,new/delete是運算子

l malloc/free不能執行建構函式與解構函式,而new/delete可以

l new/delete不能在C上執行,所以malloc/free不能被淘汰

兩者都必須要成對使用

l C++中可以使用_set_new_hander函式來定義記憶體分配異常的處理

6、標頭檔案作用

加強安全檢測

通過標頭檔案可能方便地呼叫庫功能,而不必關心其實現方式

#IFNDEF/#DEFINE/#ENDIF有什麼作用

仿止該標頭檔案被重複引用

7、C++程式的效能瓶頸

1)缺頁:如第四章中所述,缺頁往往意味著需要訪問外部儲存。因為外部儲存訪問相對於訪問記憶體或者程式碼執行,有數量級的差別。因此只要有可能,應該儘量想辦法減少缺頁。

2)從堆中動態申請和釋放記憶體:如C語言中的malloc/freeC++語言中的new/delete操作非常耗時,因此要儘可能優先考慮從執行緒棧中獲得記憶體。優先考慮棧而減少從動態堆中申請記憶體,不僅僅是因為在堆中開闢記憶體比在棧中要慢很多,而且還與“儘量減少缺頁”這一宗旨有關。當執行程式時,當前棧幀空間所在的記憶體頁肯定在實體記憶體中,因此程式程式碼對其中變數的存取不會引起缺頁;相反,從堆中生成的物件,只有指向它的指標在棧上,物件本身卻是在堆中。堆一般來說不可能都在實體記憶體中,而且因為堆分配記憶體的特性,即使兩個相鄰生成的物件,也很有可能在堆記憶體位置上相隔很遠。因此當訪問這兩個物件時,雖然分別指向它們指標都在棧上,但是通過這兩個指標引用它們時,很有可能會引起兩次“缺頁”。

3)複雜物件的建立和銷燬:這往往是一個層次相當深的遞迴呼叫,因為一個物件的建立往往只需要一條語句,看似很簡單。另外,編譯器生成的臨時物件因為在程式的原始碼中看不到,更是不容易察覺,因此尤其值得警惕和關注。本章中專門有兩節分別講解物件的構造和析構,以及臨時物件。

4)函式呼叫:因為函式呼叫有固定的額外開銷,因此當函式體的程式碼量相對較少,且該函式被非常頻繁地呼叫時,函式呼叫時的固定額外開銷容易成為不必要的開銷。C語言的巨集和C++語言的行內函數都是為了在保持函式呼叫的模組化特徵基礎上消除函式呼叫的固定額外開銷而引入的,因為巨集在提供效能優勢的同時也給開發和除錯帶來了不便。在C++中更多提倡的是使用行內函數,本章會有一節專門講解行內函數。

   主要是缺頁(缺頁後需要訪問外部儲存),堆中動態申請和釋放記憶體(由於分配的記憶體的不連續性導致缺頁),複雜物件的建立和銷燬(層次深的遞迴呼叫),函式呼叫

8、建構函式與解構函式

建構函式和解構函式的特點是當建立物件時,自動執行建構函式;當銷燬物件時,解構函式自動被執行。這兩個函式分別是一個物件最先和最後被執行的函式,建構函式在建立物件時呼叫,用來初始化該物件的初始狀態和取得該物件被使用前需要的一些資源,比如檔案/網路連線等;解構函式執行與建構函式相反的操作,主要是釋放物件擁有的資源,而且在此物件的生命週期這兩個函式都只被執行一次。

解構函式:

建立一個物件一般有兩種方式,一種是從執行緒執行棧中建立,也稱為“區域性物件”,一般語句為:

{

        ……

        Object obj;             

        ……

}                               

銷燬這種物件並不需要程式顯式地呼叫解構函式,而是當程式執行出該物件所屬的作用域時自動呼叫。比如上述程式中在①處建立的物件obj在②處會自動呼叫該物件的解構函式。在這種方式中,物件obj的記憶體在程式進入該作用域時,編譯器生成的程式碼已經為其分配(一般都是通過移動棧指標),①句只需要呼叫物件的建構函式即可。②處編譯器生成的程式碼會呼叫該作用域內所有區域性的使用者自定義型別物件的解構函式,物件obj屬於其中之一,然後通過一個退棧語句一次性將空間返回給執行緒棧。

另一種建立物件的方式為從全域性堆中動態建立,一般語句為:

{

        ……

        Object* obj = new Object;   

        ……

        delete obj;                 

         ……

}                                   

當執行①句時,指標obj所指向物件的記憶體從全域性堆中取得,並將地址值賦給obj。但指標obj本身卻是一個區域性物件,需要從執行緒棧中分配,它所指向的物件從全域性堆中分配記憶體存放。從全域性堆中建立的物件需要顯式呼叫delete銷燬,delete會呼叫該指標指向的物件的解構函式,並將該物件所佔的全域性堆記憶體空間返回給全域性堆,如②句。執行②句後,指標obj所指向的物件確實已被銷燬。但是指標obj卻還存在於棧中,直到程式退出其所在的作用域。即執行到③處時,指標obj才會消失。需要注意的是,指標obj的值在②處至③處之間,仍然指向剛才被銷燬的物件的位置,這時使用這個指標是危險的。在Win32平臺中,訪問剛才被銷燬物件,可能出現3種情況。第1種情況是該處位置所在的“記憶體頁”沒有任何物件,堆管理器已經將其進一步返回給系統,此時通過指標obj訪問該處記憶體會引起“訪問違例”,即訪問了不合法的記憶體,這種錯誤會導致程序崩潰;第2種情況是該處位置所在的“記憶體頁”還有其他物件,且該處位置被回收後,尚未被分配出去,這時通過指標obj訪問該處記憶體,取得的值是無意義的,雖然不會立刻引起程序崩潰,但是針對該指標的後續操作的行為是不可預測的;第3種情況是該處位置所在的“記憶體頁”還有其他物件,且該處位置被回收後,已被其他物件申請,這時通過指標obj訪問該處記憶體,取得的值其實是程式其他處生成的物件。雖然對指標obj的操作不會立刻引起程序崩潰,但是極有可能會引起該物件狀態的改變。從而使得在建立該物件處看來,該物件的狀態會莫名其妙地變化。第2種和第3種情況都是很難發現和排查的bug,需要小心地避免。

建構函式:

建立一個物件分成兩個步驟,即首先取得物件所需的記憶體(無論是從執行緒棧還是從全域性堆中),然後在該塊記憶體上執行建構函式。在建構函式構建該物件時,建構函式也分成兩個步驟。即第1步執行初始化(通過初始化列表),第2步執行建構函式的函式體,如下:

class  Derived  :  public Base

{

public :

        Derived() : i(10), string("unnamed")        

        {

            ...                                      

        }

        ...

private :

        int  i;

        string  name;

        ...

};

①步中的 “: i(10), string("unnamed")” 即所謂的“初始化列表”,以“:”開始,後面為初始化單元。每個單元都是“變數名(初始值)”這樣的模式,各單元之間以逗號隔開。建構函式首先根據初始化列表執行初始化,然後執行建構函式的函式體,即②處語句。對初始化操作,有下面幾點需要注意。

(1)建構函式其實是一個遞迴操作,在每層遞迴內部的操作遵循嚴格的次序。遞迴模式為首先執行父類的建構函式(父類的建構函式操作也相應的包括執行初始化和執行建構函式體兩個部分),父類建構函式返回後構造該類自己的成員變數。構造該類自己的成員變數時,一是嚴格按照成員變數在類中的宣告順序進行,而與其在初始化列表中出現的順序完全無關;二是當有些成員變數或父類物件沒有在初始化列表中出現時,它們仍然在初始化操作這一步驟中被初始化。內建型別成員變數被賦給一個初值。父類物件和類成員變數物件被呼叫其預設建構函式初始化,然後父類的建構函式和子成員變數物件在建構函式執行過程中也遵循上述遞迴操作。一直到此類的繼承體系中所有父類和父類所含的成員變數都被構造完成後,此類的初始化操作才告結束。

(2)父類物件和一些成員變數沒有出現在初始化列表中時,這些物件仍然被執行建構函式,這時執行的是“預設建構函式”。因此這些物件所屬的類必須提供可以呼叫的預設建構函式,為此要求這些類要麼自己“顯式”地提供預設建構函式,要麼不能阻止編譯器“隱式”地為其生成一個預設建構函式,定義除預設建構函式之外的其他型別的建構函式就會阻止編譯器生成預設建構函式。如果編譯器在編譯時,發現沒有可供呼叫的預設建構函式,並且編譯器也無法生成,則編譯無法通過。

(3)對兩類成員變數,需要強調指出即“常量”(const)型和“引用”(reference)型。因為已經指出,所有成員變數在執行函式體之前已經被構造,即已經擁有初始值。根據這個特點,很容易推斷出“常量”型和“引用”型變數必須在初始化列表中正確初始化,而不能將其初始化放在建構函式體內。因為這兩類變數一旦被賦值,其整個生命週期都不能修改其初始值。所以必須在第一次即“初始化”操作中被正確賦值。

(4)可以看到,即使初始化列表可能沒有完全列出其子成員或父類物件成員,或者順序與其在類中宣告的順序不符,這些成員仍然保證會被“全部”且“嚴格地按照順序”被構建。這意味著在程式進入建構函式體之前,類的父類物件和所有子成員變數物件都已經被生成和構造。如果在建構函式體內為其執行賦初值操作,顯然屬於浪費。如果在建構函式時已經知道如何為類的子成員變數初始化,那麼應該將這些初始化資訊通過建構函式的初始化列表賦予子成員變數,而不是在建構函式體中進行這些初始化。因為進入建構函式體時,這些子成員變數已經初始化一次。

9、繼承與虛擬函式

虛擬函式是C++語言引入的一個很重要的特性,它提供了動態繫結機制,正是這一機制使得繼承的語義變得相對明晰。

1)基類抽象了通用的資料及操作,就資料而言,如果該資料成員在各派生類中都需要用到,那麼就需要將其宣告在基類中;就操作而言,如果該操作對各派生類都有意義,無論其語義是否會被修改或擴充套件,那麼就需要將其宣告在基類中。

2)有些操作,如果對於各個派生類而言,語義保持完全一致,而無需修改或擴充套件,那麼這些操作宣告為基類的非虛擬成員函式。各派生類在宣告為基類的派生類時,預設繼承了這些非虛擬成員函式的宣告/實現,如同預設繼承基類的資料成員一樣,而不必另外做任何宣告,這就是繼承帶來的程式碼重用的優點。

3另外還有一些操作,雖然對於各派生類而言都有意義,但是其語義並不相同。這時,這些操作應該宣告為基類的虛擬成員函式。各派生類雖然也預設繼承了這些虛擬成員函式的宣告/實現,但是語義上它們應該對這些虛擬成員函式的實現進行修改或者擴充套件。另外在實現這些修改或擴充套件過程中,需要用到額外的該派生類獨有的資料時,將這些資料宣告為此派生類自己的資料成員。

再考慮更大背景下的繼承體系,當更高層次的程式框架(繼承體系的使用者)使用此繼承體系時,它處理的是一個抽象層次的物件集合(即基類)。雖然這個物件集合的成員實質上可能是各種派生類物件,但在處理這個物件集合中的物件時,它用的是抽象層次的操作。並不區分在這些操作中,哪些操作對各派生類來說是保持不變的,而哪些操作對各派生類來說有所不同。這是因為,當執行時實際執行到各操作時,執行時系統能夠識別哪些操作需要用到動態繫結,從而找到對應此派生類的修改或擴充套件的該操作版本。

也就是說,對繼承體系的使用者而言,此繼承體系內部的多樣性是透明的。它不必關心其繼承細節,處理的就是一組對它而言整體行為一致的物件。即只需關心它自己問題域的業務邏輯,只要保證正確,其任務就算完成了。即使繼承體系內部增加了某種派生類,或者刪除了某種派生類,或者某某派生類的某個虛擬函式的實現發生了改變,它的程式碼不必任何修改。這也意味著,程式的模組化程度得到了極大的提高。而模組化的提高也就意味著可擴充套件性、可維護性,以及程式碼的可讀性的提高,這也是面向物件程式設計的一個很大的優點。

虛擬函式的“動態繫結”特性雖然很好,但也有其內在的空間以及時間開銷,每個支援虛擬函式的類(基類或派生類)都會有一個包含其所有支援的虛擬函式指標的“虛擬函式表”(virtual table)。另外每個該類生成的物件都會隱含一個“虛擬函式指標”(virtual pointer),此指標指向其所屬類的“虛擬函式表”。當通過基類的指標或者引用呼叫某個虛擬函式時,系統需要首先定位這個指標或引用真正對應的“物件”所隱含的虛擬函式指標。“虛擬函式指標”,然後根據這個虛擬函式的名稱,對這個虛擬函式指標所指向的虛擬函式表進行一個偏移定位,再呼叫這個偏移定位處的函式指標對應的虛擬函式,這就是“動態繫結”的解析過程(當然C++規範只需要編譯器能夠保證動態繫結的語義即可,但是目前絕大多數的C++編譯器都是用這種方式實現虛擬函式的),通過分析,不難發現虛擬函式的開銷:

— 空間:每個支援虛擬函式的類,都有一個虛擬函式表,這個虛擬函式表的大小跟該類擁有的虛擬函式的多少成正比,此虛擬函式表對一個類來說,整個程式只有一個,而無論該類生成的物件在程式執行時會生成多少個。

— 空間:通過支援虛擬函式的類生成的每個物件都有一個指向該類對應的虛擬函式表的虛擬函式指標,無論該類的虛擬函式有多少個,都只有一個函式指標,但是因為與物件繫結,因此程式執行時因為虛擬函式指標引起空間開銷跟生成的物件個數成正比。

— 時間:通過支援虛擬函式的類生成的每個物件,當其生成時,在建構函式中會呼叫編譯器在建構函式內部插入的初始化程式碼,來初始化其虛擬函式指標,使其指向正確的虛擬函式表。

— 時間:當通過指標或者引用呼叫虛擬函式時,跟普通函式呼叫相比,會多一個根據虛擬函式指標找到虛擬函式表的操作。

行內函數:因為行內函數常常可以提高程式碼執行的速度,因此很多普通函式會根據情況進行內聯化,但是虛擬函式無法利用內聯化的優勢,這是因為行內函數是在“編譯期”編譯器將呼叫行內函數的地方用行內函數體的程式碼代替(內聯展開),但是虛擬函式本質上是“執行期”行為,本質上在“編譯期”編譯器無法知道某處的虛擬函式呼叫在真正執行的時候會呼叫到那個具體的實現(即在“編譯期”無法確定其繫結),因此在“編譯期”編譯器不會對通過指標或者引用呼叫的虛擬函式進行內聯化。也就是說,如果想利用虛擬函式的“動態繫結”帶來的設計優勢,那麼必須放棄“行內函數”帶來的速度優勢。

因為要實現相同的程式功能(語義),已經看到,每個物件雖然沒有編譯器生成的虛擬函式指標(解構函式往往被設計為virtual,如果如此,仍然免不了會隱含增加一個虛擬函式指標,這裡假設不是這樣),但是還是需要另外增加一個type變數用來標識派生類的型別。構造物件時,雖然不必初始化虛擬函式指標,但是仍然需要初始化type。另外,圖形繼承體系的使用者呼叫函式時雖然不再需要一次間接的根據虛擬函式表找尋虛擬函式指標的操作,但是再呼叫之前,仍然需要一個switch語句對其型別進行識別。

綜上所述,這裡列舉的5條虛擬函式帶來的缺陷只剩下兩條,即虛擬函式表的空間開銷及無法利用“行內函數”的速度優勢。再考慮虛擬函式表,每一個含有虛擬函式的類在整個程式中只會有一個虛擬函式表。可以想像到虛擬函式表引起的空間開銷實際上是非常小的,幾乎可以忽略不計。

這樣可以得出結論,即虛擬函式引入的效能缺陷只是無法利用行內函數。

可以進一步設想,非虛擬函式的常規設計假如需要增加一種新的圖形型別,或者刪除一種不再支援的圖形型別,都必須修改該圖形系統所有使用者的所有與型別相關的函式呼叫的程式碼。這裡使用者只有Canvas一個,與型別相關的函式呼叫程式碼也只有PaintRotateSelected兩處。但是在一個複雜的程式中,其使用者很多。並且型別相關的函式呼叫很多時,每次對圖形系統的修改都會波及到這些使用者。可以看出不使用虛擬函式的常規設計增加了程式碼的耦合度,模組化不強,因此帶來的可擴充套件性、可維護性,以及程式碼的可讀性方面都極大降低。面向物件程式設計的一個重要目的就是增加程式的可擴充套件性和可維護性,即當程式的業務邏輯發生變化時,對原有程式的修改非常方便。而不至於對原有程式碼大動干戈,從而降低因為業務邏輯的改變而增加出錯的可能性。根據這點分析,虛擬函式可以大大提升程式的可擴充套件性及可維護性。

因此在效能和其他方面特性的選擇方面,需要開發人員根據實際情況進行權衡和取捨。當然在權衡之前,需要通過效能檢測確認效能的瓶頸是由於虛擬函式沒有利用到行內函數的優勢這一缺陷引起;否則可以不必考慮虛擬函式的影響。

虛擬函式可以大大提高可擴充套件性和可維護性,但是相比行內函數效能不是很高,但有比沒有好很多

10、臨時物件

物件的建立與銷燬對程式的效能影響很大。尤其當該物件的類處於一個複雜繼承體系的末端,或者該物件包含很多成員變數物件(包括其所有父類物件,即直接或者間接父類的所有成員變數物件)時,對程式效能影響尤其顯著。因此作為一個對效能敏感的開發人員,應該儘量避免建立不必要的物件,以及隨後的銷燬。這裡“避免建立不必要的物件”,不僅僅意味著在程式設計時,主要減少顯式出現在原始碼中的物件建立。還有在編譯過程中,編譯器在某些特殊情況下生成的開發人員看不見的隱式的物件。這些物件的建立並不出現在原始碼級別,而是由編譯器在編譯過程中“悄悄”建立(往往為了某些特殊操作),並在適當時銷燬,這些就是所謂的“臨時物件”。需要注意的是,臨時物件與通常意義上的臨時變數是完全不同的兩個概念。

 臨時變量出現在原始碼中,臨時物件不出現在原始碼中但又在編譯時不得不出現。

a + b實際上是執行operator+(const Matrix& arg1, const Matrix& arg2),過載的操作符本質上是一個函式,這裡ab就是此函式的兩個變數。此函式返回一個Matrix變數,然後進一步將此變數通過Matrix::operator=(const Matrix& mt)c進行賦值。因為a + b返回時,其中的sum已經結束了其生命週期。即在operator+(const Matrix& arg1, const Matrix& arg2)結束時被銷燬,那麼其返回的Matrix物件需要在呼叫a + b函式(這裡是main()函式)的棧中開闢空間用來存放此返回值。這個臨時的Matrix物件是在a + b返回時通過Matrix拷貝建構函式構造,即⑤處的輸出。

既然如上所述,建立和銷燬物件經常會成為一個程式的效能瓶頸所在,那麼有必要對臨時物件產生的原因進行深入探究,並在不損害程式功能的前提下儘可能地規避它。

臨時物件在C++語言中的特徵是未出現在原始碼中,從堆疊中產生的未命名物件。這裡需要特別注意的是,臨時物件並不出現在原始碼中。即開發人員並沒有宣告要使用它們,沒有為其宣告變數。它們由編譯器根據情況產生,而且開發人員往往都不會意識到它們的產生。

產生臨時物件一般來說有如下兩種場合。

1)當實際呼叫函式時傳入的引數與函式定義中宣告的變數型別不匹配。

2)當函式返回一個物件時(這種情形下也有例外,下面會講到)。

另外,也有很多開發人員認為當函式傳入引數為物件,並且實際呼叫時因為函式體內的該物件實際上並不是傳入的物件,而是該傳入物件的一份拷貝,所以認為這時函式體內的那個拷貝的物件也應該是一個臨時物件。但是嚴格說來,這個拷貝物件並不符合“未出現在原始碼中”這一特徵。當然只要能知道並意識到物件引數的工作原理及背後隱含的效能特徵,並能在編寫程式碼時儘量規避之,那麼也就沒有必要在字面上較真了,畢竟最終目的是寫出正確和高效的程式。

可以看到C++編譯器為了成功編譯某些語句,往往會在私底下“悄悄”地生成很多從原始碼中不易察覺的輔助函式,甚至物件。比如上段程式碼中,編譯器生成的賦值操作符、型別轉換,以及型別轉換的中間結果,即一個臨時物件。

很多時候,這種編譯器提供的自動型別轉換確實提高了程式的可讀性,也在一定程度上簡化了程式的編寫,從而提高了開發速度。但是型別轉換意味著臨時物件的產生,物件的建立和銷燬意味著效能的下降,型別轉換還意味著編譯器還需要生成額外的程式碼等。因此在設計階段,預計到不需要編譯器提供這種自動型別轉換的便利時,可以明確阻止這種自動型別轉換的發生,即阻止因此而引起臨時物件的產生。這種明確阻止就是通過對類的建構函式增加“explicit”宣告。

這裡可以看到,與operator+不同,operator+=並沒有產生臨時變數,operator+則只有在返回值被用來初始化一個物件,而不是對一個已經生成的物件進行賦值時才不產生臨時物件。而且往往返回值被用來賦值的情況並不少見,甚至比初始化的情況還要多。因此使用operator+=不產生臨時物件,效能會比operator+要好,為此儘量使用語句:

a += b;

而避免使用:

a = a + b;

可以看到,因為考慮到後置++的語義,所以在實現中必須首先保留其原來的值。為此需要一個區域性變數,如①處所示。然後值增1後,將儲存其原值的區域性變數作為返回值返回。相比較而言,前置++的實現不會需要這樣一個區域性變數。而且不僅如此,前置的++只需要將自身返回即可,因此只需返回一個引用;後置++需要返回一個物件。已經知道,函式返回值為一個物件時,往往意味著需要生成一個臨時物件用來存放返回值。因此如果呼叫後置++,意味著需要多生成兩個物件,分別是函式內部的區域性變數和存放返回值的臨時變數。

有鑑於此,對於非內建型別,在保證程式語義正確的前提下應該多用:

++i;

而避免使用:

i++;

同樣的規律也適用於前置--和後置--(與=/+=相同的理由,考慮到維護性,儘量用前置++來實現後置++)。

11、行內函數

C++語言的設計中,行內函數的引入可以說完全是為了效能的考慮。因此在編寫對效能要求比較高的C++程式時,非常有必要仔細考量行內函數的使用。

所謂“內聯”,即將被呼叫函式的函式體程式碼直接地整個插入到該函式被呼叫處,而不是通過call語句進行。當然,編譯器在真正進行“內聯”時,因為考慮到被行內函數的傳入引數、自己的區域性變數,以及返回值的因素,不僅僅只是進行簡單的程式碼拷貝,還需要做很多細緻的工作,但大致思路如此。

開發人員可以有兩種方式告訴編譯器需要內聯哪些類成員函式,一種是在類的定義體外;一種是在類的定義體內。

(1)當在類的定義體外時,需要在該成員函式的定義前面加“inline”關鍵字,顯式地告訴編譯器該函式在呼叫時需要“內聯”處理。

(2)當在類的定義體內且宣告該成員函式時,同時提供該成員函式的實現體。此時,“inline”關鍵字並不是必需的。

(3)當普通函式(非類成員函式)需要被內聯時,則只需要在函式的定義時前面加上“inline”關鍵字,如:

   inline int DoSomeMagic(int a, int b)

{

        return a * 13 + b % 4 + 3;

}

因為C++是以“編譯單元”為單位編譯的,而一個編譯單元往往大致等於一個“.cpp”檔案。在實際編譯前,前處理器會將“#include”的各標頭檔案的內容(可能會有遞迴標頭檔案展開)完整地拷貝到cpp檔案對應位置處(另外還會進行巨集展開等操作)。前處理器處理後,編譯真正開始。一旦C++編譯器開始編譯,它不會意識到其他cpp檔案的存在。因此並不會參考其他cpp檔案的內容資訊。聯想到內聯的工作是由編譯器完成的,且內聯的意思是將被呼叫行內函數的函式體程式碼直接代替對該行內函數的呼叫。這也就意味著,在編譯某個編譯單元時,如果該編譯單元會呼叫到某個行內函數,那麼該行內函數的函式定義(即函式體)必須也包含在該編譯單元內。因為編譯器使用行內函數體程式碼替代行內函數呼叫時,必須知道該行內函數的函式體程式碼,而且不能通過參考其他編譯單元資訊來獲得這一資訊。

如果有多個編譯單元會呼叫到某同一個行內函數,C++規範要求在這多個編譯單元中該行內函數的定義必須是完全一致的,這就是“ODR”(one-definition rule)原則。考慮到程式碼的可維護性,最好將行內函數的定義放在一個頭檔案中,用到該行內函數的各個編譯單元只需#include該標頭檔案即可。進一步考慮,如果該行內函數是一個類的成員函式,這個標頭檔案正好可以是該成員函式所屬類的宣告所在的標頭檔案。這樣看來,類成員行內函數的兩種宣告可以看成是幾乎一樣的,雖然一個是在類外,一個在類內。但是兩個都在同一個標頭檔案中,編譯器都能在#include該標頭檔案後直接取得行內函數的函式體程式碼。討論完如何宣告一個行內函數,來檢視編譯器如何內聯的。

可以看到使用行內函數至少有如下兩個優點。

1)減少因為函式呼叫引起開銷,主要是引數壓棧、棧幀開闢與回收,以及暫存器儲存與恢復等。

2)內聯後編譯器在處理呼叫行內函數的函式(如上例中的foo()函式)時,因為可供分析的程式碼更多,因此它能做的優化更深入徹底。前一條優點對於開發人員來說往往更顯而易見一些,但往往這條優點對最終程式碼的優化可能貢獻更大。

在前面章節中已經討論,如果傳入引數和返回值為物件時,還會涉及物件的構造與析構,函式呼叫的開銷就會更大。尤其是當傳入物件和返回物件是複雜的大物件時,更是如此。

因為函式呼叫的準備與善後工作最終都是由機器指令完成的,假設一個函式之前的準備工作與之後的善後工作的指令所需的空間為SS,執行這些程式碼所需的時間為TS,現在可以更細緻地從空間與時間兩個方面來分析內聯的效果。

1)在空間上,一般印象是不採用內聯,被呼叫函式的程式碼只有一份,呼叫它的地方使用call語句引用即可。而採用內聯後,該函式的程式碼在所有呼叫其處都有一份拷貝,因此最後總的程式碼大小比採用內聯前要大。但事實不總是這樣的,如果一個函式a的體程式碼大小為AS,假設a函式在整個程式中被呼叫了n次,不採用內聯時,對a的呼叫只有準備工作與善後工作兩處會增加最後的程式碼量開銷,即a函式相關的程式碼大小為:n * SS + AS。採用內聯後,在各處呼叫點都需要將其函式體程式碼展開,即a函式相關的程式碼大小為n * AS。這樣比較二者的大小,即比較(n * SS + AS)(n*AS)的大小。考慮到n一般次數很多時,可以簡化成比較SSAS的大小。這樣可以得出大致結論,如果被行內函數自己的函式體程式碼量比因為函式呼叫的準備與善後工作引入的程式碼量大,內聯後程序的程式碼量會變大;相反,當被行內函數的函式體程式碼量比因為函式呼叫的準備與善後工作引入的程式碼量小,內聯後程序的程式碼量會變小。這裡還沒有考慮內聯的後續情況,即編譯器可能因為獲得的資訊更多,從而對呼叫函式的優化做得更深入和徹底,致使最終的程式碼量變得更小。

2)在時間上,一般而言,每處呼叫都不再需要做函式呼叫的準備與善後工作。另外內聯後,編譯器在做優化時,看到的是呼叫函式與被呼叫函式連成的一大塊程式碼。即獲得的程式碼資訊更多,此時它對呼叫函式的優化可以做得更好。最後還有一個很重要的因素,即內聯後呼叫函式體內需要執行的程式碼是相鄰的,其執行的程式碼都在同一個頁面或連續的頁面中。如果沒有內聯,執行到被呼叫函式時,需要跳到包含被呼叫函式的記憶體頁面中執行,而被呼叫函式所屬的頁面極有可能當時不在實體記憶體中。這意味著,內聯後可以降低“缺頁”的機率,知道減少“缺頁”次數的效果遠比減少一些程式碼量執行的效果。另外即使被呼叫函式所在頁面可能也在記憶體中,但是因為與呼叫函式在空間上相隔甚遠,所以可能會引起“cache miss”,從而降低執行速度。因此總的來說,內聯後程序的執行時間會比沒有內聯要少。即程式的速度更快,這也是因為內聯後代碼的空間“locality”特性提高了。但正如上面分析空間影響時提到的,當AS遠大於SS,且n非常大時,最終程式的大小會比沒有內聯時要大很多。程式碼量大意味著用來存放程式碼的記憶體頁也會更多,這樣因為執行程式碼而引起的“缺頁”也會相應增多。如果這樣,最終程式的執行時間可能會因為大量的“缺頁”而變得更多,即程式的速度變慢。這也是為什麼很多編譯器對於函式體程式碼很多的函式,會拒絕對其進行內聯的請求。即忽略“inline”關鍵字,而對如同普通函式那樣編譯。

綜合上面的分析,在採用內聯時需要行內函數的特徵。比如該函式自己的函式體程式碼量,以及程式執行時可能被呼叫的次數等。當然,判斷內聯效果的最終和最有效的方法還是對程式的大小和執行時間進行實際測量,然後根據測量結果來決定是否應該採用內聯,以及對哪些函式進行內聯。

如下根據內聯的本質來討論與其相關的一些其他特點。

如前所述,因為呼叫行內函數的編譯單元必須有行內函數的函式體程式碼資訊。又因為ODR規則和考慮到程式碼的可維護性,所以一般將行內函數的定義放在一個頭檔案中,然後在每個呼叫該行內函數的編譯單元中#include該標頭檔案。現在考慮這種情況,即在一個大型程式中,某個行內函數因為非常通用,而被大多數編譯單元用到對該行內函數的一個修改,就會引起所有用到它的編譯單元的重新編譯。對於一個真正的大型程式,重新編譯大部分編譯單元往往意味著大量的編譯時間。因此內聯最好在開發的後期引入,以避免可能不必要的大量編譯時間的浪費。

再考慮這種情況,如果某開發小組在開發中用到了第三方提供的程式庫,而這些程式庫中包含一些行內函數。因為該開發小組的程式碼中在用到第三方提供的行內函數處,都是將該行內函數的函式體程式碼拷貝到呼叫處,即該開發小組的程式碼中包含了第三方提供程式碼的“實現”。假設這個第三方單位在下一個版本中修改了某些行內函數的定義,那麼雖然這個第三方單位並沒有修改任何函式的對外介面,而只是修改了實現,該開發小組要想利用這個新的版本,仍然需要重新編譯。考慮到可能該開發小組的程式已經發布,那麼這種重新編譯的成本會相當高;相反,如果沒有內聯,並且仍然只是修改實現,那麼該開發小組不必重新編譯即可利用新的版本。

因為內聯的本質就是用函式體程式碼代替對該函式的呼叫,所以考慮遞迴函式,如:

[inline] int foo(int n)

{

        ...

        return foo(n-1);

}

如果編譯器編譯某個呼叫此函式的編譯單元,如:

void func()

{

        ...

        int m = foo(n);

        ...

}

考慮如下兩種情況。

1)如果在編譯該編譯單元且呼叫foo時,提供的引數n不能知道其實際值,則編譯器無法知道對foo函式體進行多少次代替。在這種情況下,編譯器會拒絕對foo函式進行內聯。

2)如果在編譯該編譯單元且呼叫foo時,提供的引數n能夠知道其實際值,則編譯器可能會視n值的大小來決定是否對foo函式進行內聯。因為如果n很大,內聯展開可能會使最終程式的大小變得很大。

如前所述,因為行內函數是編譯期行為,而虛擬函式是執行期行為,因此編譯器一般會拒絕對虛擬函式進行內聯的請求。但是事情總有例外,行內函數的本質是編譯器編譯呼叫某函式時,將其函式體程式碼代替call呼叫,即內聯的條件是編譯器能夠知道該處函式呼叫的函式體。而虛擬函式不能夠被內聯,也是因為在編譯時一般來說編譯器無法知道該虛擬函式到底是哪一個版本,即無法確定其函式體。但是在兩種情況下,編譯器是能夠知道虛擬函式呼叫的真實版本的,因此虛擬函式可以被內聯。

其一是通過物件,而不是指向物件的指標或者物件的引用呼叫虛擬函式,這時編譯器在編譯期就已經知道物件的確切型別。因此會直接呼叫確定的某虛擬函式實現版本,而不會產生“動態繫結”行為的程式碼。

其二是雖然是通過物件指標或者物件引用呼叫虛擬函式,但是編譯時編譯器能知道該指標或引用對應到的物件的確切型別。比如在產生的新物件時做的指標賦值或引用初始化,發生在於通過該指標或引用呼叫虛擬函式同一個編譯單元並且二者之間該指標沒有被改變賦值使其指向到其他不能確切知道型別的物件(因為引用不能修改繫結,因此無此之虞)。此時編譯器也不會產生動態繫結的程式碼,而是直接呼叫該確定型別的虛擬函式實現版本。

當然在實際開發中,通過這兩種方式呼叫虛擬函式時應該非常少,因為虛擬函式的語義是“通過基類指標或引用呼叫,到真正執行時才決定呼叫哪個版本”。

從上面的分析中已經看到,編譯器並不總是尊重“inline”關鍵字。即使某個函式用“inline”關鍵字修飾,並不能夠保證該函式在編譯時真正被內聯處理。因此與register關鍵字性質類似,inline僅僅是給編譯器的一個“建議”,編譯器完全可以視實際情況而忽略之。

另外從內聯,即用函式體程式碼替代對該函式的呼叫這一本質看,它與C語言中的函式巨集(macro)極其相似,但是它們之間也有本質的區別。即內聯是編譯期行為,巨集是預處理期行為,其替代展開由前處理器來做。也就是說編譯器看不到巨集,更不可能處理巨集。另外巨集的引數在其巨集體內出現兩次或兩次以上時經常會產生副作用,尤其是當在巨集體內對引數進行++

相關推薦

c++重要知識點總結

1、記憶體分配的方式: 分配方式有三種: 1.靜態儲存區,是在程式編譯時就已經分配好記憶體,在整個執行期間都存在,如全域性變數、常量。 2.棧上分配,函式內的區域性變數就是從棧上分配的,但分配的記憶體容量有限。 3.堆上分配,也稱動態分配,如我們用new,malloc分配記

JS重要知識點總結-不完善

子函數 必須 his 代碼規範 重要 line java 全局 lba ###1、閉包 ??閉包就是能夠讀取其他函數內部變量的函數。由於在Javascript語言中,只有函數內部的子函數才能讀取局部變量,因此可以把閉包簡單理解成"定義在一個函數內部的函數"。所以,在本質上,

ES678重要知識點總結

ES6:也就使es2015,這一版更新了非常重要的知識點,也是目前前端面試內容佔比最多的一部分 1、let,const.     1.11塊級作用域:見到這種變數首先想到的就是es6新添了一種作用域,塊級作用域。而生效過程即使在有let和const存在就會有會計作用域,顧

3.C++語言知識點總結

1.封裝:將記錄包起來讓其自己去幹。 2.抽象:記錄一下這些傢伙的屬性,比如都是人。 3.繼承:生下來的兒子跟父親一個鳥樣。 4.多型:兒子除了含有父親的內容外,還有自己的特徵。 5.標頭檔案中不加“.h”。 6.加上名稱空間 using namespace std

計算機系統結構之重要知識點總結2

交換 受限 分析 存儲器結構 處理器 完全 如果 判斷 狀態 一.名詞解釋 1)虛擬機:指通過軟件模擬具有完整硬件系統功能的,運行在一個完全隔離環境中的完整計算機系統 2)系統加速比:同一個任務在系統改進前花費總時間和在系統改進後花費總時間的比率 3)Amdahl定律

C/C++面試知識點總結(一)

目錄: 一、基礎知識     1.C/C++     2.STL     6.資料庫 一、基礎知識 1.C/C++ (1).struct大小的確定 由於記憶體對齊的原則,在32位機器上,記憶體是4位元組對齊,也就是說,不

Java重要知識點總結

*編碼規範:常量名通常使用大寫字母,中間使用下劃線連線 *定義的final變數屬於全域性變數時,必須在定義時就設定它的初值,否則將會產生編譯錯誤。區域性變數可以不在定義的時候賦初值(但是在使用前必須賦初值)。 *普通的成員變數(全域性變數),可以為它設定初始值,也可以不設定

struts2重要知識點總結(2)

Action的配置 的配置 通過上面的示例可以看出,Action需要在struts.xml中配置才可以使用,而且Action應該配置成為元素的子元素,那麼元素的功能是什麼呢? 元素可以把邏輯上相關的一組Action、Result、Intercepter等元

C#】知識點總結(一)

一、概念:.net與c# . net/dotnet:一般指 .Net  Framework框架,一種平臺,一種技術。 C#(sharp):一種程式語言,可以開發基於.net平臺的應用。 Java是一種技術、一種程式語言。   二、.net能幹什麼(必須掌握)

前端重要知識點總結

目錄 一、引入js檔案和css檔案: 二、設定UTF-8編碼 三、常見的css屬性及其功能                四、常見CSS功能和JS功能 五、iframe標籤:實現頁面區域性重新整理

計算機組成原理重要知識點總結

1. 馮·諾依曼體系結構 2. 浮點數的表示 浮點數的表示是程式設計裡比較重要的概念,這對於金融計算來講,格式重要。1/3=多少?為什麼推薦使用BigDecimal? Float和Double適合做金融運算麼? 這些都會在浮點數裡找到

C++ 面試知識點總結

1. C++基礎知識點 1.1 有符號型別和無符號型別 當我們賦給無符號型別一個超出它表示範圍的值時,結果是初始值對無符號型別表示數值總數取模之後的餘數。當我們賦給帶符號型別一個超出它表示範圍的值時,結果是未定義的;此時,程式可能繼續工作、可能崩潰。也可能生成垃圾資料。

C++基礎知識點總結

1.過載函式是否能夠通過函式返回值的型別不同來區分? 不可以。因為在C++程式設計中,函式的返回值可以忽略(不使用其返回值),程式中呼叫此時函式名相同和引數相同的兩個函式對編譯器和程式設計師來說是沒有辦法區分的,編譯器會提示出錯。 2.C++多型機制的實現

struts2重要知識點總結(1):

struts2最近又看了一邊,每看一次都有一次的收穫,這裡總結struts2中的一些重要的知識點: action介紹: 1:action類代表著一次請求或者呼叫,每個請求的動作都對應一個相應的action類,action是一個獨立的工

C語言知識點總結

在這裡對C語言利用思維導圖的方式進行總結一下,寫的不算太深,主要是能讓大家知道複習時應該搞清楚的問題。思維導圖後續仍然會補充,如果需要思維導圖檔案,請發郵件到[email protected] 對於程式設計的學習,咱們應該多總結,多積累,反覆閱讀

C++繼承知識點總結;

C++繼承方式分為公有繼承、私有繼承、保護繼承。關鍵字分別為public 、 private 、 protected。 1.派生類幾種繼承方式總結: 2.基類物件對基類的訪問許可權總結: 3.派生類物件對基類的訪問許可權總結

C++重要知識點小結---1

1.C++中類與結構的唯一區別是:類(class)定義中預設情況下的成員是private的,而結構(struct)定義中預設情況下的成員是public的。 2. ::叫作用域區分符,指明一個函式屬於哪個類或一個數據屬於哪個類。::可以不跟類名,表示全域性資料或全域性函

C++知識點總結(更新中)

如果 知識 修飾 區別 知識點總結 str 運算 必須 初始 1. 指針和引用的區別 本質:指針是地址,引用是別名。 對象綁定:指針可以為空,如果前面不加const修飾,可在運行過程中改變其指向的對象;引用不能為空,必須初始化,一旦與對象綁定則不可改變。 對象訪問:指針是間

C#泛型基礎知識點總結

www. compile win 泛型 override amp 。。 target 類繼承   1.0 什麽是泛型 泛型是C#2.0和CLR(公共語言運行時)升級的一個新特性,泛型為.NET 框架引入了一個叫 type parameters(類型參數)的概念

C# winform 程序開發知識點總結(幹貨)

onstop 剛才 cell iss 成功 one 身份驗證 服務 cep 1、數據庫連接及操作   在說數據庫操作之前,先說一下數據庫連接操作字符串的獲取   首先,點擊服務器資源管理器,接下來選中數據連接右鍵點擊添加連接,填入你要連接的服務器名稱,點擊單選框使用SQL