## 對象的回歸
先擺出?`create_chain_node`?函數:
~~~
struct chain_node *
create_chain_node(void)
{
struct pair *left_hole = pair(pair_for_double_type(1.0, 1.0), malloc_double(0.5));
struct pair *right_hole = pair(pair_for_double_type(9.0, 1.0), malloc_double(0.5));
struct pair *holes = pair(left_hole, right_hole);
struct pair *body = pair_for_double_type(10.0, 1.0);
struct pair *shape = pair(body, holes);
struct chain_node *ret = malloc(sizeof(struct chain_node));
ret->prev = NULL;
ret->next = NULL;
ret->shape = shape;
return ret;
}
~~~
`create_chain_node`?函數可以創建鏈節,它是借助很抽象的?`pair`?結構體將很多種類型的數據層層封裝到了?`chain+node`結構體中,那么我們如何從?`chain_node`?結構體中提取這些數據,并使之重現它們所模擬的現實事物?
例如,我們怎樣從?`chain_node`?結構體中獲取一個?`left_hole`?的信息?顯然,下面的代碼
~~~
struct *t = create_chain_node();
struct pair *shape = t->shape;
struct pair *holes = shape->second;
struct pair *left_hole = holes->first;
~~~
并不能解決我們的問題,因為?`left_hole`?中只是兩個?`void *`?指針,而我們需要知道的是?`left_hole`?的中心與半徑。那么我們繼續:
~~~
struct pair *center = left_hole->first;
double radius = *((double *)(left_hole->second));
~~~
依然沒有解決我們的問題,因為我們想要的是?`left_hole`?的中心,而不是一個包含著兩個?`void *`?指針的?`center`,所以需要繼續:
~~~
double center_x = *((double *)(center->first));
double center_y = *((double *)(center->second));
~~~
最后我們得到了三個?`double`?類型的數據,即?`center_x`,?`center_y`,?`radius`,于是似乎我們的任務完成了,但是你如何將上述過程寫成一個函數?`get_left_hole`? C 語言中的函數只能有一個返回值。如果通過函數的參數來返回一些值,那么`get_left_hole`?是能寫出來的,例如:
~~~
void get_left_hole(struct chain_node *t, double *x, double *y, double *r)
{
struct pair *shape = t->shape;
struct pair *holes = shape->second;
struct pair *left_hole = holes->first;
struct pair *center = left_hole->first;
*x = *((double *)(center->first));
*y = *((double *)(center->second));
*r = *((double *)(left_hole->second));
}
~~~
但是,如果你真的這么寫了,那只能說明再好的編程語言也無法挽救你的品味。
我們應該繼續挖掘指針的功能,像下面這樣定義?`get_left_hole`會更好一些:
~~~
struct point {
double *x;
double *y;
};
struct hole {
struct point *center;
double *radius;
};
struct hole *
get_left_hole(struct chain_node *t)
{
struct pair *shape = t->shape;
struct pair *holes = shape->second;
return holes->first;
}
~~~
好在哪?我們充分利用了 C 編譯器對數據類型的隱式轉換,這實際上就是 C 編譯器的一種編譯期計算。這樣做可以避免在代碼中出現?`*((double *)(...))`?這樣的代碼。`void *`?指針總是能通過賦值語句自動轉換為左值的,前提是你需要保證左值的類型就是?`void *`?的原有類型。這是 C 語言的一條清規戒律,不能遵守這條戒律的程序猿,也許再好的編程語言也無法挽救他。
C++ 這個叛徒,所以無論它有多么強大,也無法拯救那些無法保證左值的類型就是?`void *`?原有類型的程序猿。用 C++ 編譯器迫使程序猿必須將
~~~
struct pair *shape = t->shape;
struct pair *holes = shape->second;
~~~
寫成:
~~~
struct pair *shape = (struct pair *)(t->shape);
struct pair *holes = (struct pair *)(shape->second);
~~~
否則代碼就無法通過編譯。這樣做,除了讓代碼更加混亂之外,依然無法挽救那些無法保證左值的類型就是?`void *`?原有類型的程序猿,只會讓他們對裸指針以及類型轉換這些事非常畏懼,逐漸就走上了惟類型安全的形而上學的道路。C++ 11 帶來了新的智能指針以及右值引用,希望他們能得到這些新 C++ 式的拯救吧。
當我們用面向對象的思路實現了?`get_left_hole`?之后,就可以像下面這樣使用它:
~~~
struct *t = create_chain_node();
struct hole *left_hole = get_left_hole(t);
printf("%lf, %lf, %lf\n", *(left_hole->center->x), *(left_hole->center->y), *(left_hole->radius));
~~~
一切都建立在指針上了,只是在最后要輸出數據的需用?`*`?對指針進行解引用。
上述代碼中有個特點,`left_hole`?并不占用內存,它僅僅是對?`t`?所引用的內存空間的再度引用。可能有人會擔心`left_hole`?具有直接訪問?`t`?所引用的內存空間的能力是非常危險的……有什么危險呢?你只需要清楚?`left_hole`?只是對其他空間的引用,而這一點自從你用了指針之后就已經建立了這樣的直覺了,你想修改?`left_hole`?所引用的內存空間中的數據,就可以 do it,不想修改就不去 do it,這有何難?如果自己并不打算去修改?`left_hole`?所引用的內存空間中的數據,但是又擔心自己或他人會因為失誤而修改了這些數據……你應該將這些擔心寫到?`get_left_hole`?的注釋里!
對于只需要稍加注意就可以很大程度上避免掉的事,非要從編程語言的語法層面來避免,這真的是小題大作了。如果我們在編程中對于?`void *`?指針的隱式類型正確轉換率高達 99%,為何要為 1% 的失誤而修改編程語言,使之充滿各種巧妙迂回的技巧并使得代碼愈加晦澀難懂呢?
《C 陷阱與缺陷》的作者給出了一個很好的比喻,在烹飪時,你用菜刀的時候是否失手切傷過自己的手?怎樣改進菜刀讓它在使用中更安全?你是否愿意使用這樣一把經過改良的菜刀?作者給出的答案是:我們很容易想到辦法讓一個工具更安全,代價是原來簡單的工具現在要變得復雜一些。食品加工機一般有連鎖裝置,可以保護使用者的手指不會受傷。然而菜刀卻不同,如果給菜刀這種簡單、靈活的工具安裝可以保護手指的裝置,只能讓它失去簡單性與靈活性。實際上,這樣做得到的結果也許是一臺食品加工機,而不再是一把菜刀。
我成功的將本節的題目歪到了指針上。現在再歪回來,我們來談談對象。其實已經沒什么好談的了,`get_left_hole`?返回的是泛型指針的類型具化,借助這種類型具化的指針我們可以有效避免對?`pair`?中的?`void *`?指針進行類型轉換的繁瑣過程。