上次課,錢SIR提到,Liux下面也有很多用C實現的物件導向的結構。比較感覺興趣,就在網上查了一些資料,原來C語言類比實現物件導向語言所具有的特性:多態,繼承,封裝,也是一件很簡單的事兒。並且現在很多開源軟體都了用C語言實現了這幾個特性,包括大型開來源資料庫系統postgreSQL,可移植的C語言物件導向架構GObject。
在自己機器上實踐了下,感歎C語言的靈活與強大!總結一下,以便交流:
一、基礎知識
(1)結構體
結構體可以嵌套,因而可以把一個結構體當成另一個結構體的成員,如:
struct Point{ int x; int y;};
struct Circle { struct Point point_; int radius; };
該結構體與以下定義完全一樣(包括記憶體布置都一樣
struct Circle { int x; int y; int radius; };
(2)void *
指標是整個 C 語言的精髓所在。而你也一直敬畏著指標,又愛又恨地使用著它。許多教材都告訴你,int *叫做指向整型的指標,而 char *是指向字元型的指標,等等等等不一而足。然而這裡有一個另類的指標家族成員——void *。不要按照通常的命名方式叫它做指向void 類型的指標,它的正式的名字叫做:可以指向任意類型的指標。
(3)C中的參數個數可變函數
可變參數函數的原型聲明:
type VAFunction(type arg1, type arg2, … );
參數可以分為兩部分:個數確定的固定參數和個數可變的選擇性參數。函數至少需要一個固定參數,固定參數的聲明和普通函數一樣;選擇性參數由於個數不確定,聲明時用"..."表示。固定參數和選擇性參數公同構成一個函數的參數列表。
標準C/C++包含標頭檔stdarg.h,該標頭檔中定義了操作不定變數的相關宏:
void va_start ( va_list arg_ptr, prev_param ); /* ANSI version */type va_arg ( va_list arg_ptr, type ); void va_end ( va_list arg_ptr );
在這些宏中,va就是variable argument(可變參數)的意思;
arg_ptr 是指向可變參數表的指標;
prev_param 指可變參數表的前一個固定參數;
type 為可變參數的類型。
va_list 也是一個宏,其定義為typedef char * va_list,實質上是一char型指標。
二、封裝
封裝的主要含義是隱藏內部的行為和資訊,使用者只用看到對外提供的介面和公開的資訊。
在C語言中的實現方法:把私人資料資訊放在一個不透明的priv變數或者結構體中,只有類的實現代碼才知道priv或者結構體的真正定義。
例如:
//========標頭檔:Point.h檔案========#ifndef POINT_H#define POINT_Htypedef struct Point point;typedef struct pointPrivate pointPrivate;struct Point{struct pointPrivate *pp;};int get_x(point *point_);int get_y(point *point_);point * new_point(int x,int y);}#endif
源檔案
//=======C檔案:Point.c檔案========#include "Point.h"#include<stdlib.h>struct pointPrivate; int x; int y;};int get_x(point *point_){ return point_->pp->x;}int get_y(point *point_){ return point_->pp->y;}point* new_point(int x,int y){ point* p=(point*)malloc(sizeof(point)); p->pp=(pointPrivate*)malloc(sizeof(pointPrivate)); p->pp->x=x; p->pp->y=y; return p;}
測試檔案:
int main(){point* p = new_point(1,2);//printf("x:%d,y:%d\n",p->pp->x,p->pp->y);printf("x:%d,y:%d\n",get_x(p),get_y(p));}
在測試代碼中,注釋掉的一部分是編譯不過的,因為我們已經把pointPrivate結構體的定義隱藏了。而且必須使用new_point來建立point結構對象,否則無法初始化point結構體中的pp成員變數。
有意思的是:這段代碼產生的exe檔案可能會被360誤認為病毒。
三、繼承
在C語言中,可以利用“結構在記憶體中的布局與結構的聲明具有一致的順序”這一事實實現繼承。 比如我們要設計一個作圖工具,其中可能涉及到的對象有Point(點),Circle(圓),由於圓是由點組成的,所有可以看成Circle繼承自Point。另外,Point和Circle都需要空間申請,空間釋放等操作,所有他們有共同的基類Base。
//基類Base的內部標頭檔Base.r,對外隱藏#ifndef BASE_R#define BASE_R#include struct Base {size_t size;void * (* ctor) (void * self, va_list * app);//建構函式void * (* dtor) (void * self); //解構函式void (* draw) (const void * self);//作圖函數};#endif
//Point的內部標頭檔Point.r,對外隱藏#ifndef POINT_R#define POINT_Rstruct Point {const void * base; //繼承Base類,基類指標,放在第一個位置,const是防止修改int x, y; //座標};#define x(p) (((const struct Point *)(p)) -> x)#define y(p) (((const struct Point *)(p)) -> y)#endif
//Point的標頭檔Point.h(對外提供介面)#ifndef POINT_H#define POINT_Hextern const void * Point; /* new(Point, x, y); */void move (void * point, int dx, int dy);#endif
//Point的源檔案Point.c#include #include "Point.h"#include "Point.r"#include "new.h"#include "Base.r"/**********Point類自己的建構函式***********/static void * Point_ctor (void * _self, va_list * app){ struct Point * self = _self;self -> x = va_arg(* app, int);self -> y = va_arg(* app, int);return self;}/**********Point類自己的繪圖函數***********/static void Point_draw (const void * _self){ const struct Point * self = _self;printf("Point at %d,%d\n", self -> x, self -> y);}static const struct Base _Point = {sizeof(struct Point), Point_ctor, 0, Point_draw};const void * Point = & _Point;void move (void * _self, int dx, int dy){ struct Point * self = _self;self -> x += dx, self -> y += dy;}
//Circle內部標頭檔Circle.r,對外隱藏#ifndef CIRCLE_R#define CIRCLE_R#include "Point.r"struct Circle { const struct Point _; //繼承Point類,需放在第一位int rad; };#endif
//Circle的標頭檔Circle.h(對外提供介面)#ifndef CIRCLE_H#define CIRCLE_H#include "Point.h"extern const void * Circle; /* new(Circle, x, y, rad) */#endif
//Circle的源檔案Circle.c#include #include "Circle.h"#include "Circle.r"#include "new.h"#include "Base.r"/**********Circle類自己的建構函式***********/static void * Circle_ctor (void * _self, va_list * app){ struct Circle * self = ((const struct Base *) Point) -> ctor(_self, app);self -> rad = va_arg(* app, int);return self;}/**********Circle類自己的繪圖函數***********/static void Circle_draw (const void * _self){ const struct Circle * self = _self;printf("circle at %d,%d rad %d\n",x(self), y(self), self -> rad);}static const struct Base _Circle = {sizeof(struct Circle), Circle_ctor, 0, Circle_draw};const void * Circle = & _Circle;
//記憶體管理類標頭檔new.h(對外提供介面)#ifndef NEW_H#define NEW_Hvoid * new (const void * base, ...);void delete (void * item);void draw (const void * self);#endif
//記憶體管理類的源檔案:new.c#include #include #include #include "Base.r"void * new (const void * _class, ...){ const struct Base * base = _class;void * p = calloc(1, base -> size);assert(p);* (const struct Base **) p = base;if (base -> ctor){ va_list ap;va_start(ap, _class);p = base -> ctor(p, & ap);va_end(ap);}return p;}void delete (void * self){ const struct Base ** cp = self;if (self && * cp && (* cp) -> dtor)self = (* cp) -> dtor(self);free(self);}void draw (const void * self){ const struct Base * const * cp = self;assert(self && * cp && (* cp) -> draw);(* cp) -> draw(self);}
四、多態可以是用C語言中的萬能指標void* 實現多態,接上面的例子:
#include "Circle.h"#include "new.h"int main (int argc, char ** argv){void * p;int i;for(i=0; i<2; i++){if(i==0)p = new(Circle, 1, 2, 3);elsep = new(Point, 1, 2);draw(p);move(p, 10, 20);draw(p);delete(p);}return 0;}
輸出結果:
circle at 1,2 rad 3
circle at 11,22 rad 3
Point at 1,2
Point at 11,22
五、總結
物件導向是一種程式設計思想,而 C 語言則是一種程式設計語言。也許它並不是專門為了物件導向編程而設計,但是這絕不意味著它不能實現物件導向的程式設計。當然以上所展示的這幾個操作,如果是用別的程式設計語言,可能只要寥寥幾行就可以完成,但是 C 語言想告訴我們的是:也許我不擅長,但是並不意味著我做不到。
參考書籍《Object-Oriented Programming With ANSI-C》