9. 不完全類型和複雜聲明

第 1 節 “復合類型與結構體”講過算術類型、標量類型的概念,現在又學習了幾種類型,我們完整地總結一下C語言的類型。下圖出自[Standard C]

圖 23.5. C語言類型總結

C語言類型總結

C語言的類型分為函數類型、對象類型和不完全類型三大類。對象類型又分為標量類型和非標量類型。指針類型屬於標量類型,因此也可以做邏輯與、或、非運算的操作數和ifforwhile的控製表達式,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 sstruct 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 tstruct t中又有一個struct sstruct 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)

圖 23.6. 鏈表

鏈表

head指針是鏈表的頭指針,指向第一個節點,每個節點的next指針域指向下一個節點,最後一個節點的next指針域為NULL,在圖中用0表示。

可以想像得到,如果把指針和數組、函數、結構體層層組合起來可以構成非常複雜的類型,下面看幾個複雜的聲明。

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

這個聲明來自signal(2)sighandler_t是一個函數指針,它所指向的函數帶一個參數,返回值為voidsignal是一個函數,它帶兩個參數,一個int參數,一個sighandler_t參數,返回值也是sighandler_t參數。如果把這兩行合成一行寫,就是:

void (*signal(int signum, void (*handler)(int)))(int);

在分析複雜聲明時,要借助typedef把複雜聲明分解成幾種基本形式:

我們分解一下這個複雜聲明:

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