· C語言真的是這個世界上的老古董了,1972年 Dennis MacAlistair Ritchie 建立它至今,雖然做過幾次修改,但是它畢竟是面向過程的語言,是以大家使用起來還是很費力的。但是C語言仍然在嵌入式領域占據絕對優勢,沒有比C語言更快的進階語言了,著名的作業系統Linux就是C語言的最好執行個體。可以說Linux不被淘汰,C語言就不會過時。
· 後續産生了C++,Java,Python等等各種支援面向對象的語言,而面向對象作為一種先進思想,也深刻影響C語言的程式設計風格。雖然C語言不是面向對象的語言,但是C語言仍然可以使用面向對象的方式進行程式設計(雖然比較繁瑣)。而大多數語言中的面向對象實作都是依據C語言開發的。本文為大家提供基本的面向對象的C語言實作方式,帶大家搞清楚面向對象的實質,希望大家能夠在後續的程式設計中使用。
· 本文中代碼的執行個體的思想都來源于開源代碼,本文代碼執行個體在此可免費下載下傳。
1.封裝
· 封裝就是“物以類聚”,将所有和某個對象相關的内容都放在一起,包括資料和操作函數。絕大多數的面向對象的語言都有關鍵字 class,但是C語言中沒有,我們隻能使用結構體 struct 作為封裝對象的方法。
· 支援面向對象的語言,可以将相關代碼內聚到一個對象中,但是C語言這邊做不到,建議将資料放到 struct 中,相關函數也可以以函數指針的方式放入,3個以内的函數可以直接添加,函數指針太多的話就封裝一個struct作為操作函數的集合。然後建立一個初始化函數,在函數初始化時對這些操作函數指派。
· 這裡講一個例子:
struct OrderOperations {
int (*price)(struct Order *this);
};
struct Order {
int quantity; /*這是資料*/
int itemPrice;
struct OrderOperations *orderOp; /*這是操作函數集合*/
};
· 比如為了封裝一個資料 quantity,相關的操作則以函數指針的方式指派進來,如果操作函數比較多,建議也放到一個struct中來,由于C語言沒有this可以用,這裡将第一個入參都要用這個對象的指針作為第一個參數。這裡舉一個例子。
#define max(x,y) ((x)>(y)? (x):(y))
#define min(x,y) ((x)<(y)? (x):(y))
static int price(struct Order *this) {
//price is base price - quantity discount + shipping
return this->quantity * this->itemPrice -
max(0, this->quantity - 500) * this->itemPrice * 0.05 +
min(this->quantity * this->itemPrice * 0.1, 100);
}
· 操作函數可以在初始化的時候,要指派進去,這裡建議用指針的方式使用。比如下面建立一個構造函數 alloc_Order 用來初始化這個對象的指針,在初始化的時候後将這個靜态的結構 orderOp 指派進來,如果想做個性化操作,也可以新建立一個構造函數,增加入參來實作。
struct Order* alloc_Order(void) {
#define ORDER_DEFAULT_DATA 1
static struct OrderOperations orderOp = {
.price = price,
};
struct Order* order = (struct Order* )malloc(sizeof(struct Order));
order->orderOp = &orderOp;
order->quantity = ORDER_DEFAULT_DATA;
order->itemPrice = ORDER_DEFAULT_DATA;
return order;
}
int main(int argc, char *argv[]) {
struct Order* order = alloc_Order();
order->itemPrice = 5;
order->quantity = 2;
printf("order price is %d.\n", order->orderOp->price(order));
}
· 當函數比較多的情況,更可以展現出這種方式的優勢:
struct OrderOperations {
int (*price)(struct Order *this);
int (*getBasePrice)(struct Order *this);
int (*getQuantityDiscount)(struct Order *this);
int (*getShipping)(struct Order *this);
};
struct Order {
int quantity; /*這是資料*/
int itemPrice;
struct OrderOperations *orderOp; /*這是操作函數集合*/
};
int getBasePrice(struct Order *this) {
return this->quantity * this->itemPrice;
}
int getQuantityDiscount(struct Order *this) {
return max(0, this->quantity - 500) * this->itemPrice * 0.05;
}
int getShipping(struct Order *this) {
return min(this->quantity * this->itemPrice * 0.1, 100);
}
int getPrice(struct Order *this) {
return this->orderOp->getBasePrice(this) - this->orderOp->getQuantityDiscount(this) + this->orderOp->getShipping(this);
}
struct Order* alloc_Order(void) {
#define ORDER_DEFAULT_DATA 0
static struct OrderOperations orderOp = {
.price = getPrice,
.getBasePrice = getBasePrice,
.getQuantityDiscount = getQuantityDiscount,
.getShipping = getShipping,
};
struct Order* order = (struct Order* )malloc(sizeof(struct Order));
order->orderOp = &orderOp;
order->quantity = ORDER_DEFAULT_DATA;
order->itemPrice = ORDER_DEFAULT_DATA;
return order;
}
int main(int argc, char *argv[]) {
struct Order* order = alloc_Order();/* 申請對象 */
order->itemPrice = 5;
order->quantity = 2;
printf("order price is %d.\n", order->orderOp->price(order));/* 調用對象中的函數 */
}
· 可能有人發現,封裝也可以分等級啊,private,protected 和 public這個怎麼實作?C語言一般隻分為2級,這個實作也是很簡單的,使用一個空指針void*private,在其初始化的時候使用malloc配置設定一段堆空間,然後使用這個空指針的地方定義這個結構體,使用的時候就指針強轉成對應的結構體來使用。如下例子:
struct PrivateData {
int quantity; /*這是資料*/
int itemPrice;
};
struct Order {
void *private;
/*多個資料*/
};
struct Order* alloc_Order(void) {
#define ORDER_DEFAULT_DATA 0
struct Order* order = (struct Order* )malloc(sizeof(struct Order));
struct PrivateData *private = (struct PrivateData * )malloc(sizeof(struct PrivateData ));
private->quantity = ORDER_DEFAULT_DATA;
private->itemPrice = ORDER_DEFAULT_DATA;
order->private = private;/*私有資料*/
return order;
}
· 這裡也要提到,C語言封裝雖然用了2層,但是也夠用了,隻要在要用的地方添加包含定義此結構的頭檔案就行,不加頭檔案的地方完全不知道這個指針怎麼用。
2.繼承
· 繼承就是将各個不同對象的共性提取,抽象出共同的基類,繼承的層次可能有很多。
· 用C語言怎麼做呢?好吧,沒有辦法還是使用struct,将基類放到一個struct裡面,然後它的子類都包含這個基類。如果是基類的話,建議放在struct的開頭。
· 擁有父子關系的兩個 struct 怎麼互相轉化,你可能猜到了,用指針強轉,由于位址的偏移都在struct的定義時就清楚了,是以這種方式實作時沒有問題的。先看看如下例子:
/*基類定義*/
struct base_class {
int private_data;
};
/*派生類定義*/
struct derived_class {
struct base_class parent;
int private_data;
};
/*從子對象轉成父對象*/
int main(int argc, char *argv[]) {
struct derived_class son;/*子類定義*/
struct base_class *parent = (struct base_class *)&son;/*父類指針初始化*/
return 0;
}
· 如果是父類轉化成子類呢?其實這種情況的使用會比較多,父類就是更抽象的類,也有一些專有操作會被子類覆寫,比如基類擷取子類通常就在虛函數被覆寫的時候。
· 如何擷取子類,原理就是位址偏移,如果是單繼承的偏移就是0,而多繼承的便宜需要計算偏移位置。
/*基類1定義*/
struct base_class1 {
int private_data;
};
/*基類2定義*/
struct base_class2 {
int private_data;
};
/*派生類定義*/
struct derived_class{
struct base_class1 parent1;
struct base_class2 parent2;
int private_data;
};
int get_private_data(struct base_class2 *parent) {
/******************這裡是重點*********************/
struct derived_class *son = (struct derived_class *)((char *)parent-(char *)(&((struct derived_class *)0)->parent2));
return son->private_data;
}
/*從父對象轉成子對象*/
int main(void **argc,void *argv[]) {
struct derived_class son;/*子類定義*/
son.private_data = 3;
/* son 的各種操作*/
struct base_class2 *parent = (struct base_class *)&son.parent2;/*父類指針初始化*/
parent->private_data = get_private_data(parent);
printf("parent data = %d, son data = %d.\n",son.private_data, parent->private_data);
return 0;
}
· 這個方法有點繞,但是是可以實作的,使用宏定義會比較友善,見如下代碼。
#define container_of(ptr, type, member) ({ \
(type *)((char *)ptr-(char *)(&((type *)0)->member));})
int get_private_data(struct base_class2 *parent) {
struct derived_class *son = container_of(parent, struct derived_class, parent2);
return son->private_data;
}
· 這個實作方式使用了強制轉化,而Linux核心則使用了GCC提供的關鍵字typeof 來讓這種更為合理的實作這種方式,typeof 是用來擷取某一變量的類型。我們替換一下 container_of 也能得到相同的結果。(本人用的QT環境)
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
#define container_of(ptr, type, member) ({ \
const typeof(((type *)0)->member) * __mptr = (ptr); \
(type *)((char *)__mptr - offsetof(type, member)); })
你可能覺得這個方式使用的不多,下面一節将告訴你怎麼使用。
3.多态
· 多态就是子類可以決定是否覆寫父類的虛函數,來實作不同的内容,主要是通過虛函數來實作。
· 虛函數的實作其實很簡單,就是父類定義一個函數指針,如果純虛函數就讓指針為空,不是純虛函數就給其初始化為一個預設函數,如果子類要對其覆寫,就對其重新初始化為一個自設計的函數。
· 的确虛函數了解很簡單,C++使用一個虛表的方式來實作,但是C語言使用這個思想并不用這麼複雜,使用函數指針就行,見如下例子。
/*基類定義:圓*/
struct circle {
double radius;/*半徑*/
double (*circumference)(struct circle *this);/*周長計算*/
};
double circle_circumference(struct circle *this) {
return this->radius * PI;/*預設圓的周長計算*/
}
struct circle* alloc_circle(int radius) {
struct circle* circle = (struct circle *)malloc(sizeof(struct circle));
circle->radius = radius;
circle->circumference = circle_circumference;
return circle;
}
/*派生類定義:圓方(圓平分2半,中間一個方形)*/
struct circle_square {
struct circle circle;
double side_length;/*邊長*/
};
/*自有函數聲明*/
double circle_square_circumference(struct circle *this) {
struct circle_square *cs = container_of(this, struct circle_square, circle);
return this->radius * PI + cs->side_length * 2;
}
struct circle_square* alloc_circle_square(double radius, double side_length) {
struct circle_square* cs = (struct circle_square *)malloc(sizeof(struct circle_square));
cs->circle.radius = radius;
cs->side_length = side_length;
/*覆寫為自有函數*/
cs->circle.circumference = circle_square_circumference;
return cs;
}
/**** 調用新的計算周長函數 ****/
int main(void **argc,void *argv[]) {
struct circle_square *son = alloc_circle_square(1, 2);/*子類定義*/
/* son的各種操作 */
printf("circle_square circumference = %f.\n",son->circle.circumference(&son->circle));
free(son);
return 0;
}
· 虛表就是函數指針表,JAVA也是使用虛表來實作,這種方式主要是友善大家替換,大家用的就是同一張虛表,虛函數的覆寫就變成了對表内容的修改,虛函數調用就是變成了查表,非常友善。C語言這麼做就要加很多的判斷,麻煩的多,但是我們能看到它的本質就是函數指針。
4.總結
· C語言雖然沒有添加面向對象的語言特性,但是由于C語言是計算機操作的抽象,是以絕大多數的面向對象的操作都是通過C語言來實作的,這也讓我們更能知道面向對象實作的精髓,知其然而知其是以然,更好的了解各種面向對象語言的實質。
· 寫這篇的目的是為了《重構C語言版》打基礎,因為重構中用到了太多的面向對象思想了,歡迎大家點個關注,及時擷取我的後續文章。
下一篇:https://blog.csdn.net/weixin_42523774/article/details/105619681