## 數據的抽象
當你具備了一些 C 編程基礎,并且能夠理解上文中的內容,那么你就可以對各種類型的數據進行抽象了。
我們為什么要對數據進行抽象?《計算機程序的構造和解釋》的第 2 章的導言部分給出了很好的答案,即:許多程序在設計時就是為了模擬復雜的現象,因為它們就常常需要構造出一些運算對象,為了能夠模擬真實世界中的現象的各個方面,需要將運算對象表示為一些組件的復合結構。
下面來對自行車鏈的任意一個鏈節進行模擬:

~~~
struct chain_node {
struct chain_node *prev;
struct chain_node *next;
void *shape;
};
~~~
然后我們可以造出 3 個鏈節,然后可以造出世界上最短的車鏈:
~~~
struct chain_node a, b, c;
a.next = &b;
b.prev = &a;
b.next = &c;
c.prev = &b;
c.next = &a;
a.prev = &c;
~~~
如果再多造一些鏈節,就可以得到周長大一些的車鏈,也能夠制造出各種形狀的多邊形,但是最好是借助無名的內存空間。下面的代碼可以創建一條具有 1000 個鏈節的鏈條:
~~~
struct chain_node *head = malloc(sizeof(struct chain_node));
struct chain_node *tail = head;
for (int i = 0; i < 1000; i++) {
struct chain_node *new_tail = malloc(sizeof(struct chain_node));
tail->next = new_tail;
new_tail->prev = tail;
tail = new_tail;
}
tail->next = head;
head->prev = tail;
~~~
如果我們將前面那個示例中的?`a`,`b`,?`c`?視為三角形的三個頂點,那么我們所創造的三個鏈節構成的鏈條就變成了一個三角形。同理,上述所創建的 1000 個鏈節的鏈條就變成了一個 1000 條邊首尾相接的多邊形。如果學過拓撲學,那么自然可以發現任何與圓環同胚的結構都可以基于?`struct chain_node`?這種數據結構模擬出來,而我們所仰仗的東西僅僅是將三個指針封裝到一個結構體中。
事實上,`struct chain_node`?中的第三個指針?`void *shape`?還沒被用到。這是一個?`void *`?類型的指針,是喜歡用 C 代碼玩各種抽象的程序猿的最愛,因為它能引用任何類型數據所在內存空間的基地址。這就意味著?`struct chain_node`?可以借助?`shape`?指針獲得強大的擴展能力。
現在,我要制造一種很簡陋的鏈節,它的形狀僅僅是一個矩形的小鐵片,上面打了兩個小圓孔。我將它的數據結構設計為:
~~~
struct point {
double x;
double y;
};
struct rectangle {
double width;
double height;
};
struct circle {
struct point *center;
double radius;
};
struct chain_node_shape {
struct rectangle *body;
struct circle *holes[2] ;
};
~~~
基于這些數據結構,我就可以寫出一個專門用來制造矩形小鐵片的函數:
~~~
struct chain_node_shape *
create_chain_node_shape(struct circle *c1,
struct circle *c2,
struct rectangle *rect)
{
struct chain_node_shape *ret = malloc(sizeof(struct chain_node_shape));
ret->body = rect;
ret->holes[0] = c1;
ret->holes[1] = c2;
return ret;
}
~~~
然后再為?`create_chain_node_shape`?所接受的兩種參數寫出相應的構造函數:
~~~
struct circle *
create_circle(struct point *center, double radius)
{
struct circle *ret = malloc(sizeof(struct circle));
ret->center = center;
ret->radius = radius;
return ret;
}
struct rectangle *
create_rectangle(double w, double h)
{
struct rectangle *ret = malloc(sizeof(struct rectangle));
ret->width = w;
ret->height = h;
return ret;
}
~~~
為了讓?`create_circle`?更方便使用,最好再創建一個?`struct point`?的構造函數:
~~~
struct point *
create_point(double x, double y)
{
struct point *ret = malloc(sizeof(struct point));
ret->x = x;
ret->y = y;
return ret;
}
~~~
一切所需要的構件都已準備完畢,現在可以開始生產某種特定型號的鏈節了,即:
~~~
struct chain_node *
create_chain_node(void)
{
double radius = 0.5;
double left_x = 1.0;
double left_y = 1.0;
struct point *left_center = create_point(left_x, left_y);
struct circle *left_hole = create_circle(left_center, radius);
double right_x = 9.0;
double right_y = 1.0;
struct point *right_center = create_point(right_x, right_y);
struct circle *right_hole = create_circle(right_center, radius);
struct rectangle *body = create_rectangle(10.0, 2.0);
struct chain_node *ret = malloc(sizeof(struct chain_node));
ret->prev = NULL;
ret->next = NULL;
ret->shape = create_chain_node_shape(left_hole, right_hole, body);
return ret;
}
~~~
最后再將制造鏈條的代碼略作修改:
~~~
struct chain_node *head = create_chain_node();
struct chain_node *tail = head;
for (int i = 0; i < 1000; i++) {
struct chain_node *new_tail = create_chain_node();
tail->next = new_tail;
new_tail->prev = tail;
tail = new_tail;
}
tail->next = head;
head->prev = tail;
~~~
現在我們所模擬的車鏈與現實中的車鏈已經有些形似了。上述代碼雖然有些冗長,下文會對其進行重構,現在先來總結一下上述代碼中指針的用法。
仔細觀察上述代碼中我們所定義的結構體,它們的共同特征是:所有非 C 內建的數據類型都是結構體類型,當它們作為某個結構體成員類型時均被聲明為指針類型。為什么要這樣?如果你真的打算問這個問題,那么就請你觀察一下上述的 5 個`create_xxx`?函數,你會發現這些?`create`?函數的參數與返回值也都是結構體類型的指針。將這些現象綜合起來,可以得出以下結論:
1. 將結構體指針作為函數的參數與返回值,可以避免函數調用時發生過多的內存復制。
2. 當一個結構體類型作為其他結構體的成員類型時,將前者聲明為指針類型,可以在后者的?`create`?函數中避免繁瑣的解引用。
3. `void *`?指針可以引用任意類型的數據存儲空間的基地址。例如在?`create_chain_node`?函數的定義中,我們將一個`struct chain_node_shape`?類型的指針賦給了?`void *`?類型的指針?`shape`。
這三條結論是指針在數據抽象中的慣用手法,它不僅關系到數據結構的設計,也關系到數據結構的構造與銷毀函數的設計。(上述代碼為了省事,沒有定義數據結構的銷毀函數)