用 C 語言實現程序的多態性
前言:關于多態,關于 C
多態(polymorphism)一詞最初來源于希臘語 polumorphos,含義是具有多種形式或形態的情形。在程序設計領域,一個廣泛認可的定義是“一種將不同的特殊行為和單個泛化記號相關聯的能力”。然而在人們的直觀感覺中,多態的含義大約等同于“同一個方法對于不同類型的輸入參數均能做出正確的處理過程,并給出人們所期望獲得的結果”,也許這正體現了人們對于多態性所能達到的效果所寄予的期望:使程序能夠做到越來越智能化,越來越易于使用,越來越能夠使設計者透過形形色色的表象看到代碼所要觸及到的問題本質。作為讀者的你或許對于面向對象編程已有著精深的見解,或許對于多態的方便與神奇你也有了深入的認識。這時候你訝異的開始質疑了:“多態,那是面向對象編程才有的技術,C 語言是面向過程的啊!”而我想說的是,C 語言作為一種編程語言,也許并不是為了面向對象編程而設計,但這并不意味著它不能實現面向對象編程所能實現的功能,就比如說,多態性。在本文中我們使用一個簡單的單鏈表作為例子,展示 C 語言是如何體現多態性的。
結構體:不得不說的故事
許多從寫 C 代碼開始,逐漸走向 C++的程序員都知道,其實 C++里面的 class,其前身正是 C 語言中的 structure。很多基于 C 語言背景介紹 C++的書籍,在介紹到 class 這一章的時候都會向讀者清晰地展示,一個 C 語言里的 structure 是怎樣逐漸變成一個典型的 C++ class 的,甚至最后得出結論:“structure 就是一個所有成員都公有的類”,當然了,class 還是 class,不能簡單的把它看做一個復雜化了的 structure 而已。下面我們來看看在 C 語言中定義一個簡單的存儲整型數據的單鏈表節點是怎么做的,當然是用結構體。大部分人會像我一樣,在 linkList.h 文件里定義:

struct Node // 鏈表節點
{
int data; // 存儲的整型數據
linkList next; // 指向下一個鏈表節點
};
鏈表有了,下面就是你想要實現的一些鏈表的功能,當然是定義成函數。我們只舉幾個常用功能:

void insertFirst(linkList h,int data); // 在已有鏈表的表頭進行插入節點操作
void linkListOutput(linkList h); // 輸出鏈表中數據到控制臺
這些都是再自然不過的 C 語言的編程過程,然后我們就可以在 linkList.c 文件中實現上述兩個函數,繼而在 main.c 中調用它們了。然而上面我們定義的鏈表還只能對整型數據進行操作。如果下次你要用到一個存儲字符串類型的鏈表,就只好把上面的過程重新來過。也許你覺得這個在原有代碼基礎上做略微修改的過程并不復雜,可是也許我們會不斷的增加對于鏈表這個數據結構的操作,而需要用鏈表來存儲的數據類型也越來越多,這些都意味著海量的代碼和繁瑣的后期維護工作。當你有了上百個存儲不同數據類型的鏈表結構,每當你要增加一個操作,或者修改某個操作的傳入參數,工作量會變大到像一場災難。但是我們可以改造上述代碼,讓它能夠處理你所想要讓它處理的任何數據類型:實行,字符型,乃至任何你自己定義的 structure 類型。
void *:萬能的指針“掛鉤”
幾乎所有講授 C 語言課程的老師都會告訴你:“指針是整個 C 語言的精髓所在。”而你也一直敬畏著指針,又愛又恨地使用著它。許多教材都告訴你,int *叫做指向整型的指針,而 char *是指向字符型的指針,等等等等不一而足。然而這里有一個另類的指針家族成員——void *。不要按照通常的命名方式叫它做指向void 類型的指針,它的正式的名字叫做:可以指向任意類型的指針。你一定注意到了“任意類型”這四個字,沒錯,實現多態,我們靠的就是它。下面來改造我們的鏈表代碼,在 linkList.h 里:

struct Node // 鏈表節點
{
void *data; // 存儲的數據指針
linkList next; // 指向下一個鏈表節點
};
linkList initialLinklist(); // 初始化鏈表
void insertFirst(linkList h, void *data); // 在已有鏈表的表頭進行插入節點操作
void linkListOutput(linkList h); // 輸出鏈表中數據到控制臺
我們來看看現在這個鏈表和剛才那個只能存儲整型數據的鏈表的區別。當你把 Node 結構體里面的成員定義為一個整型數據,就好像把這個鏈表節點打造成了一個大小形狀固定的盒子,你定義一個鏈表節點,程序進行編譯的時候編譯器就為你打造一個這樣的盒子:裝一個 int 類型的數據,然后裝一個 linkList 類型的指針。如果你想強行在這個盒子里裝別的東西,編譯器會告訴你,對不起,盒子的大小形狀并不合適。所以你必須為了裝各種各樣類型的數據打造出不同的生產盒子的流水線,想要裝哪種類型數據的盒子,就開啟對應的流水線來生產。
但是當你把結構體成員定義為void *,一切都變得不同了。這時的鏈表節點不再像個大小形狀固定的盒子,而更像一個掛鉤,它可以掛上一個任意類型的數據。不管你需要存儲什么類型的數據,你只要傳遞一個指針,把它存儲到 Node 節點中去,就相當于把這個數據“掛”了上去,無論何時你都可以根據指針找到它。這時的鏈表仿佛變成了一排粘貼在墻上的衣帽鉤,你可以掛一排大衣,可以掛一排帽子,可以掛一排圍巾,甚至,你可以并排掛一件大衣一頂帽子一條圍巾在墻上。void *初露猙獰,多態離 C 語言并不遙遠。
函數指針:知道我能做什么
在結構體里添加函數指針,目的在于告訴每一個使用這個結構體的人,這個結構體在設計的時候是為了支持哪些操作。當我們的鏈表定義變成如下這樣的形式:

