1. 復合類型與結構體

在編程語言中,最基本的、不可再分的數據類型稱為基本類型(Primitive Type),例如整型、浮點型;根據語法規則由基本類型組合而成的類型稱為復合類型(Compound Type),例如字元串是由很多字元組成的。有些場合下要把復合類型當作一個整體來用,而另外一些場合下需要分解組成這個復合類型的各種基本類型,復合類型的這種兩面性為數據抽象(Data Abstraction)奠定了基礎。[SICP]指出,在學習一門編程語言時要特別注意以下三個方面:

  1. 這門語言提供了哪些Primitive,比如基本類型,比如基本運算符、表達式和語句。

  2. 這門語言提供了哪些組合規則,比如基本類型如何組成復合類型,比如簡單的表達式和語句如何組成複雜的表達式和語句。

  3. 這門語言提供了哪些抽象機制,包括數據抽象和過程抽象(Procedure Abstraction)

本章以結構體為例講解數據類型的組合和數據抽象。至于過程抽象,我們在第 2 節 “if/else語句”已經見過最簡單的形式,就是把一組語句用一個函數名封裝起來,當作一個整體使用,本章將介紹更複雜的過程抽象。

現在我們用C語言表示一個複數。從直角座標系來看,複數由實部和虛部組成,從極座標系來看,複數由模和輻角組成,兩種座標系可以相互轉換,如下圖所示:

圖 7.1. 複數

複數

如果用實部和虛部表示一個複數,我們可以寫成由兩個double型組成的結構體:

struct complex_struct {
	double x, y;
};

這一句定義了標識符complex_struct(同樣遵循標識符的命名規則),這種標識符在C語言中稱為Tagstruct complex_struct { double x, y; }整個可以看作一個類型名[12],就像intdouble一樣,只不過它是一個復合類型,如果用這個類型名來定義變數,可以這樣寫:

struct complex_struct {
	double x, y;
} z1, z2;

這樣z1z2就是兩個變數名,變數定義後面帶個;號是我們早就習慣的。但即使像先前的例子那樣只定義了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);

那麼調用傳參的過程如下圖所示:

圖 7.2. 結構體傳參

結構體傳參

變數zmain函數的棧幀上,參數z1z2add_complex函數的棧幀上,z的值分別賦給z1z2。在這個函數里,z2的實部和虛部被累加到z1中,然後return z1;可以看成是:

  1. z1初始化一個臨時變數。

  2. 函數返回並釋放棧幀。

  3. 把臨時變數的值賦給變數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 };,本書不使用這種新語法。