[C 語言] 程式設計教學:如何實作類別(Class) 和物件(Object)

文章推薦指數: 80 %
投票人數:10人

真正的物件,要有狀態和行為間的連動。

狀態以資料的形式儲存在物件的屬性上,行為則是透過函式來實作。

C 語言並沒有真正的物件,只能撰寫在精神上貼近物件的函式。

Togglenavigation開源教學精選項目C語言Golang資料結構網頁程式電子書籍現代C語言程式設計C語言應用程式設計多平台Objective-C程式設計跨平台CommonLisp程式設計社群媒體臉書粉絲團臉書社團推特GitHubGumroad本站資訊關於著作權免責聲明隱私權開源教學C應用程式設計如何實作類別(Class)和物件(Object)最後修改日期為JUL9,2020前言真正的物件(object),要有狀態(state)和行為(behavior)間的連動。

狀態以資料(data)的形式儲存在物件的屬性(field)上,行為則是透過函式(function)來實作。

和物件連動的函式,又稱為方法(method)。

C語言並沒有真正的物件,只能撰寫在精神上貼近物件的函式。

在本文中,我們會以平面座標中的點(point)為例,展示兩種物件的寫法。

典型的寫法在本節中,我們展示第一種以C語言撰寫物件的方式,這算是主流的手法,而且實作上比較簡單。

我們先來看外部程式如何使用point_t*物件。

假定point_t*類別已經實作出來,在外部程式中引入point.h標頭檔。

參考以下範例程式碼:#include/*1*/ #include"point.h"/*2*/ intmain(void)/*3*/ {/*4*/ point_t*a=point_new(0,0);/*5*/ if(!a){/*6*/ perror("Failedtoallocatea\n");/*7*/ gotoERROR;/*8*/ }/*9*/ point_t*b=point_new(3,4);/*10*/ if(!b){/*11*/ perror("Failedtoallocateb\n");/*12*/ gotoERROR;/*13*/ }/*14*/ if(!(point_distance(a,b)==5.0)){/*15*/ perror("Wrongdistance\n");/*16*/ gotoERROR;/*17*/ }/*18*/ point_delete((void*)b);/*19*/ point_delete((void*)a);/*20*/ return0;/*21*/ ERROR:/*22*/ if(b)/*23*/ point_delete((void*)b);/*24*/ if(a)/*25*/ point_delete((void*)a);/*26*/ return1;/*27*/ }/*28*/ 我們在第5行及第10行分別建立型態為point_t*物件a和物件b。

point_new()函式在內部使用到malloc()函式動態配置記憶體,而malloc()是有可能失敗的動作,所以我們要檢查物件是否成功建立。

我們在第6行至第9行檢查物件a是否成功建立。

若a未成功建立,放棄一般的程式流程,直接跳到第22行後的錯誤處理流程。

同樣地,我們在第11行至第14行間檢查物件b是否成功建立,再來決定後續的程式流程。

接著,我們在第15行將物件a和物件b傳入point_distance()函式,求兩點間的距離。

我們在第15行至第18行間以if敘述確認兩點間的距離是正確的。

最後,我們分別在第19行及第20行將物件a和物件b所占用的記憶體釋放掉。

並在第21行回傳程式正常結束的離開狀態值0。

如果程式在某段過程出錯了,我們會把程式跳到第22行,即ERROR標籤所在的位置,走錯誤處理的流程。

我們同樣會先釋放物件的記憶體,但我們不確定物件是否已建立,故使用if敘述檢查物件是否存在。

最後,我們改在第27行回傳非零數值1,代表程式發生錯誤。

我們來看point.h標頭檔的宣告:#pragmaonce/*1*/ typedefstructpoint_tpoint_t;/*2*/ structpoint_t{/*3*/ doublex;/*4*/ doubley;/*5*/ };/*6*/ point_t*point_new(doublex,doubley);/*7*/ voidpoint_delete(void*self);/*8*/ doublepoint_x(point_t*self);/*9*/ doublepoint_y(point_t*self);/*10*/ voidpoint_set_x(point_t*self,doublex);/*11*/ voidpoint_set_y(point_t*self,doubley);/*12*/ doublepoint_distance(point_t*a,point_t*b);/*13*/ 在第1行時,我們使用#pragmaonce防止重覆引入標頭檔。

