C++の函數——內聯函數&函數指針
- 2020 年 1 月 14 日
- 筆記
C++の函數
—— 內聯函數&函數指針
今天我們繼續討論C++函數部分,剩下兩個點,一個是內聯函數,另一個是函數指針。
內聯函數
我們先看一下內聯函數。內聯函數也是C++中的一個重要特性。所謂內聯函數,其實本質上也是一種函數,在形式上的表現就是在普通函數前面加上關鍵字"inline",然後相對於普通函數來說,它也比較短小。C++中"inline"的作用其實是為了優化程式碼的運行,降低程式碼的執行時間,就像在C語言中的宏函數一樣,作用也是為了降低程式碼的執行時間。
當內聯函數被調用時,並不會向普通函數一樣從主函數跳轉到函數,而是直接將內聯函數中的程式碼邏輯替換進主函數,提高運行效率。而這個過程是在程式碼編譯的過程即完成的,當我們將一個函數定義為內聯函數時,編譯器識別到內聯函數的特徵後,就將函數的定義替換到函數的調用。那麼我們怎麼定義內聯函數呢?
如何定義內聯函數
定義內聯函數就要在函數的前面使用「inline」關鍵字。像下面這樣:
inline int add(int a, int b); //(1) inline int add(int a, int b) //(2) { return (a + b); } void test() //test func { int i = 4; int j = 6; add(i, j); } void test_f() //equal to test() { int i = 4; int j = 6; (i + j); }
我們看到上面有兩個部分,一個是add函數的聲明,一個是add函數的定義,並且每個函數前都有「inline」,我們便將「add」函數定義為內聯函數,那麼在程式碼中調用時就是將add函數的定義替換為調用部分的程式碼,如上面的test(),在編譯的時候就會自動轉為test_f()。
注意:有一點需要注意,並不是每一個用inline標明的函數都會被編譯器轉為內聯,內聯的根本目的是優化程式的運行,因此對於使用較為頻繁的短小的函數,才有明顯的效果,如果函數較為龐大,編譯器也會忽略掉函數前面的「inline」將其變為普通函數。
為什麼要用內聯函數
我們在程式碼中經常會用到一些小函數,它們邏輯簡單,程式碼量少,但是如果考慮到這些函數被調用者調用的時候,我們會發現大部分的時間都耗費在調用這個過程,也就是程式從主函數跳轉到被調用函數的過程,而不是在我們寫的這個小函數中。實際上正常的函數調用指令時,程式立即在函數調用語句之後存儲指令的記憶體地址,將被調用的函數載入到記憶體中複製參數值,跳轉到被調用函數的記憶體位置,執行函數程式碼,存儲函數的返回值,然後跳轉回執行被調用函數之前保存的指令地址。這樣會導致程式的運行時間開銷太大。
而C++的內聯函數則提供了一種替代的方法,使用inline關鍵字,編譯器用函數程式碼本身替換函數調用語句,然後再編譯整個程式碼。因此,對於內聯函數,編譯器不必跳到另一個位置去執行函數,然後再跳回去,因為被調用程式的程式碼已經是調用程式中程式碼的一部分了。
下面我們列舉一下內聯函數的優缺點:
優點:
1、內聯函數通過避免函數調用開銷從而加速了我們的程式
2、當函數調用發生時,內聯函數節省了堆棧上變數push/pop的開銷
3、內聯函數節省了從函數返回調用開銷
4、內聯函數通過使用指令快取來增加引用的局部性
5、通過將其標記為內聯,您可以將函數定義放入頭文件中
缺點:
1、由於程式碼擴展,它增加了可執行文件的大小
2、c++內聯在編譯時解決。這意味著如果您更改內聯函數的程式碼,您將需要使用它重新編譯所有程式碼,以確保它將被更新
3、當在頭文件中使用時,它會使頭文件變大,包含用戶不關心的資訊
4、如上所述,它增加了可執行文件的大小,這可能會導致記憶體抖動。更多的頁面錯誤會降低程式性能
5、有時並不有用,例如在嵌入式系統中,由於記憶體限制,大的可執行文件大小根本不是首選
什麼時候使用內聯函數
函數可以根據程式設計師的需要進行內嵌,那麼我們什麼時候使用呢?
1、當性能優先時,應該使用內聯函數
2、在宏上使用內嵌函數
3、優先在函數定義中使用類外的inline關鍵字來隱藏實現細節
函數指針
所謂函數指針,其實本質上還是指針,但是不同於我們之前提到的指針,函數指針是指向函數的指針。根據前面的文章,我們很容易聲明一個函數,如下我們聲明一個比較兩個字元串長度的函數:
bool lengthCompare(const string &, const string &);
現在,我們再來看一下函數指針的聲明方式吧:
bool (*pf)(const string &, const string &);
從上面我們可以看到pf前面有個*,因此pf是一個指針,右面是形參列表,所以pf是指向函數的指針,從前面的bool可以看出這個函數的返回值類型是bool類型。
注意: *pf兩邊的()是必須的,因為這代表*pf是一個整體,pf是一個指針,如果不加括弧,就表示bool* 是一個整體,pf就成了函數名,那麼它的含義就變成了返回值為bool類型指針的函數了,這樣是不是很好理解?
如何使用函數指針
其實同數組一樣,函數名就代表了函數入口的首地址,也就是我們說的函數指針。對於上面兩個例子來說,由於他們具有相同的參數列表,因此我們可以得到下面的等價式:
pf = lengthCompare; pf = &lengthCompare;
可以看到,函數名就是函數的首地址,也表示函數本身。
因此,我們也會有下面的調用方式:
bool b1 = pf("leoay", "learn C++"); bool b2 = (*pf)("leoay", "learn C++!"); bool b3 = lengthCompare("leoay", "learn C++!!");
可以看到,我們並不需要對函數指針進行解引用就能直接調用它,因為我們在調用函數的時候其實就是找函數在程式中的首地址,然後將參數傳進去。
重載函數的指針
前面我們說到了函數的重載,就是說在同一個源文件中函數具有相同的名字,但是具有不同的參數列表時的情況,因此我們很容易延伸到函數指針裡面,就是這裡要說的重載函數指針。我們先來看一下怎麼聲明重載函數指針:
void ff(int*); void ff(unsigned int); void (*pf1)(unsigned int) = ff;
從上面的程式碼,我們可以看出想要使用重載函數指針,我們就要先聲明重載函數,然後我們在定義一個函數指針時,將重載函數的地址賦值給這個函數指針,這裡有一點我們需要注意,既然重載函數有不同的列表,那麼我們在定義重載函數指針時該怎麼選擇呢?當然是與我們想要使用的那個重載函數保持一致。就是說我們想用哪個重載函數定義函數指針,函數指針的參數列表就應該與哪個重載函數保持一致。
把函數指針當做參數
到這裡,我們發現函數指針並沒有什麼神奇的地方,我們完全可以把它當做一個指針看待,只不過具備函數的一些特徵。但是,回歸根本,它還是一枚可愛的指針。因此,它應該具備指針的一些特徵。比如,我們可以把它當做參數傳遞給其他的參數。以後我們會講到,C++中常見的回調函數就是這樣使用的。
下面我們就用程式碼演示一下這種騷操作吧:
void useBigger(const string &s1, const string &s2, bool pf(const string &, const string &)); void useBigger(const string &s1, const string &s2, bool (*pf)(const string &, const string &));
可以看到上面的程式碼中兩個函數的參數中分別有下面這兩個參數:
bool pf(const string &, const string &) bool (*pf)(const string &, const string &)
上面是一個函數,下面是一個函數指針。但是在這裡實際上他們是等價的,當函數被作為參數傳遞給另一個參數的時候,是等價於函數指針的。所以上面兩個聲明其實是等價的。
對於函數指針與內聯函數的說明這篇文章就到這裡,由於是基礎系列文章,先不詳細展開,還有一些知識需要用實例和練習加以說明。以後如果出高級系列再詳細展開討論。