C語言的inline


轉以前我用Docbook寫的一篇關於C語言inline關鍵字使用的文章。唉,要是能用docbook直接寫Blog就好了。用得越多發現Docbook這個東西真是越好用啊~~

   

本文介紹了GCC和C99標準中inline使用上的不同之處。inline屬性在使用的時候,要注意以下兩點:

  1. inline關鍵字在GCC參考文檔中僅有對其使用在函數定義(Definition)上的描述,而沒有提到其是否能用於函數聲明(Declare)。

inline的作用來看,其放置於函數聲明中應當也是毫無作用的:inline只會影響函數在translation unit(可以簡單理解爲C源碼文件)內的編譯行爲,只要超出了這個範圍inline屬性就沒有任何作用了。所以inline關鍵字不應該出現在函數聲明中,沒有任何作用不說,有時還可能造成編譯錯誤(在包含了sys/compiler.h的情況下,聲明中出現inline關鍵字的部分通常無法編譯通過);

  1. inline關鍵字僅僅是建議編譯器做內聯展開處理,而不是強制。在gcc編譯器中,如果編譯優化設置爲-O0,即使是inline函數也不會被內聯展開,除非設置了強制內聯(__attribute__((always_inline)))屬性。

1. GCC的inline

gcc對C語言的inline做了自己的擴展,其行爲與C99標準中的inline有較大的不同。

1.1. static inline

GCC的static inline定義很容易理解:你可以把它認爲是一個static的函數,加上了inline的屬性。這個函數大部分表現和普通的static函數一樣,只不過在調用這種函數的時候,gcc會在其調用處將其彙編碼展開編譯而不爲這個函數生成獨立的彙編碼。除了以下幾種情況外:

  • 函數的地址被使用的時候。如通過函數指針對函數進行了間接調用。這種情況下就不得不爲static inline函數生成獨立的彙編碼,否則它沒有自己的地址。

  • 其他一些無法展開的情況,比如函數本身有遞歸調用自身的行爲等。

static inline函數和static函數一樣,其定義的範圍是local的,即可以在程序內有多個同名的定義(只要不位於同一個文件內即可)。

注意


gcc的static inline的表現行爲和C99標準的static inline是一致的。所以這種定義可以放心使用而沒有兼容性問題。


要點:


gcc的static inline相對於static函數來說只是在調用時建議編譯器進行內聯展開;

gcc不會特意爲static inline函數生成獨立的彙編碼,除非出現了必須生成不可的情況(如通過函數指針調用和遞歸調用);

gcc的static inline函數僅能作用於文件範圍內。


1.2. inline

相對於C99的inline來說,GCC的inline更容易理解:可以認爲它是一個普通全局函數加上了inline的屬性。即在其定義所在文件內,它的表現和static inline一致:在能展開的時候會被內聯展開編譯。但是爲了能夠在文件外調用它,gcc一定會爲它生成一份獨立的彙編碼,以便在外部進行調用。即從文件外部看來,它和一個普通的extern的函數無異。舉個例子:

foo.c:

 

/* 這裏定義了一個inline的函數foo() */

inline foo() {

...; <- 編譯器會像非inline函數一樣爲foo()生成獨立的彙編碼

}

 

void func1() {

foo(); <- 同文件內foo()可能被編譯器內聯展開編譯而不是直接call上面生成的彙編碼

}

而在另一個文件裏調用foo()的時候,則直接call的是上面文件內生成的彙編碼:

bar.c:

 

extern foo(); <- 聲明foo(),注意不能在聲明內帶inline關鍵字

 

void func2() {

foo(); <- 這裏就是直接call在foo.c內爲foo()函數生成的彙編碼了

}

重要


雖然gcc的inline函數的行爲很好理解,但是它和C99的inline是有很大差別的。請注意看後面對C99 inline的描述(第 2.2 節 "inline"),以及如何以兼顧GCC和C99的方式使用inline函數。


要點:


gcc的inline函數相對於普通extern函數來說只是在同一個文件內調用時建議編譯器進行內聯展開;