struct Node // 鏈表節點
{
void *data; // 存儲的數據指針
linkList next; // 指向下一個鏈表節點
void (*insertFirst)(linkList, void *); // 在已有鏈表的表頭進行插入節點操作的函數指針
void (*linkListOutput)(linkList); // 輸出鏈表中數據到控制臺操作的函數指針
};
也許你的心里也在暗暗覺得,是的,真的很像一個 class 了。當然,即使是加入了函數指針,也并不能讓這個結構體和一個 C++中的類等同起來。因為無論如何函數指針能夠調用到的函數也必須在結構體外定義,并且要保證在使用結構體的時候這些函數可以被調用。所以實際上這些函數并不是我們的鏈表專用的一些方法,只要在有效范圍內,使用了正確的參數,任何時候都可以調用這些函數。
觀察上面的鏈表定義中的兩個函數指針,其中用來插入節點的操作 insertFirst 對于任何類型的鏈表來說都是一樣的,但是對于輸出的操作 linkListOutput 則完全不是那么回事了。通常,對于不同類型的鏈表,它的輸出操作當然也不同,這就需要程序員在使用的時候自己清楚在什么情況下需要傳給鏈表結構體怎樣的輸出函數。在本例當中,我們在鏈表初始化的時候給它傳入固定的插入函數和一個默認情況下的輸出函數,這個輸出函數的作用是在程序員未指定任何輸出函數的時候提醒程序員,你還沒有指定一個與你的鏈表匹配的輸出函數。在 linkList.h 當中,我給出了 4 種對于基本類型的輸出函數,當然對于其它類型,也都可以指定相應的輸出函數。
實現:你的多態你做主
當你真正開始著手做這個工作的時候,你會發現把數據放入鏈表中的操作和普通的存放 int 類型的鏈表的實現并沒有什么大的區別,很方便。但是當你要把已經存進去的數據讀取出來的時候,就有一點麻煩了。對于void *類型的指針,編譯器只知道它里面存儲了一個地址,但是關于這個地址里的數據類型,編譯器是沒有任何概念的。畢竟我們不能指望編譯器什么都知道,什么都能替你做好,所以存進去的數據的類型,作為程序員的我們必須清楚的知道,并且在取出這個數據的時候,用這一類型的指針來對void *做強制類型轉換。為了方便的做到這一點,我采取的方法是在 Node 結構體中增加一個標識數據類型的域,并用一個枚舉類型來存放這些數據類型。這時的 linkList.h 如下所示:

#define LINKLIST_H
typedef struct Node* linkList;
struct Node // 鏈表節點
{
void *data; // 存儲的數據指針
linkList next; // 指向下一個鏈表節點
void (*insertFirst)(linkList, void *); // 在已有鏈表的表頭進行插入節點操作的函數指針
void (*linkListOutput)(linkList); // 輸出鏈表中數據到控制臺操作的函數指針
};
linkList initialLinklist(); // 初始化鏈表
void insertFirst(linkList h, void *data); // 在已有鏈表的表頭進行插入節點操作
void linkListOutput(linkList h); // 未指定輸出類型時的默認輸出函數
void stringLinkListOutput(linkList h); // 輸出字符串類型鏈表中數據到控制臺
void intLinkListOutput(linkList h); // 輸出整型鏈表中數據到控制臺
void doubleLinkListOutput(linkList h); // 輸出浮點型鏈表中數據到控制臺
void charLinkListOutput(linkList h); // 輸出字符型鏈表中數據到控制臺
#endif
初始化鏈表,代碼如下:

{
linkList link = (linkList*)malloc(sizeof(*link));
link->data = NULL;
link->next = NULL;
link->insertFirst = insertFirst;
link->linkListOutput = linkListOutput;
return link;
}
在已有鏈表的表頭進行插入節點操作,代碼如下:
{
linkList link = initialLinklist();
link->data = data;
link->next = h->next;
h->next = link;
}
輸出鏈表中數據到控制臺,代碼如下:

{
printf("You should point which output function is wanted!\n");
}
void stringLinkListOutput(linkList h)
{
linkList p = h->next;
while(p)
{
printf("%s\t", (char*)(p->data));
p = p->next;
}
printf("\n");
}
void intLinkListOutput(linkList h)
{
linkList p = h->next;
while(p)
{
printf("%d\t", *(int*)(p->data));
p = p->next;
}
printf("\n");
}
void doubleLinkListOutput(linkList h)
{
linkList p = h->next;
while(p)
{
printf("%f\t", *(double*)(p->data));
p = p->next;
}
printf("\n");
}
void charLinkListOutput(linkList h)
{
linkList p = h->next;
while(p)
{
printf("%c\t", *(char*)(p->data));
p = p->next;
}
printf("\n");
}
小結
通過上面這個鏈表的小例子,大家可能已經看到了 C 語言的靈活性。這段代碼雖然短并且功能簡單,但是已經實現了多態性。這篇文章的本意并不在于想要告訴大家用 C 實現多態的方法,而多態的含義也無疑是更加廣泛的。這篇文章的初衷其實是基于這樣一點認識:面向對象是一種程序設計思想,而 C 語言則是一種編程語言。也許它并不是專門為了面向對象編程而設計,但是這絕不意味著它不能實現面向對象的程序設計。當然以上所展示的這幾個操作,如果是用別的編程語言,可能只要寥寥幾行就可以完成,但是 C 語言想告訴我們的是:也許我不擅長,但是并不意味著我做不到。