[C 語言] 程式設計教學:如何實作類別(Class) 和物件(Object)
文章推薦指數: 80 %
真正的物件,要有狀態和行為間的連動。
狀態以資料的形式儲存在物件的屬性上,行為則是透過函式來實作。
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
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程式設計的內容:分享本文追蹤本站
延伸文章資訊
- 1[C 語言] 程式設計教學:如何實作類別(Class) 和物件(Object)
真正的物件,要有狀態和行為間的連動。狀態以資料的形式儲存在物件的屬性上,行為則是透過函式來實作。C 語言並沒有真正的物件,只能撰寫在精神上貼近物件的函式。
- 2C++物件導向及增進效率程式技巧
第一個部分是對基礎的C++物件導向程式作文獻的整理與說明,而物件導向是C ... 如果我們在定義一個class 成員的時候沒有宣告其允許範圍,這些成員將被默認.
- 3你所不知道的C 語言:物件導向程式設計篇 - HackMD
Saxophone)" } } # required to initialize the class Type::Initialize Human ... libmowgli (OO inter...
- 4C++的Class中的一些重點整理| Jason note
實際上,當我們定義一個class 而沒有定義建構子的時候,編譯器會自動假設兩個重載的建構子(預設建構子"default constructor" 和複製建構子"copy constructor"...
- 5【Day25】:從struct進化成class的物件導向技巧(上) - iT 邦幫忙
(c++中,如果類別內沒有加上存取權限,預設都是private)。 資料來源. 吳燦銘(2019)。C++程式設計與運算思維事務。新北市:博碩。