學會C語言面向對象編程,弄清面向對象實質。

· 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語言中實現的意義並不大,畢竟這些級別都是爲了限制開發人員後續使用對象時的對其修改。這裏給出一個建議,將private的變量使用以“_”爲開頭,提示後續的人員謹慎修改。

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章