在編程語言中,最基本的、不可再分的數據類型稱為基本類型(Primitive Type),例如整型、浮點型;根據語法規則由基本類型組合而成的類型稱為復合類型(Compound Type),例如字元串是由很多字元組成的。有些場合下要把復合類型當作一個整體來用,而另外一些場合下需要分解組成這個復合類型的各種基本類型,復合類型的這種兩面性為數據抽象(Data Abstraction)奠定了基礎。[SICP]指出,在學習一門編程語言時要特別注意以下三個方面:
本章以結構體為例講解數據類型的組合和數據抽象。至于過程抽象,我們在第 2 節 “if/else語句”已經見過最簡單的形式,就是把一組語句用一個函數名封裝起來,當作一個整體使用,本章將介紹更複雜的過程抽象。
現在我們用C語言表示一個複數。從直角座標系來看,複數由實部和虛部組成,從極座標系來看,複數由模和輻角組成,兩種座標系可以相互轉換,如下圖所示:
如果用實部和虛部表示一個複數,我們可以寫成由兩個double
型組成的結構體:
struct complex_struct { double x, y; };
這一句定義了標識符complex_struct
(同樣遵循標識符的命名規則),這種標識符在C語言中稱為Tag,struct complex_struct { double x, y; }
整個可以看作一個類型名[12],就像int
或double
一樣,只不過它是一個復合類型,如果用這個類型名來定義變數,可以這樣寫:
struct complex_struct { double x, y; } z1, z2;
這樣z1
和z2
就是兩個變數名,變數定義後面帶個;號是我們早就習慣的。但即使像先前的例子那樣只定義了complex_struct
這個Tag而不定義變數,}後面的;號也不能少。這點一定要注意,類型定義也是一種聲明,聲明都要以;號結尾,結構體類型定義的}後面少;號是初學者常犯的錯誤。不管是用上面兩種形式的哪一種定義了complex_struct
這個Tag,以後都可以直接用struct complex_struct
來代替類型名了。例如可以這樣定義另外兩個複數變數:
struct complex_struct z3, z4;
如果在定義結構體類型的同時定義了變數,也可以不必寫Tag,例如:
struct { double x, y; } z1, z2;
但這樣就沒辦法再次引用這個結構體類型了,因為它沒有名字。每個複數變數都有兩個成員(Member)x和y,可以用.運算符(.號,Period)來訪問,這兩個成員的存儲空間是相鄰的[13],合在一起組成複數變數的存儲空間。看下面的例子:
例 7.1. 定義和訪問結構體
#include <stdio.h> int main(void) { struct complex_struct { double x, y; } z; double x = 3.0; z.x = x; z.y = 4.0; if (z.y < 0) printf("z=%f%fi\n", z.x, z.y); else printf("z=%f+%fi\n", z.x, z.y); return 0; }
注意上例中變數x
和變數z
的成員x
的名字並不衝突,因為變數z
的成員x
只能通過表達式z.x
來訪問,編譯器可以從語法上區分哪個x
是變數x
,哪個x
是變數z
的成員x
,第 3 節 “變數的存儲佈局”會講到這兩個標識符x
屬於不同的命名空間。結構體Tag也可以定義在全局作用域中,這樣定義的Tag在其定義之後的各函數中都可以使用。例如:
struct complex_struct { double x, y; }; int main(void) { struct complex_struct z; ... }
結構體變數也可以在定義時初始化,例如:
struct complex_struct z = { 3.0, 4.0 };
Initializer中的數據依次賦給結構體的各成員。如果Initializer中的數據比結構體的成員多,編譯器會報錯,但如果只是末尾多個逗號則不算錯。如果Initializer中的數據比結構體的成員少,未指定的成員將用0來初始化,就像未初始化的全局變數一樣。例如以下幾種形式的初始化都是合法的:
double x = 3.0; struct complex_struct z1 = { x, 4.0, }; /* z1.x=3.0, z1.y=4.0 */ struct complex_struct z2 = { 3.0, }; /* z2.x=3.0, z2.y=0.0 */ struct complex_struct z3 = { 0 }; /* z3.x=0.0, z3.y=0.0 */
注意,z1
必須是局部變數才能用另一個變數x
的值來初始化它的成員,如果是全局變數就只能用常量表達式來初始化。這也是C99的新特性,C89隻允許在{}中使用常量表達式來初始化,無論是初始化全局變數還是局部變數。
{}這種語法不能用於結構體的賦值,例如這樣是錯誤的:
struct complex_struct z1; z1 = { 3.0, 4.0 };
以前我們初始化基本類型的變數所使用的Initializer都是表達式,表達式當然也可以用來賦值,但現在這種由{}括起來的Initializer並不是表達式,所以不能用來賦值[14]。Initializer的語法總結如下:
Initializer → 表達式
Initializer → { 初始化列表 }
初始化列表 → Designated-Initializer, Designated-Initializer, ...
(最後一個Designated-Initializer末尾可以有一個多餘的,號)
Designated-Initializer → Initializer
Designated-Initializer → .標識符 = Initializer
Designated-Initializer → [常量表達式] = Initializer
Designated Initializer是C99引入的新特性,用於初始化稀疏(Sparse)結構體和稀疏數組很方便。有些時候結構體或數組中只有某一個或某幾個成員需要初始化,其它成員都用0初始化即可,用Designated Initializer語法可以針對每個成員做初始化(Memberwise Initialization),很方便。例如:
struct complex_struct z1 = { .y = 4.0 }; /* z1.x=0.0, z1.y=4.0 */
數組的Memberwise Initialization語法將在下一章介紹。
結構體類型用在表達式中有很多限制,不像基本類型那麼自由,比如+ - * /等算術運算符和&& || !等邏輯運算符都不能作用於結構體類型,if
語句、while
語句中的控製表達式的值也不能是結構體類型。嚴格來說,可以做算術運算的類型稱為算術類型(Arithmetic Type),算術類型包括整型和浮點型。可以表示零和非零,可以參與邏輯與、或、非運算或者做控製表達式的類型稱為標量類型(Scalar Type),標量類型包括算術類型和以後要講的指針類型,詳見圖 23.5 “C語言類型總結”。
結構體變數之間使用賦值運算符是允許的,用一個結構體變數初始化另一個結構體變數也是允許的,例如:
struct complex_struct z1 = { 3.0, 4.0 }; struct complex_struct z2 = z1; z1 = z2;
同樣地,z2
必須是局部變數才能用變數z1
的值來初始化。既然結構體變數之間可以相互賦值和初始化,也就可以當作函數的參數和返回值來傳遞:
struct complex_struct add_complex(struct complex_struct z1, struct complex_struct z2) { z1.x = z1.x + z2.x; z1.y = z1.y + z2.y; return z1; }
這個函數實現了兩個複數相加,如果在main
函數中這樣調用:
struct complex_struct z = { 3.0, 4.0 }; z = add_complex(z, z);
那麼調用傳參的過程如下圖所示:
變數z
在main
函數的棧幀上,參數z1
和z2
在add_complex
函數的棧幀上,z
的值分別賦給z1
和z2
。在這個函數里,z2
的實部和虛部被累加到z1
中,然後return z1;
可以看成是:
用z1
初始化一個臨時變數。
函數返回並釋放棧幀。
把臨時變數的值賦給變數z
,釋放臨時變數。
由.運算符組成的表達式能不能做左值取決於.運算符左邊的表達式能不能做左值。在上面的例子中,z
是一個變數,可以做左值,因此表達式z.x
也可以做左值,但表達式add_complex(z, z).x
只能做右值而不能做左值,因為表達式add_complex(z, z)
不能做左值。
[12] 其實C99已經定義了複數類型_Complex
。如果包含C標準庫的標頭檔complex.h
,也可以用complex
做類型名。當然,只要不包含標頭檔complex.h
就可以自己定義標識符complex
,但為了儘量減少混淆,本章的示例代碼都用complex_struct
做標識符而不用complex
。
[13] 我們在第 4 節 “結構體和聯合體”會看到,結構體成員之間也可能有若干個填充位元組。
[14] C99引入一種新的表達式語法Compound Literal可以用來賦值,例如z1 = (struct complex_struct){ 3.0, 4.0 };
,本書不使用這種新語法。