宏定義函數-普通函數-內聯函數區別

宏定義函數VS普通函數VS內聯函數

宏定義函數VS普通函數

  1. 宏定義函數
  • 要點:變量都用括號括起來,防止出錯,結尾不需要;。在實際編程中,不推薦把複雜的函數使用宏,不容易調試。多行用\

  • 例子:
    單行:
    #define MAX(a, b) ((a) > (b) ? (a):(b))
    多行:

    #define MALLOC(n, type) \
    ((type *) malloc((n)* sizeof(type))
    

    對於第一個函數,如果用普通函數,該怎樣寫?

      int max(int a, int b)
      { return (a > b ? a : b); }
    

    很顯然,我們不會選擇用函數來完成這個任務,原因有兩個:

    1. 首先,函數調用會帶來額外的開銷,它需要開闢一片棧空間,記錄返回地址,將形參壓棧,從函數返回還要釋放堆棧。這種開銷會降低代碼效率,而使用宏定義則在代碼速度方面比函數更勝一籌;
    2. 其次,函數的參數必須被聲明爲一種特定的類型,所以它只能在類型合適的表達式上使用,我們如果要比較兩個浮點型的大小,就不得不再寫一個專門針對浮點型的比較函數。反之,上面的那個宏定義可以用於整形、長整形、單浮點型、雙浮點型以及其他任何可以用“>”操作符比較值大小的類型,也就是說,宏是與類型無關的
    3. 和使用函數相比,使用宏的不利之處在於每次使用宏時,一份宏定義代碼的拷貝都會插入到程序中。除非宏非常短,否則使用宏會大幅度增加程序的長度。
    4. 還有一些任務根本無法用函數實現,但是用宏定義卻很好實現。比如參數類型沒法作爲參數傳遞給函數,但是可以把參數類型傳遞給帶參的宏。比如上面的malloc的例子.
  • 總結:

屬性 #define宏 函數
代碼長度 每次使用時,宏代碼都被插入到程序中。除了非常小的宏之外,程序的長度將大幅度增長。 函數代碼只出現於一個地方:每次使用這個函數時,都調用那個地方的同一份代碼
執行速度 更快 存在函數調用、返回的額外開銷
操作符優先級 宏參數的求值是在所有周圍表達式的上下文環境裏,除非它們加上括號,否則鄰近操作符的優先級可能產生不可預料的結果。 函數參數只在函數調用時求值一次,它的結果值傳遞給函數。表達式的求值結果更容易預測。
參數求值 參數用於宏定義時,每次都將重新求值,由於多次求值,具有副作用的參數可能會產生不可預測的結果。 參數在函數調用前只求值一次,在函數中多次使用參數並不會導致多次求值過程,參數的副作用並不會造成任何特殊問題。
參數類型 宏與類型無關,只要參數的操作是合法的,它可以用於任何參數類型。 函數的參數是與類型有關係的,如果參數的類型不同,就需要使用不同的函數,即使它們執行的任務是相同的。
  1. 補充: 普通函數的調用堆棧情況.
    比如:
    void P(){
        phase 1;
        Q();
        phase 2;
    }
    
    void Q() {
        phase 3;
        R();
        phase 4;
    }
    
    P( ),Q( ),R( )在經過編譯器變爲彙編後, 都有自己的代碼位置,比如
    0x0000400A P:
                xxx     #phase1
                call Q
                xxx     #phase2
                ret
    0x0000400B Q:
                yyy     #phase3
                call R
                yyy     #phase4
    0x0000400C R: 
                zzz     #phase
                zzz     #phase
    
    而對應的棧幀呢:
    ---
    P的參數入棧
    返回地址1(phase2的地址,即函數Q後的一下條語句)
    ---
    Q的參數入棧
    返回地址2(phas4的地址,即函數R後的一下條語句)
    ---
    R的參數
    ---
    
    當R函數調用完畢,則R的棧幀出棧,同時將返回地址2pop,返回給rip,這樣就繼續執行一下條語句了.Q調用完畢也是同理的,所以這就是普通函數的調用情況.
    很顯然,普通函數調用在彙編後,這個函數只有一份代碼量,調用它就入棧出棧它的地址,造成一定的開銷.而對於宏定義來說,只是簡單的文本替換,這在預處理時就替換過了,故實際上你可以認爲就沒有宏定義出來的函數,所以當然不會有這樣的開銷.

普通函數VS內聯函數 內聯函數VS宏

例子:

inline void P() {
    phase P1;
    phase P2;
}

void Q() {
    phase1;
    P();
    phase2;
}

P是一個內聯函數,對於Q來說編譯後得到的彙編是什麼樣子呢?

Q:
    xxx     #phase 1
    yyy     #phase P1
    zzz     #phase P2
    xxx     #pahse 2

我們可以發現,Q的彙編代碼中並沒有對P的調用(call),而是直接將P的代碼拷到Q中,這樣的好處就是由於沒有call ret,所以就沒有普通函數調用帶來的出棧入棧開銷,速度更快,但增加代碼量,增加內存開銷.

那麼內聯函數又有哪些不同?
內聯函數本質上還是一個函數,只是在調用時不必call ret罷了.

  • 內聯函數是一個真正的函數,遵循函數的類型和作用域規則,編譯器在調用一個內聯函數時,會首先檢查它的參數的類型,保證調用正確,這樣就消除了它的隱患和侷限性;
    即: inline void func(int i,int j);調用這個時候,我必須符合參數類型才能正確調用,而宏直接替換就完事了,是類型無關的.
  • 內聯函數可以作爲某個類的成員函數,這樣就可以在其中使用所在類的保護成員及私有成員;
  • 宏是不加任何驗證的簡單代碼替換,除非萬不得已,不要使用。

注意:
內聯是以代碼膨脹(複製)爲代價,僅僅省去了函數調用的開銷,從而提高函數的執行效率。如果執行函數代碼的時間比處理函數調用機制的時間長,則節省的時間佔比很小;如果代碼執行時間很短,則內聯函數就可以節省函數調用的時間。

  • 含有遞歸調用的函數不能設置爲inline;
  • 使用了複雜流程控制語句:循環語句和switch語句,不能設置爲inline;
  • 由於inline增加體積的特性,所以建議inline函數內的代碼應很短小。最好不超過5行;
  • 內聯函數應該在頭文件中定義,關鍵字inline 必須與函數定義體放在一起才能使函數成爲內聯,僅將inline 放在函數聲明前面不起任何作用。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章