C語言的變數宣告規則
在初學C的時候,你一定會學到如何宣告一個變數,但變數宣告並沒有想像中那麼簡單,變數宣告可以變得更複雜,更不直觀
本文將會帶你了解C語言中的變數宣告規則,讓你可以僅用肉眼就能辨識出某個變數的型別,並善用typedef
簡化你的宣告式
一般變數宣告
一般變數宣告,是C語言中最基本的操作,包含宣告最基本型別 ( 如int
,array
,pointer
等 ) 和自訂型別(struct
,union
)
想必大家都可以簡單的辨識下列程式的意義
1 | int variable = 5; |
上面程式的意思是產生一個型別為int
的變數variable
,並設定variable
為5
基本的陣列宣告或是指標宣告對於程式新手來說想必也不是問題
1 | int array[5]; // 含有5個int元素的陣列array |
函式指標
函式指標是指一個指向函式的指標變數,表現上比較不直觀,但是學久了就會慢慢熟悉這個樣貌
有學過函式指標的人應該可以了解下面的程式
1 | int (*func)(void); |
這段的意思是宣告一個名為func
的函式指標,該指標指向一個沒有參數,回傳int
的函式
不過,接下來的程式就不是每個人都會解的了
1 | void (*signal(int, void(*)(int)))(int); |
這是C標準函式庫中,用來處理信號的函式的宣告
看起來不是很直觀,它給人一種很像函式指標,卻又不像函式指標的樣子
短時間內很難去解析這個函式的樣貌為何
不過只要善用typedef
,就可以使得這個函式變得好看許多
使用typedef
typedef
的用途是用來對某個類別進行簡化,或者說給它一個別名,本質上很像變數宣告,但最後產生出來的是一個型別
首先我們先宣告一個型別定義
1 | typedef void (*SignalHandler)(int); |
這段看起來很像是宣告一個函式指標變數,不過它其實是宣告一個函式指標類型,名為SignalHandler
接著我們對signal
這個函式套用SignalHandler
,最後會變成
1 | SignalHandler signal(int, SignalHandler); |
有沒有覺得這樣好看多了呢?
你可能會覺得很神奇,原本一個看起來有點複雜的函式最後卻可以簡化成這麼簡單的樣子
但是簡化之前我們要如何知道它可以這樣被簡化呢?
這種時候你就需要了解宣告的規則,抽出型別,再針對該型別進行typedef
,最後進行簡化
宣告的規則
其實所謂的規則比較像是觀察出來的結果
C語言的宣告式子你可以分解成:最終型別 中間過程 ;
舉例來說:
1 | int var; |
上面的式子可以拆成int
跟var
你最後可以得知var
最終會得到一個型別為int
的資料
再進一步舉例:
1 | int *pointer; |
可以被拆成int
跟*pointer
這表示*pointer
可以得到型別為int
的資料
接著再去推導*pointer
,你會發現這是一個取值的過程,這表示pointer
將會是一個指標
最後得出pointer
將會是一個指標只向一個型別為int
的資料
注意:如果是 IDE ,
pointer
的型別會是int*
但是你在解析時要把*
跟變數放在一塊看
接著是函式指標的解析,我要拆解
1 | int (*func)(void); |
依照剛剛的規則,拆成int
跟(*func)(void)
這2塊,由此可以得知經過(*func)(void)
會得到int
接著觀察(*func)(void)
,你可以發現它其實是呼叫了一個無參數的函式(*func)
在繼續往下,函式(*func)
來自*func
,*
是一個取值運算子,因此func
是一個指標
接著將剛剛的所有結果串起來:
int
來自一個無參數的函式,函式來自*func
因此func
便是一個指向無參數,回傳int
的函式的指標
大致上到這裡如果你都能了解,那就已經差不多可以來分析signal
函式了
注:typedef 的宣告樣式和變數宣告一模一樣,差別就在 typedef 會產生型別,而一般的宣告會產生變數
分析 signal
我們先看一下signal的長相
1 | void (*signal(int, void(*)(int)))(int); |
一樣,先拆成2個部份:void
跟(*signal(int, void(*)(int)))(int)
,(*signal(int, void(*)(int)))(int)
最後回傳void
,
接著解析(*signal(int, void(*)(int)))(int)
,可以發現他是一個參數為int
的函式(*signal(int, void(*)(int)))
再來是(*signal(int, void(*)(int)))
,這邊使用了取值運算子*
然後你就會看到我們的函式主體signal(int, void(*)(int))
因此,藉由剛剛的推導,我們可以知道signal
回傳了一個指向參數為int
,回傳void
的函式指標,也就是void (*)(int)
而signal
的參數則是int
跟void (*)(int)
由於函式指標的樣子實在不太好看,因此使用typedef
進行簡化:
1 | typedef void (*SignalHandler)(int); |
接著將void (*)(int)
替換成SignalHandler
,最後我們的signal
函式就變成了:
1 | SignalHandler signal(int, SignalHandler); |
這樣看起來就會簡單很多
typedef 的用途
經過了剛剛的解析與改寫,你會發現typedef
可以簡化複雜難懂的類別,並且使得該類別的名子更具有意義,可以大大的增加程式碼的可讀性,而且使用typedef
,也可以很簡單的替換型別
這麼好用的typedef
,還不用嗎?
單一型別的多重宣告
有些人可能看過這樣的語法
1 | int var1, var2 = 5; |
這將會建立兩個變數var1
跟var2
這種宣告方式一樣也可以拆成最終型別跟中間過程
後面的部份通通都是中間過程,用逗號做區隔
因此你可以這樣宣告
1 | int var, // int的變數 |
觀察上面的程式,不難發現一般變數跟pointer
可以同時在裡面
由此可知,pointer
是跟著變數走的
因此
int *var1, var2
中var2
的型別並不是int*
,而是int
Struct 宣告
其實struct
跟union
的宣告也是變數宣告的一環,舉例來說:
1 | struct my_struct_t { |
上面的式子不僅產生了自訂型別struct my_struct_t
,同時也產生了型別為struct my_struct_t
的變數var1
結語
看完是不是有一種 “想不到變數宣告可以這樣解析呢!” 的想法呢?
老實說我第一次看到signal
函式時也是蠻驚訝的,第一次見到如此神奇的函數宣告,甚至還有人觀察出了一種特殊的解法來解析signal
函式,名子我不太記得了,但大致上的原理是從變數名稱開始左右掃射,直到所有的符號都看完為止,就可以解析出這個變數的型別
不過我並沒有運用那種方式,畢竟實做起來感覺還是有點麻煩