在第 1 節 “復合類型與結構體”講過算術類型、標量類型的概念,現在又學習了幾種類型,我們完整地總結一下C語言的類型。下圖出自[Standard C]。
C語言的類型分為函數類型、對象類型和不完全類型三大類。對象類型又分為標量類型和非標量類型。指針類型屬於標量類型,因此也可以做邏輯與、或、非運算的操作數和if、for、while的控製表達式,NULL指針表示假,非NULL指針表示真。不完全類型是暫時沒有完全定義好的類型,編譯器不知道這種類型該占幾個位元組的存儲空間,例如:
struct s; union u; char str[];
具有不完全類型的變數可以通過多次聲明組合成一個完全類型,比如數組str聲明兩次:
char str[]; char str[10];
當編譯器碰到第一個聲明時,認為str是一個不完全類型,碰到第二個聲明時str就組合成完全類型了,如果編譯器處理到程序檔案的末尾仍然無法把str組合成一個完全類型,就會報錯。讀者可能會想,這個語法有什麼用呢?為何不在第一次聲明時就把str聲明成完全類型?有些情況下這麼做有一定的理由,比如第一個聲明是寫在標頭檔裡的,第二個聲明寫在.c檔案裡,這樣如果要改數組長度,只改.c檔案就行了,標頭檔可以不用改。
不完全的結構體類型有重要作用:
struct s {
struct t *pt;
};
struct t {
struct s *ps;
};struct s和struct t各有一個指針成員指向另一種類型。編譯器從前到後依次處理,當看到struct s { struct t* pt; };時,認為struct t是一個不完全類型,pt是一個指向不完全類型的指針,儘管如此,這個指針卻是完全類型,因為不管什麼指針都占4個位元組存儲空間,這一點很明確。然後編譯器又看到struct t { struct s *ps; };,這時struct t有了完整的定義,就組合成一個完全類型了,pt的類型就組合成一個指向完全類型的指針。由於struct s在前面有完整的定義,所以struct s *ps;也定義了一個指向完全類型的指針。
這樣的類型定義是錯誤的:
struct s {
struct t ot;
};
struct t {
struct s os;
};編譯器看到struct s { struct t ot; };時,認為struct t是一個不完全類型,無法定義成員ot,因為不知道它該占幾個位元組。所以結構體中可以遞歸地定義指針成員,但不能遞歸地定義變數成員,你可以設想一下,假如允許遞歸地定義變數成員,struct s中有一個struct t,struct t中又有一個struct s,struct s又中有一個struct t,這就成了一個無窮遞歸的定義。
以上是兩個結構體構成的遞歸定義,一個結構體也可以遞歸定義:
struct s {
char data[6];
struct s* next;
};當編譯器處理到第一行struct s {時,認為struct s是一個不完全類型,當處理到第三行struct s *next;時,認為next是一個指向不完全類型的指針,當處理到第四行};時,struct s成了一個完全類型,next也成了一個指向完全類型的指針。類似這樣的結構體是很多種資料結構的基本組成單元,如鏈表、二叉樹等,我們將在後面詳細介紹。下圖示意了由幾個struct s結構體組成的鏈表,這些結構體稱為鏈表的節點(Node)。
head指針是鏈表的頭指針,指向第一個節點,每個節點的next指針域指向下一個節點,最後一個節點的next指針域為NULL,在圖中用0表示。
可以想像得到,如果把指針和數組、函數、結構體層層組合起來可以構成非常複雜的類型,下面看幾個複雜的聲明。
typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler);
這個聲明來自signal(2)。sighandler_t是一個函數指針,它所指向的函數帶一個參數,返回值為void,signal是一個函數,它帶兩個參數,一個int參數,一個sighandler_t參數,返回值也是sighandler_t參數。如果把這兩行合成一行寫,就是:
void (*signal(int signum, void (*handler)(int)))(int);
在分析複雜聲明時,要借助typedef把複雜聲明分解成幾種基本形式:
T *p;,p是指向T類型的指針。
T a[];,a是由T類型的元素組成的數組,但有一個例外,如果a是函數的形參,則相當於T *a;
T1 f(T2, T3...);,f是一個函數,參數類型是T2、T3等等,返回值類型是T1。
我們分解一下這個複雜聲明:
int (*(*fp)(void *))[10];
1、fp和*號括在一起,說明fp是一個指針,指向T1類型:
typedef int (*T1(void *))[10]; T1 *fp;
2、T1應該是一個函數類型,參數是void *,返回值是T2類型:
typedef int (*T2)[10]; typedef T2 T1(void *); T1 *fp;
3、T2和*號括在一起,應該也是個指針,指向T3類型:
typedef int T3[10]; typedef T3 *T2; typedef T2 T1(void *); T1 *fp;
顯然,T3是一個int數組,由10個元素組成。分解完畢。