天天看點

學會C語言面向對象程式設計,弄清面向對象實質。1.封裝2.繼承3.多态4.總結

· 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

繼續閱讀