C語言的變數宣告規則

在初學C的時候,你一定會學到如何宣告一個變數,但變數宣告並沒有想像中那麼簡單,變數宣告可以變得更複雜,更不直觀

本文將會帶你了解C語言中的變數宣告規則,讓你可以僅用肉眼就能辨識出某個變數的型別,並善用typedef簡化你的宣告式

一般變數宣告

一般變數宣告,是C語言中最基本的操作,包含宣告最基本型別 ( 如intarraypointer等 ) 和自訂型別(structunion)

想必大家都可以簡單的辨識下列程式的意義

1
int variable = 5;

上面程式的意思是產生一個型別為int的變數variable,並設定variable為5

基本的陣列宣告或是指標宣告對於程式新手來說想必也不是問題

1
2
int  array[5]; // 含有5個int元素的陣列array
int *pointer; // 指向int的指標pointer

函式指標

函式指標是指一個指向函式的指標變數,表現上比較不直觀,但是學久了就會慢慢熟悉這個樣貌

有學過函式指標的人應該可以了解下面的程式

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;

上面的式子可以拆成intvar

你最後可以得知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的參數則是intvoid (*)(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;

這將會建立兩個變數var1var2

這種宣告方式一樣也可以拆成最終型別中間過程

後面的部份通通都是中間過程,用逗號做區隔

因此你可以這樣宣告

1
2
3
4
int   var, 		    // int的變數
array[5], // 有5個int元素的陣列
*pointer, // 指向int資料的指標
(*func_pointer)(void); // 指向無參數,回傳int的函式指標

觀察上面的程式,不難發現一般變數跟pointer可以同時在裡面

由此可知,pointer是跟著變數走的

因此

int *var1, var2

var2的型別並不是int*,而是int

Struct 宣告

其實structunion的宣告也是變數宣告的一環,舉例來說:

1
2
3
struct my_struct_t {
int field;
} var1;

上面的式子不僅產生了自訂型別struct my_struct_t,同時也產生了型別為struct my_struct_t的變數var1

結語

看完是不是有一種 “想不到變數宣告可以這樣解析呢!” 的想法呢?

老實說我第一次看到signal函式時也是蠻驚訝的,第一次見到如此神奇的函數宣告,甚至還有人觀察出了一種特殊的解法來解析signal函式,名子我不太記得了,但大致上的原理是從變數名稱開始左右掃射,直到所有的符號都看完為止,就可以解析出這個變數的型別

不過我並沒有運用那種方式,畢竟實做起來感覺還是有點麻煩