# 第?7?章?結構體
**目錄**
+ [1\. 復合類型與結構體](ch07s01.html)
+ [2\. 數據抽象](ch07s02.html)
+ [3\. 數據類型標志](ch07s03.html)
+ [4\. 嵌套結構體](ch07s04.html)
## 1.?復合類型與結構體
在編程語言中,最基本的、不可再分的數據類型稱為基本類型(Primitive Type),例如整型、浮點型;根據語法規則由基本類型組合而成的類型稱為復合類型(Compound Type),例如字符串是由很多字符組成的。有些場合下要把復合類型當作一個整體來用,而另外一些場合下需要分解組成這個復合類型的各種基本類型,復合類型的這種兩面性為數據抽象(Data Abstraction)奠定了基礎。[[SICP]](bi01.html#bibli.sicp "Structure and Interpretation of Computer Programs")指出,在學習一門編程語言時要特別注意以下三個方面:
1. 這門語言提供了哪些Primitive,比如基本類型,比如基本運算符、表達式和語句。
2. 這門語言提供了哪些組合規則,比如基本類型如何組成復合類型,比如簡單的表達式和語句如何組成復雜的表達式和語句。
3. 這門語言提供了哪些抽象機制,包括數據抽象和過程抽象(Procedure Abstraction)。
本章以結構體為例講解數據類型的組合和數據抽象。至于過程抽象,我們在[第?2?節 “if/else語句”](ch04s02.html#cond.ifelse)已經見過最簡單的形式,就是把一組語句用一個函數名封裝起來,當作一個整體使用,本章將介紹更復雜的過程抽象。
現在我們用C語言表示一個復數。從直角座標系來看,復數由實部和虛部組成,從極座標系來看,復數由模和輻角組成,兩種座標系可以相互轉換,如下圖所示:
**圖?7.1.?復數**

如果用實部和虛部表示一個復數,我們可以寫成由兩個`double`型組成的結構體:
```
struct complex_struct {
double x, y;
};
```
這一句定義了標識符`complex_struct`(同樣遵循標識符的命名規則),這種標識符在C語言中稱為Tag,`struct complex_struct { double x, y; }`整個可以看作一個類型名<sup>[[12](#ftn.id2730268)]</sup>,就像`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)來訪問,這兩個成員的存儲空間是相鄰的<sup>[[13](#ftn.id2730413)]</sup>,合在一起組成復數變量的存儲空間。看下面的例子:
**例?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?節 “變量的存儲布局”](ch19s03.html#asmc.layout)會講到這兩個標識符`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并不是表達式,所以不能用來賦值<sup>[[14](#ftn.id2730593)]</sup>。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語言類型總結”](ch23s09.html#pointer.typesummary)。
結構體變量之間使用賦值運算符是允許的,用一個結構體變量初始化另一個結構體變量也是允許的,例如:
```
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.?結構體傳參**

變量`z`在`main`函數的棧幀上,參數`z1`和`z2`在`add_complex`函數的棧幀上,`z`的值分別賦給`z1`和`z2`。在這個函數里,`z2`的實部和虛部被累加到`z1`中,然后`return z1;`可以看成是:
1. 用`z1`初始化一個臨時變量。
2. 函數返回并釋放棧幀。
3. 把臨時變量的值賦給變量`z`,釋放臨時變量。
由.運算符組成的表達式能不能做左值取決于.運算符左邊的表達式能不能做左值。在上面的例子中,`z`是一個變量,可以做左值,因此表達式`z.x`也可以做左值,但表達式`add_complex(z, z).x`只能做右值而不能做左值,因為表達式`add_complex(z, z)`不能做左值。
* * *
<sup>[[12](#id2730268)]</sup> 其實C99已經定義了復數類型`_Complex`。如果包含C標準庫的頭文件`complex.h`,也可以用`complex`做類型名。當然,只要不包含頭文件`complex.h`就可以自己定義標識符`complex`,但為了盡量減少混淆,本章的示例代碼都用`complex_struct`做標識符而不用`complex`。
<sup>[[13](#id2730413)]</sup> 我們在[第?4?節 “結構體和聯合體”](ch19s04.html#asmc.structunion)會看到,結構體成員之間也可能有若干個填充字節。
<sup>[[14](#id2730593)]</sup> C99引入一種新的表達式語法Compound Literal可以用來賦值,例如`z1 = (struct complex_struct){ 3.0, 4.0 };`,本書不使用這種新語法。
## 2.?數據抽象
現在我們來實現一個完整的復數運算程序。在上一節我們已經定義了復數的結構體類型,現在需要圍繞它定義一些函數。復數可以用直角座標或極座標表示,直角座標做加減法比較方便,極座標做乘除法比較方便。如果我們定義的復數結構體是直角座標的,那么應該提供極座標的轉換函數,以便在需要的時候可以方便地取它的模和輻角:
```
#include <math.h>
struct complex_struct {
double x, y;
};
double real_part(struct complex_struct z)
{
return z.x;
}
double img_part(struct complex_struct z)
{
return z.y;
}
double magnitude(struct complex_struct z)
{
return sqrt(z.x * z.x + z.y * z.y);
}
double angle(struct complex_struct z)
{
return atan2(z.y, z.x);
}
```
此外,我們還提供兩個函數用來構造復數變量,既可以提供直角座標也可以提供極座標,在函數中自動做相應的轉換然后返回構造的復數變量:
```
struct complex_struct make_from_real_img(double x, double y)
{
struct complex_struct z;
z.x = x;
z.y = y;
return z;
}
struct complex_struct make_from_mag_ang(double r, double A)
{
struct complex_struct z;
z.x = r * cos(A);
z.y = r * sin(A);
return z;
}
```
在此基礎上就可以實現復數的加減乘除運算了:
```
struct complex_struct add_complex(struct complex_struct z1, struct complex_struct z2)
{
return make_from_real_img(real_part(z1) + real_part(z2),
img_part(z1) + img_part(z2));
}
struct complex_struct sub_complex(struct complex_struct z1, struct complex_struct z2)
{
return make_from_real_img(real_part(z1) - real_part(z2),
img_part(z1) - img_part(z2));
}
struct complex_struct mul_complex(struct complex_struct z1, struct complex_struct z2)
{
return make_from_mag_ang(magnitude(z1) * magnitude(z2),
angle(z1) + angle(z2));
}
struct complex_struct div_complex(struct complex_struct z1, struct complex_struct z2)
{
return make_from_mag_ang(magnitude(z1) / magnitude(z2),
angle(z1) - angle(z2));
}
```
可以看出,復數加減乘除運算的實現并沒有直接訪問結構體`complex_struct`的成員`x`和`y`,而是把它看成一個整體,通過調用相關函數來取它的直角座標和極座標。這樣就可以非常方便地替換掉結構體`complex_struct`的存儲表示,例如改為用極座標來存儲:
```
#include <math.h>
struct complex_struct {
double r, A;
};
double real_part(struct complex_struct z)
{
return z.r * cos(z.A);
}
double img_part(struct complex_struct z)
{
return z.r * sin(z.A);
}
double magnitude(struct complex_struct z)
{
return z.r;
}
double angle(struct complex_struct z)
{
return z.A;
}
struct complex_struct make_from_real_img(double x, double y)
{
struct complex_struct z;
z.A = atan2(y, x);
z.r = sqrt(x * x + y * y);
}
struct complex_struct make_from_mag_ang(double r, double A)
{
struct complex_struct z;
z.r = r;
z.A = A;
return z;
}
```
雖然結構體`complex_struct`的存儲表示做了這樣的改動,`add_complex`、`sub_complex`、`mul_complex`、`div_complex`這幾個復數運算的函數卻不需要做任何改動,仍然可以用,原因在于這幾個函數只把結構體`complex_struct`當作一個整體來使用,而沒有直接訪問它的成員,因此也不依賴于它有哪些成員。我們結合下圖具體分析一下。
**圖?7.3.?數據抽象**

這里是一種抽象的思想。其實“抽象”這個概念并沒有那么抽象,簡單地說就是“提取公因式”:ab+ac=a(b+c)。如果a變了,ab和ac這兩項都需要改,但如果寫成a(b+c)的形式就只需要改其中一個因子。
在我們的復數運算程序中,復數有可能用直角座標或極座標來表示,我們把這個有可能變動的因素提取出來組成復數存儲表示層:`real_part`、`img_part`、`magnitude`、`angle`、`make_from_real_img`、`make_from_mag_ang`。這一層看到的數據是結構體的兩個成員`x`和`y`,或者`r`和`A`,如果改變了結構體的實現就要改變這一層函數的實現,但函數接口不改變,因此調用這一層函數接口的復數運算層也不需要改變。復數運算層看到的數據只是一個抽象的“復數”的概念,知道它有直角座標和極座標,可以調用復數存儲表示層的函數得到這些座標。再往上看,其它使用復數運算的程序看到的數據是一個更為抽象的“復數”的概念,只知道它是一個數,像整數、小數一樣可以加減乘除,甚至連它有直角座標和極座標也不需要知道。
這里的復數存儲表示層和復數運算層稱為抽象層(Abstraction Layer),從底層往上層來看,復數越來越抽象了,把所有這些層組合在一起就是一個完整的系統。_組合使得系統可以任意復雜,而抽象使得系統的復雜性是可以控制的,任何改動都只局限在某一層,而不會波及整個系統_。著名的計算機科學家Butler Lampson說過:“All problems in computer science can be solved by another level of indirection.”這里的indirection其實就是abstraction的意思。
### 習題
1、在本節的基礎上實現一個打印復數的函數,打印的格式是x+yi,如果實部或虛部為0則省略,例如:1.0、-2.0i、-1.0+2.0i、1.0-2.0i。最后編寫一個`main`函數測試本節的所有代碼。想一想這個打印函數應該屬于上圖中的哪一層?
2、實現一個用分子分母的格式來表示有理數的結構體`rational`以及相關的函數,`rational`結構體之間可以做加減乘除運算,運算的結果仍然是`rational`。測試代碼如下:
```
int main(void)
{
struct rational a = make_rational(1, 8); /* a=1/8 */
struct rational b = make_rational(-1, 8); /* b=-1/8 */
print_rational(add_rational(a, b));
print_rational(sub_rational(a, b));
print_rational(mul_rational(a, b));
print_rational(div_rational(a, b));
return 0;
}
```
注意要約分為最簡分數,例如1/8和-1/8相減的打印結果應該是1/4而不是2/8,可以利用[第?3?節 “遞歸”](ch05s03.html#func2.recursion)練習題中的Euclid算法來約分。在動手編程之前先思考一下這個問題實現了什么樣的數據抽象,抽象層應該由哪些函數組成。
## 3.?數據類型標志
在上一節中,我們通過一個復數存儲表示抽象層把`complex_struct`結構體的存儲格式和上層的復數運算函數隔開,`complex_struct`結構體既可以采用直角座標也可以采用極座標存儲。但有時候需要同時支持兩種存儲格式,比如先前已經采集了一些數據存在計算機中,有些數據是以極座標存儲的,有些數據是以直角座標存儲的,如果要把這些數據都存到`complex_struct`結構體中怎么辦?一種辦法是規定`complex_struct`結構體采用直角座標格式,直角座標的數據可以直接存入`complex_struct`結構體,而極座標的數據先轉成直角座標再存,但由于浮點數的精度有限,轉換總是會損失精度的。這里介紹另一種辦法,`complex_struct`結構體由一個數據類型標志和兩個浮點數組成,如果數據類型標志為0,那么兩個浮點數就表示直角座標,如果數據類型標志為1,那么兩個浮點數就表示極座標。這樣,直角座標和極座標的數據都可以適配(Adapt)到`complex_struct`結構體中,無需轉換和損失精度:
```
enum coordinate_type { RECTANGULAR, POLAR };
struct complex_struct {
enum coordinate_type t;
double a, b;
};
```
`enum`關鍵字的作用和`struct`關鍵字類似,把`coordinate_type`這個標識符定義為一個Tag,`struct complex_struct`表示一個結構體類型,而`enum coordinate_type`表示一個枚舉(Enumeration)類型。枚舉類型的成員是常量,它們的值由編譯器自動分配,例如定義了上面的枚舉類型之后,`RECTANGULAR`就表示常量0,`POLAR`表示常量1。如果不希望從0開始分配,可以這樣定義:
```
enum coordinate_type { RECTANGULAR = 1, POLAR };
```
這樣,`RECTANGULAR`就表示常量1,而`POLAR`表示常量2。枚舉常量也是一種整型,其值在編譯時確定,因此也可以出現在常量表達式中,可以用于初始化全局變量或者作為`case`分支的判斷條件。
有一點需要注意,雖然結構體的成員名和變量名不在同一命名空間中,但枚舉的成員名卻和變量名在同一命名空間中,所以會出現命名沖突。例如這樣是不合法的:
```
int main(void)
{
enum coordinate_type { RECTANGULAR = 1, POLAR };
int RECTANGULAR;
printf("%d %d\n", RECTANGULAR, POLAR);
return 0;
}
```
`complex_struct`結構體的格式變了,就需要修改復數存儲表示層的函數,但只要保持函數接口不變就不會影響到上層函數。例如:
```
struct complex_struct make_from_real_img(double x, double y)
{
struct complex_struct z;
z.t = RECTANGULAR;
z.a = x;
z.b = y;
return z;
}
struct complex_struct make_from_mag_ang(double r, double A)
{
struct complex_struct z;
z.t = POLAR;
z.a = r;
z.b = A;
return z;
}
```
### 習題
1、本節只給出了`make_from_real_img`和`make_from_mag_ang`函數的實現,請讀者自己實現`real_part`、`img_part`、`magnitude`、`angle`這些函數。
2、編譯運行下面這段程序:
```
#include <stdio.h>
enum coordinate_type { RECTANGULAR = 1, POLAR };
int main(void)
{
int RECTANGULAR;
printf("%d %d\n", RECTANGULAR, POLAR);
return 0;
}
```
結果是什么?并解釋一下為什么是這樣的結果。
## 4.?嵌套結構體
結構體也是一種遞歸定義:結構體的成員具有某種數據類型,而結構體本身也是一種數據類型。換句話說,結構體的成員可以是另一個結構體,即結構體可以嵌套定義。例如我們在復數的基礎上定義復平面上的線段:
```
struct segment {
struct complex_struct start;
struct complex_struct end;
};
```
從[第?1?節 “復合類型與結構體”](ch07s01.html#struct.intro)講的Initializer的語法可以看出,Initializer也可以嵌套,因此嵌套結構體可以嵌套地初始化,例如:
```
struct segment s = {{ 1.0, 2.0 }, { 4.0, 6.0 }};
```
也可以平坦(Flat)地初始化。例如:
```
struct segment s = { 1.0, 2.0, 4.0, 6.0 };
```
甚至可以把兩種方式混合使用(這樣可讀性很差,應該避免):
```
struct segment s = {{ 1.0, 2.0 }, 4.0, 6.0 };
```
利用C99的新特性也可以做Memberwise Initialization,例如<sup>[[15](#ftn.id2731613)]</sup>:
```
struct segment s = { .start.x = 1.0, .end.x = 2.0 };
```
訪問嵌套結構體的成員要用到多個.運算符,例如:
```
s.start.t = RECTANGULAR;
s.start.a = 1.0;
s.start.b = 2.0;
```
* * *
<sup>[[15](#id2731613)]</sup> 為了便于理解,[第?1?節 “復合類型與結構體”](ch07s01.html#struct.intro)講的Initializer語法并沒有描述這種復雜的用法。
- Linux C編程一站式學習
- 歷史
- 前言
- 部分?I.?C語言入門
- 第?1?章?程序的基本概念
- 第?2?章?常量、變量和表達式
- 第?3?章?簡單函數
- 第?4?章?分支語句
- 第?5?章?深入理解函數
- 第?6?章?循環語句
- 第?7?章?結構體
- 第?8?章?數組
- 第?9?章?編碼風格
- 第?10?章?gdb
- 第?11?章?排序與查找
- 第?12?章?棧與隊列
- 第?13?章?本階段總結
- 部分?II.?C語言本質
- 第?14?章?計算機中數的表示
- 第?15?章?數據類型詳解
- 第?16?章?運算符詳解
- 第?17?章?計算機體系結構基礎
- 第?18?章?x86匯編程序基礎
- 第?19?章?匯編與C之間的關系
- 第?20?章?鏈接詳解
- 第?21?章?預處理
- 第?22?章?Makefile基礎
- 第?23?章?指針
- 第?24?章?函數接口
- 第?25?章?C標準庫
- 第?26?章?鏈表、二叉樹和哈希表
- 第?27?章?本階段總結
- 部分?III.?Linux系統編程
- 第?28?章?文件與I/O
- 第?29?章?文件系統
- 第?30?章?進程
- 第?31?章?Shell腳本
- 第?32?章?正則表達式
- 第?33?章?信號
- 第?34?章?終端、作業控制與守護進程
- 第?35?章?線程
- 第?36?章?TCP/IP協議基礎
- 第?37?章?socket編程
- 附錄?A.?字符編碼
- 附錄?B.?GNU Free Documentation License Version 1.3, 3 November 2008
- 參考書目
- 索引