最近一直做C編譯器相關(guān)的開發(fā),感覺該總結(jié)一下。以前一直以為對C已經(jīng)足夠熟悉了,結(jié)果被它奇葩的語法樹震驚了。碰巧最近心血來潮,想把一個GNU的僵尸項(xiàng)目jamvm救活改造一下,又發(fā)現(xiàn)了GCC的一些奇葩C語言擴(kuò)展。
聲明
C語言的聲明足夠奇怪,以至于丑魚書用了一章來解釋這個問題。對于一般的變量聲明,C語言采用的語法一般是T var的形式,T表示變量的類型,var表示變量的名字。但對于數(shù)組的聲明,如果要聲明一個大小為10的int數(shù)組a,C語言需要
int a[10]
而不是
int[10] a
而許多其他的語言采用的往往是類似后者的方式,比如Java,C#,Go等等。
函數(shù)的聲明同樣的問題。比如這樣的一個函數(shù)
int foo(int a, int b)
{
return a + b;
}
它的類型可以表示為int ()(int, int), 其實(shí)一定有人發(fā)現(xiàn)了,如果對foo函數(shù)做前向引用的聲明,我們會這么寫:
int foo(int, int);
而不是
int ()(int, int) foo;
函數(shù)指針也是同樣的問題,對于foo函數(shù)的指針,類型可以表示為int (*)(int, int),但聲明函數(shù)指針時,我們只能
int (*f)(int, int)
即聲明了一個變量f,類型為指針類型,指向目標(biāo)的類型為int ()(int, int)。如果想用T var的形式聲明函數(shù)指針,只能曲線救國,利用typedef。
C語言之所以把聲明形式搞得這么復(fù)雜,原因或許是為了追求變量的定義和使用盡量寫法上保持一致。怎么樣,是不是很奇怪?除了引入復(fù)雜性,本身完全不一樣的兩個概念非要寫的一樣有何用?比如這個聲明是啥意思?
char* const (next)()
左值
先說結(jié)論。C99標(biāo)準(zhǔn)明確說明了Cast不能作為左值,所以現(xiàn)在的編譯器(gcc4.x或者clang3.x)遇到這種情況都會complain。但是老版本gcc居然有一個擴(kuò)展,名曰casts-as-lvalue。
左值可以簡單理解為允許被賦值的值,可以被放在賦值號(=)左邊的值。那什么樣的值可以放在賦值號左邊?權(quán)威的解答需要參考ISO/IEC 9899:1999。為了方便我用CIL對左值的定義舉例
lval =
| Mem of exp
| Var of varinfo</pre>
可以看到左值如果是一個表達(dá)式,那么一定只一個訪存操作。比如*(ptr+1) = 1,顯然這里的ptr是一個指針類型。在編譯jamvm1.0版本源碼的時候,出現(xiàn)了大量的lvalue required as left operand of assignment錯誤。這里錯誤大部分都長的這個樣*((long long*)ptr)++ = 1。我根據(jù)代碼上下文理解,它是想根據(jù)強(qiáng)制類型轉(zhuǎn)換后的類型進(jìn)行指針自增操作。不過經(jīng)過我調(diào)研,發(fā)現(xiàn)現(xiàn)在的編譯器已經(jīng)不支持一行代碼實(shí)現(xiàn)類似這樣語義的操作了。如果想要編譯含有這樣語句的C代碼,可以嘗試使用gcc3.3.6。gcc3.3.6支持cast-as-lvalue擴(kuò)展,但是本身并不含有cast-as-lvalue的代碼。
undefined behaviour
只舉個例子。比如*p++ = p[-1],賦值號兩邊的計算順序不同編譯器是不同的,C標(biāo)準(zhǔn)對它沒有做嚴(yán)格的要求。所以這種寫法在開發(fā)中一定要避免,clang默認(rèn)會拋一個warning,gcc不加-Wall參數(shù)不會有任何提示。
總結(jié)
- 不要為了少寫一兩行代碼而是用一些非標(biāo)準(zhǔn)的extenstion或者trick,得不償失。
- Treat all warning as error !
對JVM感興趣的話,jamvm1.0的確是個不錯的起點(diǎn),不到7000行的C,實(shí)現(xiàn)了一個java虛擬機(jī)該有的幾乎所有功能。問題是現(xiàn)在的主流編譯器都無法編譯它了。neojam是對jamvm1.0代碼修改后可以用gcc4.x編譯的版本,鏈接先放在這,相關(guān)文檔補(bǔ)全后會public。