在第 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個元素組成。分解完畢。