gcc一定會爲inline函數生成一份獨立的彙編碼,以便其在本文件之外被調用。在別的文件內看來,這個inline函數和普通的extern函數無異;

gcc的inline函數是全局性的:在文件內可以作爲一個內聯函數被內聯展開,而在文件外可以調用它。

1.3. extern inline

GCC的static inlineinline都很好理解:看起來都像是對普通函數添加了可內聯的屬性。但是這個extern inline就千萬不能想當然地理解成就是一個extern的函數+inline屬性了。實際上gcc的extern inline十分古怪:一個extern inline的函數只會被內聯進去,而絕對不會生成獨立的彙編碼!即使是通過指針應用或者是遞歸調用也不會讓編譯器爲它生成彙編碼,在這種時候對此函數的調用會被處理成一個外部引用。另外,extern inline的函數允許和外部函數重名,即在存在一個外部定義的全局庫函數的情況下,再定義一個同名的extern inline函數也是合法的。以下用例子具體說明一下extern inline的特點:

foo.c:

 

extern inline

int foo(int a)

{

return (-a);

}

 

void func1()

{

...;

a = foo(a); ①

p_foo = foo; ②

b = p_foo(b); ③

}

在這個文件內,gcc不會生成foo函數的彙編碼。在func1中的調用點①,編譯器會將上面定義的foo函數在這裏內聯展開編譯,其表現類似於普通inline函數。因爲這樣的調用是能夠進行內聯處理的。而在②處,引用了foo函數的地址。但是注意:編譯器是絕對不會爲externinline函數生成獨立彙編碼的!所以在這種非要個函數地址不可的情況下,編譯器不得不將其處理爲外部引用,在鏈接的時候鏈接到外部的foo函數去(填寫外部函數的地址)。這時如果外部沒有再定義全局的foo函數的話就會在鏈接時產生foo函數未定義的錯誤。

假設在另一個文件裏面也定義了一個全局函數foo:

foo2.c:

 

int foo(int a)

{

return (a);

}

那麼在上面那個例子裏面,後面一個對foo函數地址的引用就會在鏈接時被指到這個foo2.c中定義的foo函數去。也就是說:①調用foo函數的結果是a=-a,因爲其內聯了foo.c內的foo函數;而③調用的結果則是b=b,因爲其實際上調用的是foo2.c裏面的foo函數!

extern inline的用法很奇怪也很少見,但是還是有其實用價值的。第一:它可以表現得像宏一樣,可以在文件內用extern inline版本的定義取代外部定義的庫函數(前提是文件內對其的調用不能出現無法內聯的情況);第二:它可以讓一個庫函數在能夠被內聯的時候儘量被內聯使用。舉個例子:

在一個庫函數的c文件內,定義一個普通版本的庫函數libfunc:

lib.c:

 

void libfunc()

{

...;

}

然後再在其頭文件內,定義(注意不是聲明!)一個實現相同的exterin inline的版本:

lib.h:

 

extern inline libfunc()

{

...;

}

那麼在別的文件要使用這個庫函數的時候,只要include了lib.h,在能內聯展開的地方,編譯器都會使用頭文件內extern inline的版本來展開。而在無法展開的時候(函數指針引用等情況),編譯器就會引用lib.c中的那個獨立編譯的普通版本。即看起來似乎是個可以在外部被內聯的函數一樣,所以這應該是gcc的extern inline意義的由來。

但是注意這樣的使用是有代價的:c文件中的全局函數的實現必須和頭文件內extern inline版本的實現完全相同。否則就會出現前面所舉例子中直接內聯和間接調用時函數表現不一致的問題。

重要


gcc的extern inline函數的用法相當奇怪,使用的範圍也非常狹窄:幾乎沒有什麼情況會需要用它。


在C99中,也沒有關於extern inline這樣的描述,所以不建議大家使用externinline,除非你明確理解了這種用法的意義並且有充足的理由使用它!


要點:


gcc絕對不會爲extern inline的函數生成獨立彙編碼

