typedef是一種特殊的聲明方式,不過它與普通聲明的含義大不相同。普通聲明的主角是“變量”,它或是創建一個新變量或是對外文件變量使用前的聲明;而typedef聲明的主角則是“類型”,通過這個聲明對一種數據類型引入新的名字。從引入新名字這個角度來說,typedef聲明又和宏定義有些相似:用新名字代替已有的名字。接下來的敘述會看到這兩者之間的區別。
typedef是特殊的
正如一開始所說的那樣,typedef是特殊的聲明。最常見以及常用的方式如下:
1 /* 代碼段1 */
2 struct stuinfo
3 {
4 char id[20];
5 char name[20];
6 int age;
7 };
8 typedef struct stuinfo stu; /* 語句1 */
通過typedef聲明為stuinfo結構體引入了一個新的名字stu。現在stuinfo結構和stu屬於同一種數據類型,只不過兩者在聲明一個變量時使用的名字不同:
1 /* 代碼段2 */
2 stu mystu1;
3 struct stuinfo mystu2; /* 語句2 */
通過上述兩個代碼段,可以再一次的理解typedef聲明和普通聲明的區別。代碼段1通過typedef聲明為stuinfo引入了一個新的名字stu;而代碼段2則通過同一種數據類型的不同名稱分別聲明了兩個同類型的變量。注意到語句1和語句2,除了語句1在聲明前多了typedef關鍵字外,兩者在形式上幾乎一樣,因此都可以通過上文所述的聲明規則進行閱讀。正是由於typedef這個關鍵字,使得這兩種聲明的含義有着巨大差異。
像上面的舉例那樣通過typedef聲明而省去一個struct並沒有多大的意義。使用typedef聲明的最大優點是可以簡潔的表達一個指針。比如ANSI C中的signal(),它的定義如下:
1 void ( *signal(int signum, void (*handler)(int)) ) (int);
考驗你的時刻到了!你是否能快速說出這個聲明的含義?
這個復雜的語句聲明了signal函數,這個函數有兩個參數signum和handler;signum參數是一個整型變量。handler是一個函數指針,指向一個擁有整型參數並且返回空值的函數;signal函數的返回值是一個函數指針,該指針同樣指向一個擁有整型參數並且返回空值的函數。
對於這個復雜聲明的解讀的確很令人費勁。但是經過typedef的改進,它的閱讀過程就簡化了很多:
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
通過typedef的聲明,使得sighandler_t是這樣一種類型:它是一個函數指針,該函數擁有一個整型參數並且返回空值。第二條語句則聲明了函數signal,它擁有兩個參數signum和handler。並且這個函數的返回值和參數handler都是sighandler_t類型的。
雖然這樣的聲明在形式上簡潔許多,不過和普通聲明一樣,此時閱讀聲明時仍然要記住聲明符號的優先級規則。這種困擾在C語言中是難以避免的。
typedef int x和#define x int是不一樣的
typedef和宏定義看似都是文本替換,但其實質不同。typedef表面上是對已有數據類型引入新名稱,實則是對數據類型的嚴格封裝。這種封裝體現在下述兩個方面。
首先,經過宏定義后的類型名可以進行再次擴展,但是經過typedef引入的類型名則不能進行擴充。比如:
#define myint1 int
unsigned myint1 x; /* 正確 */
typedef int myint2;
unsigned myint2 x; /* 語法錯誤! */
由於typedef是一種嚴格的數據封裝,它只引入了myint2類型而沒有引入unsigned myint2類型。也就是說,通過typedef的聲明,編譯器只能識別myint2類型。而unsigned myint2既不是基本類型也不是經過typedef聲明過的類型,編譯器就無法識別。
其次,在連續的幾個變量聲明中,使用typedef定義的類型能夠保證所有變量均為相同類型,而用宏定義的變量則無法保證統一性。比如:
#define myint1 int *
myint1 x,y; /* 經過宏替換后為: int *x,y; */
typedef int * myint2;
myint2 x,y;
由於宏定義只是直接的文本替換,因此只能保證x是整型的指針變量而y為整型變量。而typedef定義過的類型myint2則是對int *的完全封裝,所以x和y均為整型的指針變量。
C語言中的名字空間
在說明名字空間之前,先閱讀下面的代碼:
1 #include < stdio.h >
2 #include < string.h >
3
4 struct id
5 {
6 int id;
7 }id;
8
9 typedef struct name
10 {
11 char name[20];
12 }name;
13
14 struct name name1;
15 name name2;
16
17 int main()
18 {
19 id.id = 1;
20 strcpy(name1.name,"hello,");
21 strcpy(name2.name,"edsionte!");
22 printf("id.id = %d, %s%s\n",id.id,name1.name,name2.name);
23 return 0;
24 }
25
26 /* 運行結果 */
27 id.id = 1, hello,edsionte!
你可能已經發現在上述代碼中出現了多個id和name,並且這樣的代碼可以成功的編譯。這些相同的名字標簽為何可以同時出現?每個標簽代表什么含義?這些問題將是下面的重點。
以上述代碼中4到7行的代碼為例,這條語句中包含三個id標簽。它們分別對應C語言中三種常見的名字空間:
- 結構標簽:這種標簽用於結構體、聯合體和枚舉類型;struct后的id即為此類型的名字空間;
- 成員名:每個結構體或聯合體內部都與屬於自己的名字空間;struct內部的成員id即為此類型;
- 標簽名:聲明中的標示符;比如最后一個id,他是struct id類型的變量;
由於這三種標簽所處的名字空間不同,因此它們可以同時存在。但是在同一個名字空間中不能出現多個同名的標簽。常見的例子就是一個結構體內不可能出現同名的變量。
根據上面對名字空間的划分,9到12行的代碼的解釋為:struct后的name屬於結構體標簽;結構體內部的name屬於成員名;而最后一個name屬於聲明的標示符;整條語句的含義是通過typedef聲明將name結構體重命名為name。
通過上面對typedef的分析,你應該對於struct name和name均可以聲明一個變量不再陌生。此處我們用名字空間的來理解他們的區別,14句中的name屬於結構標簽,15句中的name屬於一種類型的名稱。
上述同名的情況在日常的代碼中實屬罕見,這里只是為了說明名字空間而特別的舉例。一般為了提高代碼的可閱讀性,最好對容易產生混淆的標簽加上特別標記。比如VFS中inode和dentry結構體,兩者內部均有flag一字段。盡管不同的結構體內有各自的名字空間,但是實際命名時仍然采用i_flag和d_flag。
參考:
《C專家編程》