雖然#pragmaonce不是標準C語法,很多C編譯器都有實作這項功能,可用來取代傳統的#includeguard。

我們在第2行利用typedef宣告結構體point_t的別名。

由於結構體名稱和別名可以用相同的名字,建議用這種方式來宣告,看起來比較簡潔。

接著,在第3行至第6行宣告結構體point_t內部的欄位。

我們按照數學上的習慣,用x和y來命名這兩個欄位。

接著在第7行至第13行的部分是數個函式宣告。

這些函式宣告都相當簡短,讀者可試著自己閱讀。

注意我們用point_前綴來模擬命名空間,以避免函式命名衝突。

我們分段來看point_t*物件的實作。

先看建構函式的部分:point_t*point_new(doublex,doubley)/*1*/ {/*2*/ point_t*pt=(point_t*)malloc(sizeof(point_t));/*3*/ if(!pt)/*4*/ returnpt;/*5*/ point_set_x(pt,x);/*6*/ point_set_y(pt,y);/*7*/ returnpt;/*8*/ }/*9*/ C語言沒有真正的建構子,使用一般函式充當建構函式即可。

我們常用new、create、ctor等字眼來表達該函式是建構函式,像是本範例的point_new()。

我們在第3行為物件pt配置記憶體。

由於malloc()是有可能失敗的動作,所以要考慮失敗處理。

在建構函式中,配置記憶體失敗時會回傳空指標,而配置成功時會回傳物件。

外部程式可藉由判斷物件是否為空來確認物件是否成功地建立。

在第6行及第7行中,我們刻意使用x和y的setter函式來修改欄位,而不直接對x和y賦值,因為我們要確保物件的一致性。

當我們更動setter函式的行為時,在建構函式中也可以獲得一致的行為。

再來看解構函式的部分:voidpoint_delete(void*self) { if(!self) return; free(self); } 同樣地,C語言沒有真正的解構子,使用一般函式充當解構函式即可。

我們常用delete、free、dtor等字眼來表達該函式是解構函式,像是本範例的point_delete()。

point_t類別的getter和setter都相當簡單:doublepoint_x(point_t*self) { assert(self); returnself->x; } doublepoint_y(point_t*self) { assert(self); returnself->y; } voidpoint_set_x(point_t*self,doublex) { assert(self); self->x=x; } voidpoint_set_y(point_t*self,doubley) { assert(self); self->y=y; } 相信讀者可以很輕易地理解這段程式碼。

最後來看計算距離的函式:doublepoint_distance(point_t*a,point_t*b) { assert(a); assert(b); doubledx=point_x(a)-point_x(b); doubledy=point_y(a)-point_y(b); returnsqrt(pow(dx,2)+pow(dy,2)); } 按照數學上的定義來計算即可,應該相當容易。

由本節的範例可看出,C語言的擬物件和函式之間沒有真正的連動,只是利用刻意安排,寫出具有物件感的C程式碼。

替代的寫法在本節中,我們展示另一種實作物件的方式。

這個方式稍嫌麻煩,但寫起來更有物件的精神。

我們先看外部程式如何使用point_t*物件。

同樣地,我們假定point_t*類別已經實作出來,引入其標頭檔point.h。

