內聯函數與宏定義函數的異同

面試時被問到了這個問題,當時突然懵了,所以來整理一下。如有疏漏,還望指摘。
(結論在最後,懶得看的同學可以直接翻到總結)

內聯函數與宏定義的函數很相似,都是在運行前,先將一些簡短的函數,直接替換爲實現代碼,以此來省去函數調用時,參數入棧、程序跳轉等的時間開支。C++引入了內聯機制作爲宏函數的改進與補充,但並不是替代,說明他們有很多相似的地方,但也有很多不同的性質。

宏定義的函數:

使用宏定義#define定義的函數。其在預編譯階段就被進行簡單的替換,因爲只是簡單的替換,所以無需定義參數和返回值的類型,也不會對參數進行臨時拷貝以及析構。如下宏定義函數:

    #define ADD(a, b) a+b
    void main(){
        int x=1, y=2, z;
        z = ADD(x,y);
    }

這段代碼就會被替換爲如下代碼:

    void main(){
        int x=1, y=2, z;
        z = x+y;
    }

宏定義函數使用時值得注意以下三點:

  • 首先,宏定義函數不會進行類型檢查,它不會檢查傳入的參數類型是否正確,傳出的參數類型是否正確,因爲在定義時,就不需要規定這些。

  • 其次,因爲只是進行簡單的替換,所以如果不注意的話,執行順序可能會和預想的不一樣。比如還是上面的宏定義函數,如果我想計算

    z=3 * ADD(x,y);
    

    就會被替換爲

    z = 3 * x + y;
    

    這樣在執行時,其實會先計算3*x,在計算+y,而與我們預想的邏輯不同。

  • 最後,因爲不會製作傳入參數的臨時拷貝,而只是進行簡單的替換,所以如果傳入的參數是一個表達式(如x++),或者是一個函數(func())的話,這個表達式或者函數可能會被多次執行。如下:

    #define MUL2(a) a+a
    void main(){
        int x=1, y;
        y = MUL2(x++);
    }
    

    這段代碼在執行時就會被替換爲:

    void main(){
        int x=1, y;
        y = x++ + x++;
    }
    

    其中x++就會被多次執行。

內聯函數:

使用關鍵詞inline修飾的函數爲內聯函數。內聯函數是指在編譯階段,編譯器將內聯函數展開,替換爲等效的實現代碼,而不是進行函數調用。內聯函數在使用上,在程序員看來,和一般函數無二,需要定義入參類型,返回值類型等,執行完畢會對函數內部的臨時變量進行析構等。如下內聯函數:

    inline int addint(int a, int b){
        return a+b;
    }
    void main(){
        int x = 1, y = 2, z;
        z = addint(x ,y);
    }

這段代碼,在編譯時就會被替換下面這樣(這裏只是做一個簡單的示意,實際替換應該會更復雜):

    void main(){
        int x = 1, y = 2, z;
        {
            int _a = x;	//製作傳入參數的臨時拷貝
            int _b = y;
            int temp;	//製作傳出參數的臨時拷貝
            temp = _a + _b;	//函數體
            z = temp;	//傳出返回值
        }//代碼塊結束,析構一些臨時變量
    }

編譯器將函數替換爲實現代碼,並加入一些處理,使得其執行起來在程序員看來,與一般函數無二。
內聯函數在使用時需要注意幾點:

  • 首先,類內定義的函數,會被默認爲內聯函數。
  • 其次,即使定義了內聯函數,編譯器也會進行檢查,如果函數太複雜,也不會進行替換(也可以使用某些關鍵字進行強制替換,但是性能可能會得不償失)。
  • 再有,內聯函數關鍵字和函數聲明寫在一起是無效的,必須與函數的定義寫在一起才能生效。
  • 最後,如果內聯函數需要在其他源文件中進行調用,那麼內聯函數的定義必須寫在頭文件中,寫在源文件中會造成編譯出錯。
  • 最後的最後,在多個源文件中可以定義相同的內聯函數,但是在一個源文件中,只能有一個定義相同的內聯函數。

總結

從上面的介紹,我們可以看出以下幾個相同點和不同點。

相同點:

  • 目的都是代碼的替換(雖然內聯函數不一定會被替換),以此省去函數調用的消耗。
  • 代碼的邏輯都應該是簡短的。
  • 如果被多個文件調用,那麼宏函數和內聯函數都應寫在頭文件中。

不同點:

  • 內聯函數的替換是在編譯階段,宏定義的替換是在預編譯階段。
  • 內聯函數會進行類型檢查,宏定義函數不會進行類型檢查。
  • 內聯函數的參數中,表達式和函數只會被執行一次,宏定義函數中可能會被執行多次。
  • 內聯函數的執行順序和正常函數一樣,宏定義函數替換後,表達式的執行順序可能會有不同。
  • 內聯函數不一定會被替換,如果代碼過於複雜,編譯器將不進行替換。而宏定義一定會被替換。

補充

我在後續學習中又發現他們還有一些別的不同點:

  • 內聯函數一定是完整的邏輯,上文中的變量需要進行參數傳遞,函數體內括號也都需要一一對應,等等;而宏定義因爲只是替換,因此邏輯可以不完整(比如說使用了函數體外的變量,比如說只有左括號卻沒有對應的右括號),只需要替換之後整個程序的邏輯是完整的即可。
  • 宏函數的參數可以不一定是完整變量名,甚至不一定是變量,可以是類型名、函數名、或者不完整的變量名、類型名、函數名等。比如:#define MACRO(type, name) type var_##name,這個時候你就可以調用MACRO(int, a)來構建一個名爲var_aint型變量(關於##的用法請看我的另一篇博客)。(感覺這一點應該算是上一點的補充)
  • 可以使函數指針指向內聯函數(但是此時不再內聯,即不再替換,而是和正常函數一樣進行跳轉),但是不能指向宏函數。
  • 內聯函數可以重載、繼承,宏函數不可以。
  • 宏函數中使用的函數可以不必提前聲明,只需在真正使用這個宏函數的代碼前聲明過即可。比如如下代碼是正確的:
    #define TEST_MACRO0() TEST_MACRO1(1)
    #define TEST_MACRO1(i) TEST_MACRO2(i,2)
    #define TEST_MACRO2(i1, i2) TEST_MACRO3(i1, i2, 3)
    #define TEST_MACRO3(i1, i2, i3) {cout << i1 << i2 << i3 << endl;}
    
    int main(void) { TEST_MACRO0(); }
    
    可以看到,在定義TEST_MACRO0之前,是沒有定義TEST_MACRO1的,但是只要在main函數真正調用TEST_MACRO0之前定義好TEST_MACRO1,那麼代碼就是正確的。

感覺說了這麼多不同點,其實都是宏函數與內聯函數兩種不同基本性質的不同體現:

  • 宏函數只是預編譯階段純粹的代碼層面的簡單替換。因爲只是簡答替換,所以可能會被執行多次,可能順序會不同,參數可以不必是變量,邏輯不必完整。因爲在預編譯階段的替換,所以不必提前聲明。
  • 宏函數一定會被替換,而內聯可以被替換也可以不替換。因爲有可能不被替換,所以內聯函數必須可以被當做正常函數使用,因此必須具備正常函數所有的基本特點,所以必須進行錯誤檢測,參數必須正常合法,邏輯必須完備,參數中的函數和表達式必須只會被執行一次,順序必須和正常函數相同,必須提前聲明,可以使用函數指針,可以重載繼承。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章