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 &)  

上面是一個函數,下面是一個函數指針。但是在這裡實際上他們是等價的,當函數被作為參數傳遞給另一個參數的時候,是等價於函數指針的。所以上面兩個聲明其實是等價的。

對於函數指針與內聯函數的說明這篇文章就到這裡,由於是基礎系列文章,先不詳細展開,還有一些知識需要用實例和練習加以說明。以後如果出高級系列再詳細展開討論。