參考範例程式如下:#include"point.h"/*1*/ intmain(void)/*2*/ {/*3*/ point_class_t*cls=point_class_new();/*4*/ if(!cls){/*5*/ perror("Failedtoallocatecls\n");/*6*/ gotoERROR;/*7*/ }/*8*/ point_t*a=cls->new(0,0);/*9*/ if(!a){/*10*/ perror("Failedtoallocatea\n");/*11*/ gotoERROR;/*12*/ }/*13*/ point_t*b=cls->new(3,4);/*14*/ if(!b){/*15*/ perror("Failedtoallocateb\n");/*16*/ gotoERROR;/*17*/ }/*18*/ if(!(cls->distance(a,b)==5.0)){/*19*/ perror("Wrongdistance\n");/*20*/ gotoERROR;/*21*/ }/*22*/ cls->delete((void*)b);/*23*/ cls->delete((void*)a);/*24*/ point_class_delete((void*)cls);/*25*/ return0;/*26*/ ERROR:/*27*/ if(b)/*28*/ cls->delete((void*)b);/*29*/ if(a)/*30*/ cls->delete((void*)a);/*31*/ if(cls)/*32*/ point_class_delete((void*)cls);/*33*/ return1;/*34*/ }/*35*/ 一開始,我們不急著建立point_t*物件,而是額外建立point_class_t*物件,該物件用來代表point_t*的類別,這段動作的程式位於第4行。

由於建立cls物件的建構函式內部有用到malloc(),需檢查該物件是否成功地建立。

接著,我們用建好的cls物件為類別,在第9行及第14行分別建立point_t*物件a和物件b。

同樣地,需分別檢查兩物件是否成功地建立。

我們在第19行將物件a和物件b傳入cls的方法distance()以求得兩點間的距離。

這裡用簡單的if敘述檢查兩點間的距離是否有錯。

最後,在第23行至第25行將物件所占的記憶體逐一釋放掉。

在第26行回傳0代表整個程式正確地運行。

如果程式在運行中發生問題,會跳到第27行,即ERROR標籤所在的位置,走錯誤處理的流程。

在這裡,確認物件存在後,同樣會逐一釋放物件的記憶體。

但在第34行回傳非零值1,表示程式運行中發生錯誤。

我們接著來看point.h的宣告:#pragmaonce typedefstructpoint_class_tpoint_class_t; typedefstructpoint_tpoint_t; structpoint_class_t{ point_t*(*new)(doublex,doubley); void(*delete)(void*self); double(*x)(point_t*self); double(*y)(point_t*self); void(*set_x)(point_t*self,doublex); void(*set_y)(point_t*self,doubley); double(*distance)(point_t*a,point_t*b); }; point_class_t*point_class_new(); voidpoint_class_delete(void*cls); structpoint_t{ doublex; doubley; }; point_t*_point_new(doublex,doubley); void_point_delete(void*self); double_point_x(point_t*self); double_point_y(point_t*self); void_point_set_x(point_t*self,doublex); void_point_set_y(point_t*self,doubley); double_point_distance(point_t*a,point_t*b); 在標頭檔中,我們宣告兩個結構體,分別是代表類別的point_class_t和代表物件的point_t。

在point_class_t類別中,我們利用函式指標宣告數個該類別的方法(method)。

但在point_t中,我們仍然要宣告相對應的函式,point_class_t物件才能指向各個方法的實作。

我們來看point_class_t類別的實作:point_class_t*point_class_new() { point_class_t*cls=\ (point_class_t*)malloc(sizeof(point_class_t)); if(!cls) returncls; cls->new=_point_new; cls->delete=_point_delete; cls->x=_point_x; cls->y=_point_y; cls->set_x=_point_set_x; cls->set_y=_point_set_y; cls->distance=_point_distance; returncls; } 在這個建構函式中,cls本身是物件,但在外部程式中當成類別來使用。

由這段程式碼可看出,cls物件本身不負責實作,其方法會另外指向各個實作函式。

雖然cls物件在外部程式中當成類別來用,cls物件同樣需要自己的解構函式:voidpoint_class_delete(void*cls) { if(!cls) return; free(cls); } 至於各個實作函式的細節和前例相同,故不重覆展示。

結語在本文中,我們展示兩種撰寫類別和物件的方法。

由於物件導向程式不是標準C的一部分,兩種寫法都可行。

讀者可以參考本文所列的方法,自己實作C語言的物件。

繼續深入如果你覺得這篇中階C程式設計的文章對你有幫助,可以看看「C語言應用程式設計」電子書。

這本書有更多關於中階C程式設計的內容:分享本文追蹤本站



請為這篇文章評分?