宏和函數的區別,C語言宏和函數區別詳解

2020-07-16 10:04:19
在 C 語言中,對於一些常用或通用的功能或程式碼段的封裝可以有兩種方式:函數和宏定義。那麼,對於這兩種方式,我們該如何抉擇呢?在解決這個問題之前,有必要先來了解一下它們之間的區別。

1) 從程式的執行來看

函數呼叫會帶來額外的開銷,它需要開闢一片棧空間,記錄返回地址,將形參壓棧,從函數返回還要釋放棧。這種開銷不僅會降低程式碼效率,而且程式碼量也會大大增加。而宏定義只在編譯前進行,不分配記憶體,不占執行時間,只占編譯時間,因此在程式碼規模和速度方面都比函數更勝一籌。

2) 從引數的型別來看

函數的引數必須宣告為一種特定的資料型別,如果引數的型別不同,就需要使用不同的函數來解決,即使這些函數執行的任務是相同的。而宏定義則不存在著型別問題,因此它的引數也是無型別的。也就是說,在宏定義中,只要引數的操作是合法的,它可以用於任何引數型別。

3) 從引數的副作用來看

毋庸置疑,在宏定義中,在對宏引數傳入自增(或者自減)之類的表示式時很容易引起副作用,儘管前面也給出了一些解決方案,但還是不能夠完全杜絕這種情況的發生。與此同時,在進行宏替換時,如果不使用括號完備地保護各個宏引數,那麼很可能還會產生意想不到的結果。除此之外,宏還缺少必要的型別檢查。而函數卻從根本上避免了這些情況的產生。

4) 從程式碼的長度來看

在每次使用宏時,一份宏定義程式碼的副本都會插入程式中。除非宏非常短,否則使用宏會大幅度地增加程式的長度。而函數程式碼則只會出現在一個地方,以後每次呼叫這個函數時,呼叫的都是那個地方的同一份程式碼。

不難發現,單從上面 4 點區別來看,函數和宏定義各有優缺點,這就要求我們根據具體情況具體分析,合理地對二者進行取捨。看下面兩個封裝範例:
/*宏定義的方式*/
#define  MAX(x,y) (((x)>(y)) ? (x):(y))
#define  MIN(x,y) (((x)<(y)) ? (x):(y))
/*函數的方式*/
int max(int x,int y)
{
    return  (x>y?x:y);
}
int min(int x,int y)
{
    return (x<y?x:y);
}
從表面上來看這兩個範例,使用宏的封裝方式明顯優於函數的方式,原因很簡單:如果這裡要繼續比較兩個浮點型別數的大小時,就不得不再寫兩個專門針對浮點型別數的比較函數,對於其他型別數的比較以此類推;而宏定義因為不存在任何型別問題,因此可以用於整型、長整型、浮點型以及其他任何可以使用“>”與“<”操作符比較值大小的型別,正所謂一勞永逸。

但是,假如我們這裡需要呼叫上面的MAX宏來尋找 i1、i2、i3、i4 與 i5 5 個數的最大者(甚至更多數),如下面的範例程式碼所示:
int i1=0;
int i2=1;
int i3=2;
int i4=3;
int i5=4;
int max=MAX(i1,(MAX(i2,(MAX(i3,MAX(i4,i5))))));
接下來,編譯器對語句“MAX(i1,(MAX(i2,(MAX(i3,MAX(i4,i5))))))”進行展開如下:

(((i1)>(((((i2)>(((((i3)>((((i4)>(i5)) ? (i4) : (i5)))) ? (i3) : ((((i4)>(i5)) ? (i4) : (i5))))))) ? (i2) : (((((i3)>((((i4)>(i5)) ? (i4) : (i5)))) ? (i3) : ((((i4)>(i5)) ? (i4) : (i5)))))))))) ? (i1) : (((((i2)>(((((i3)>((((i4)>(i5)) ? (i4) : (i5)))) ? (i3) : ((((i4)>(i5)) ? (i4) : (i5))))))) ? (i2) : (((((i3)>((((i4)>(i5)) ? (i4) : (i5)))) ? (i3) : ((((i4)>(i5)) ? (i4) : (i5))))))))));

上面的展開程式碼看起來很鬱悶,基本快花眼了。當然,這裡還可以對宏呼叫語句進行優化,如下面的範例程式碼所示:
MAX(MAX(MAX(i1,i2),MAX(i3,i4)),i5);
現在看起來雖然精簡許多,但還不是很樂觀,展開程式碼如下所示:

(((((((((i1)>(i2)) ? (i1) : (i2)))>((((i3)>(i4)) ? (i3) : (i4)))) ? ((((i1)>(i2)) ? (i1) : (i2))) : ((((i3)>(i4)) ? (i3) : (i4)))))>(i5)) ? (((((((i1)>(i2)) ? (i1) : (i2)))>((((i3)>(i4)) ? (i3) : (i4)))) ? ((((i1)>(i2)) ? (i1) : (i2))) : ((((i3)>(i4)) ? (i3) : (i4))))) : (i5));

面對這種情況,有讀者或許會認為函數比宏方便,程式碼也顯得苗條與可愛多了。但不能夠一概而論,應具體情況具體分析。

除此之外,一些特殊功能根本無法用函數實現時,可以選擇使用宏定義來實現。例如,引數型別無法作為引數傳遞給函數,但是可以把引數型別傳遞給帶參的宏,如下面的範例程式碼所示:
#define MALLOC(n,type) ((type *) malloc((n)* sizeof(type)))
現在,利用 MALLOC 宏,就可以為任何型別分配一段指定的空間大小,並返回指向這段空間的指標。如下面的範例程式碼所示:
p = MALLOC(8,int);
展開以後的結果為:
p = (int *) malloc((8) * sizeof(int));
由此可見,宏定義有時候還可以完成函數不能夠完成的一些特殊功能。因此,如何取捨這二者,還需要根據具體情況具體分析,千萬不能夠武斷地做出判斷。一般來說,應該用宏去替換小的、可重複的程式碼段,這樣可以使程式執行速度更快。當任務比較複雜,需要多行程式碼才能實現時,或者要求程式越小越好時,就應該使用函數。