extern inline函數允許和全局函數重名,可以在文件範圍內替代外部定義的全局函數

extern inline函數的應用範圍十分狹窄,而且行爲比較奇怪,不建議使用

2. C99inline

以下主要描述C99的inline與Gcc不同的部分。對於相同的部分請參考GCC inline的說明。

2.1. static inline

同GCC的static inline第 1.1 節 "static inline")。

2.2. inline

C99的inline的使用相當令人費解。當一個定義爲inline的函數沒有被聲明爲extern的時候,其表現有點類似於gcc中extern inline那樣(C99裏面這段描述有點晦澀,原文如下):

If all of the file scope declarations for a function in a translation unit include the

inline function specifier without extern, then the definition in that translation unit is an inline definition. An inline definition does not provide an external definition for the function, and does not forbid an external definition in another translation unit. An inlinedefinition provides an alternative to an external definition, which a translator may use to implement any call to the function in the same translation unit. It is unspecified whether a call to the function uses the inline definition or the external definition.

即如果一個inline函數在文件範圍內沒有被聲明爲extern的話,這個函數在文件內的表現就和gcc的extern inline相似:在本文件內調用時允許編譯器使用本文件內定義的這個內聯版本,但同時也允許外部存在同名的全局函數。只是比較奇怪的是C99居然沒有指定編譯器是否必須在本文件內使用這個inline的版本而是讓編譯器廠家自己來決定,相當模糊的定義。

如果在文件內把這個inline函數聲明爲extern,則這個inline函數的表現就和gcc的inline一致了:這個函數即成爲一個"external definition"(可以簡單理解爲全局函數):可以在外部被調用,並且在程序內僅能存在一個這樣名字的定義。

下面舉例說明C99中inline的特性:

inline double fahr(double t)

{

return (9.0 * t) / 5.0 + 32.0;

}

 

inline double cels(double t)

{

return (5.0 * (t - 32.0)) / 9.0;

}

 

extern double fahr(double); ①

 

double convert(int is_fahr, double temp)

{

return is_fahr ? cels(temp) : fahr(temp); ②

}

在上面這個例子裏,函數fahr是個全局函數:因爲在①處將fahr聲明爲extern,因此在②處調用fahr的時候使用的一定是這個文件內所定義的版本(只不過編譯器可以將這裏的調用進行內聯展開)。在文件外部也可以調用這個函數(說明像gcc的inline一樣,編譯器在這種情況下會爲fahr生成獨立的彙編碼)。

而cels函數因爲沒有在文件範圍內被聲明爲extern,因此它就是前面所說的"inline definition",這時候它實際上僅能作用於本文件範圍(就像一個static的函數一樣),外部也可能存在一個名字也爲cels的同名全局函數。在②處調用cels的時候編譯器可能選擇用本文件內的inline版本,也有可能跑去調用外部定義的cels函數(C99沒有規定此時的行爲,不過編譯器肯定都會盡量使用文件內定義的inline版本,要不然inline函數就沒有存在的意義了)。從這裏的表現上看C99中未被聲明爲extern的inline函數已經和gcc的extern inline十分相似了:本文件內的inline函數可以作爲外部庫函數的替代。

重要


C99標準中的inline函數行爲定義的比較模糊,並且inline函數有沒有在文件範圍內被聲明爲extern的其表現有本質不同。如果和gcc的inline函數比較的話,一個被聲明爲extern的inline函數基本等價於GCC的普通inline函數;而一個沒有被聲明爲extern的inline函數基本等價於GCC的extern inline函數。


因爲C99的inline函數如此古怪,所以在使用的時候,建議爲所有的inline函數都在頭文件中創建extern的聲明:


foo.h:


extern foo();


而在定義inline函數的c文件內include這個頭文件:


foo.c:


#include "foo.h"


inline void foo()


{


...;


}


這樣無論是用gcc的inline規則還是C99的,都能得到完全相同的結果:foo函數會在foo.c文件內被內聯使用,而在外部又可以像普通全局函數一樣直接調用。

2.3. extern inline

C99沒有見到extern inline的用法。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章