嚴格說起來C語言不是物件導向的語言,一般所認知C就不能寫物件導向的程式嗎?[C 語言] 程式設計教學:物件導向程式入門有提到,「C程式設計藝術」這本書前半部都是介紹C,開始介紹C++後才引入物件導向的概念,讓初學者覺得:只有C++以後的語言才能用物件導向,C是沒有辦法的,很不幸的,我剛開始學C語言就是用這本螞蟻書。
看看下面一段程式碼:
#include <stdio.h> #include <conio.h> main() { FILE *fp; char ch; fp = fopen("hello.txt", "w"); printf("Enter data"); while( (ch = getchar()) != EOF) { putc(ch, fp); } fclose(fp); }
對初學者來說,fp是什麼?一個指標。為什麼是一個指標?fopen返回一個fp指標,看來是一個規定,我就死背。有open也有close,嗯,是對稱的,為什麼?我也背起來。putc代入的引數也有fp。這個fp好特別,為什麼可以有這樣的騷操作?
如果有學過C++語言,C++的初學都知道會有一個建構子,也會有一個解構子,也就是物件需要建立,結束也需要銷毀,中間可以對物件做不同的運算。上面的fp是一個物件嗎?fopen回傳一個handle,然後putc對handle操作運算,結束後用fclose對handle做關閉。若把handle看成是一個物件,fopen建立並回傳物件,putc對物件操作,fclose則把物件銷毀。這一切似乎就說得通。只是差在C++我不用拿這個handle,而C語言需要額外帶入這個handle值。
handle中文翻成控制碼,好神奇,是裡面有系統嗎?是誰分配一個神奇的控制碼?它是一個數字,這數字通常也不是一個流水號,他到底是什麼?
handle其實是一個記憶體位址而已,位址說到底也是一個數字,我只要知道這個記憶體位址,就會知道這個物件是放在哪。那這個記憶體位址是誰分配的?其實他也只是C語言而已,C語言也有一個語法是需要對稱呼叫的,想起來了嗎?若不做對稱處理的話可能造成問題,那就是malloc & free。
這個就要講到在1966年以前,Dahl和Nygaard將函式呼叫堆疊框架(stack frame)移到了累堆(heap)中,並發明了OO(Uncle Bob教你如何建構好的軟體架構)。從stack移到heap是一個關鍵的觀念,function裡面的區域變數都是放在stack中的,有什麼方法可以讓function已經返回,而記憶體空間還依舊存在的?除了該死盡量不要用的全域變數,就是使用malloc後,產生的一個記憶體位址,這個位址是存放於heap中,而這個位址,即為物件的handle。所以這個函式就變為constructor,裡面的區域變數則成為instance variables,內部巢狀函式成為method,若使用函式指標,則可以實現多型。
還記得物件導向的三大特性嗎?上段最後提到的多型(polymorphism),還有封裝(encapsulation)、繼承(inheritance)。C語言又如何實現這三大特性?
封裝
下面是用C++寫的一段以class封裝起來的程式碼:
point.hclass Point { public: Point(double x, double y); double distance(const Point& p) const; private: double x; double y; };point.cc
#include "point.h" #include <math.h> Point::Point(double x, double y) : x(x), y(y) {} double Point::distance(const Point& p) const { double dx = x-p.x; double dy = y-p.y; return sqrt(dx*dx + dy*dy); }
封裝就是將「資料」和「函式」封裝,讓資訊隱藏。上面的header檔中的class, public, 及private很明顯都是C++的關鍵字,在C語言中如何做到這樣?
其實在C語言就做到了「完美封裝」,即私有資料成員就放在.c中,若要嚴格禁止caller呼叫.c中的變數及函式,像是使用C++中的private關鍵字,在變數及函式前面加'static'即可達到;公有函式成員就在.h中,供caller使用,這就是C++裡面所用的public關鍵字,而class就簡單用struct取代。C++有一項規定破壞了「完美封裝」,因技術原因,C++編譯器需要在該類別的標頭檔中宣告一個類別的成員變數,而C語言不需要這麼做,他乖乖的放在.c中就好,有時我們會給caller函式庫檔及標頭檔,這樣封裝起來的struct,caller不會知道裡面有什麼成員。我們看看C語言的寫法:
point.hstruct Point; struct Point* makePoint(double x, double y); double distance (struct Point *p1, struct Point *p2);point.c
#include "point.h" #include <stdlib.h> #include <math.h> struct Point { double x,y; }; struct Point* makepoint(double x, double y) { struct Point* p = malloc(sizeof(struct Point)); p->x = x; p->y = y; return p; } double distance(struct Point* p1, struct Point* p2) { double dx = p1->x - p2->x; double dy = p1->y - p2->y; return sqrt(dx*dx+dy*dy); }
header files宣告資料結構與函式;implementation files則是實作。可以注意到point.c中的struct Point,裡面的x,y即為attribute,makepoint即為constructor,distance即為method。在makepoint中,會使用malloc產生一塊struct Point*樣式及大小的記憶體位址,並回傳返回給call makepoint這支function的caller,這個位址即為handle,即代表物件。一句話說明,控制碼(handle)在C語言物件程式設計中用來描述或表示一個「物件」,本質是一個指向某個資料結構的指標。
Uncle Bob說:因為C++需要在標頭檔中宣告一個類別的成員變數,進而caller會知道成員變數的相關資訊,所以編譯器要阻止使用者對它們的存取,才會有加入 public, private和protected等關鍵字的事情。所以當初C++好像很好意的加了這些關鍵字,讓語言多了很多功能,原來才知道是一個不得不的原因,並且破壞C原有的完美封裝,無法將一個類別的宣告(declaration)和定義(definition)分離。他說OO對於封裝可說是一點貢獻都沒有。
所以搞清楚了嗎?不是C語言沒有封裝特性,是有,而且還是完美封裝。
0 意見:
張貼留言