C語言面向物件程式設計
C 語言實現面向物件程式設計
轉載https://blog.csdn.net/onlyshi/article/details/81672279
C 語言實現面向物件程式設計
1、引言
面向物件程式設計(OOP)並不是一種特定的語言或者工具,它只是一種設計方法、設計思想。它表現出來的三個最基本的特性就是封裝、繼承與多型。很多面向物件的程式語言已經包含這三個特性了,例如 Smalltalk、C++、Java。但是你也可以用幾乎所有的程式語言來實現面向物件程式設計,例如 ANSI-C。要記住,面向物件是一種思想,一種方法,不要太拘泥於程式語言。
2、封裝
封裝就是把資料和方法打包到一個類裡面。其實C語言程式設計者應該都已經接觸過了,C 標準庫
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#ifndef SHAPE_H
#define SHAPE_H
#include <stdint.h>
// Shape 的屬性
typedef struct {
int16_t x;
int16_t y;
} Shape;
// Shape 的操作函式,介面函式
void Shape_ctor(Shape * const me, int16_t x, int16_t y);
void Shape_moveBy(Shape * const me, int16_t dx, int16_t dy);
int16_t Shape_getX(Shape const * const me);
int16_t Shape_getY(Shape const * const me);
#endif /* SHAPE_H */
|
這是 Shape 類的宣告,非常簡單,很好理解。一般會把宣告放到標頭檔案裡面 “Shape.h”。
來看下 Shape 類相關的定義,當然是在 “Shape.c” 裡面。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
#include "shape.h"
// 建構函式
void Shape_ctor(Shape * const me, int16_t x, int16_t y)
{
me->x = x;
me->y = y;
}
void Shape_moveBy(Shape * const me, int16_t dx, int16_t dy)
{
me->x += dx;
me->y += dy;
}
// 獲取屬性值函式
int16_t Shape_getX(Shape const * const me)
{
return me->x;
}
int16_t Shape_getY(Shape const * const me)
{
return me->y;
}
|
再看下 main.c
1 #include "shape.h" /* Shape class interface */ 2 #include <stdio.h> /* for printf() */ 3 4 int main() 5 { 6 Shape s1, s2; /* multiple instances of Shape */ 7 8 Shape_ctor(&s1, 0, 1); 9 Shape_ctor(&s2, -1, 2); 10 11 printf("Shape s1(x=%d,y=%d)\n", Shape_getX(&s1), Shape_getY(&s1)); 12 printf("Shape s2(x=%d,y=%d)\n", Shape_getX(&s2), Shape_getY(&s2)); 13 14 Shape_moveBy(&s1, 2, -4); 15 Shape_moveBy(&s2, 1, -2); 16 17 printf("Shape s1(x=%d,y=%d)\n", Shape_getX(&s1), Shape_getY(&s1)); 18 printf("Shape s2(x=%d,y=%d)\n", Shape_getX(&s2), Shape_getY(&s2)); 19 20 return 0; 21 }
編譯之後,看看執行結果:
Shape s1(x=0,y=1)
Shape s2(x=-1,y=2)
Shape s1(x=2,y=-3)
Shape s2(x=0,y=0)
整個例子,非常簡單,非常好理解。以後寫程式碼時候,要多去想想標準庫的檔案IO操作,這樣也有意識的去培養面向物件程式設計的思維。
3、繼承
繼承就是基於現有的一個類去定義一個新類,這樣有助於重用程式碼,更好的組織程式碼。在 C 語言裡面,去實現單繼承也非常簡單,只要把基類放到繼承類的第一個資料成員的位置就行了。
例如,我們現在要建立一個 Rectangle 類,我們只要繼承 Shape 類已經存在的屬性和操作,再新增不同於 Shape 的屬性和操作到 Rectangle 中。
下面是 Rectangle 的宣告與定義:
1 #ifndef RECT_H 2 #define RECT_H 3 4 #include "shape.h" // 基類介面 5 6 // 矩形的屬性 7 typedef struct { 8 Shape super; // 繼承 Shape 9 10 // 自己的屬性 11 uint16_t width; 12 uint16_t height; 13 } Rectangle; 14 15 // 建構函式 16 void Rectangle_ctor(Rectangle * const me, int16_t x, int16_t y, 17 uint16_t width, uint16_t height); 18 19 #endif /* RECT_H */
#include "rect.h" // 建構函式 void Rectangle_ctor(Rectangle * const me, int16_t x, int16_t y, uint16_t width, uint16_t height) { /* first call superclass’ ctor */ Shape_ctor(&me->super, x, y); /* next, you initialize the attributes added by this subclass... */ me->width = width; me->height = height; }
我們來看一下 Rectangle 的繼承關係和記憶體佈局
因為有這樣的記憶體佈局,所以你可以很安全的傳一個指向 Rectangle 物件的指標到一個期望傳入 Shape 物件的指標的函式中,就是一個函式的引數是 “Shape *”,你可以傳入 “Rectangle *”,並且這是非常安全的。這樣的話,基類的所有屬性和方法都可以被繼承類繼承!
1 #include "rect.h" 2 #include <stdio.h> 3 4 int main() 5 { 6 Rectangle r1, r2; 7 8 // 例項化物件 9 Rectangle_ctor(&r1, 0, 2, 10, 15); 10 Rectangle_ctor(&r2, -1, 3, 5, 8); 11 12 printf("Rect r1(x=%d,y=%d,width=%d,height=%d)\n", 13 Shape_getX(&r1.super), Shape_getY(&r1.super), 14 r1.width, r1.height); 15 printf("Rect r2(x=%d,y=%d,width=%d,height=%d)\n", 16 Shape_getX(&r2.super), Shape_getY(&r2.super), 17 r2.width, r2.height); 18 19 // 注意,這裡有兩種方式,一是強轉型別,二是直接使用成員地址 20 Shape_moveBy((Shape *)&r1, -2, 3); 21 Shape_moveBy(&r2.super, 2, -1); 22 23 printf("Rect r1(x=%d,y=%d,width=%d,height=%d)\n", 24 Shape_getX(&r1.super), Shape_getY(&r1.super), 25 r1.width, r1.height); 26 printf("Rect r2(x=%d,y=%d,width=%d,height=%d)\n", 27 Shape_getX(&r2.super), Shape_getY(&r2.super), 28 r2.width, r2.height); 29 30 return 0; 31 }
輸出結果:
Rect r1(x=0,y=2,width=10,height=15)
Rect r2(x=-1,y=3,width=5,height=8)
Rect r1(x=-2,y=5,width=10,height=15)
Rect r2(x=1,y=2,width=5,height=8)
4、多型
C++ 語言實現多型就是使用虛擬函式。在 C 語言裡面,也可以實現多型。
現在,我們又要增加一個圓形,並且在 Shape 要擴充套件功能,我們要增加 area() 和 draw() 函式。但是 Shape 相當於抽象類,不知道怎麼去計算自己的面積,更不知道怎麼去畫出來自己。而且,矩形和圓形的面積計算方式和幾何影象也是不一樣的。
下面讓我們重新宣告一下 Shape 類
1 #ifndef SHAPE_H 2 #define SHAPE_H 3 4 #include <stdint.h> 5 6 struct ShapeVtbl; 7 // Shape 的屬性 8 typedef struct { 9 struct ShapeVtbl const *vptr; 10 int16_t x; 11 int16_t y; 12 } Shape; 13 14 // Shape 的虛表 15 struct ShapeVtbl { 16 uint32_t (*area)(Shape const * const me); 17 void (*draw)(Shape const * const me); 18 }; 19 20 // Shape 的操作函式,介面函式 21 void Shape_ctor(Shape * const me, int16_t x, int16_t y); 22 void Shape_moveBy(Shape * const me, int16_t dx, int16_t dy); 23 int16_t Shape_getX(Shape const * const me); 24 int16_t Shape_getY(Shape const * const me); 25 26 static inline uint32_t Shape_area(Shape const * const me) 27 { 28 return (*me->vptr->area)(me); 29 } 30 31 static inline void Shape_draw(Shape const * const me) 32 { 33 (*me->vptr->draw)(me); 34 } 35 36 37 Shape const *largestShape(Shape const *shapes[], uint32_t nShapes); 38 void drawAllShapes(Shape const *shapes[], uint32_t nShapes); 39 40 #endif /* SHAPE_H */
看下加上虛擬函式之後的類關係圖
4.1 虛表和虛指標
虛表(Virtual Table)是這個類所有虛擬函式的函式指標的集合。
虛指標(Virtual Pointer)是一個指向虛表的指標。這個虛指標必須存在於每個物件例項中,會被所有子類繼承。
在《Inside The C++ Object Model》的第一章內容中,有這些介紹。
4.2 在建構函式中設定vptr
在每一個物件例項中,vptr 必須被初始化指向其 vtbl。最好的初始化位置就是在類的建構函式中。事實上,在建構函式中,C++ 編譯器隱式的建立了一個初始化的vptr。在 C 語言裡面, 我們必須顯示的初始化vptr。
1 下面就展示一下,在 Shape 的建構函式裡面,如何去初始化這個 vptr。 2 3 #include "shape.h" 4 #include <assert.h> 5 6 // Shape 的虛擬函式 7 static uint32_t Shape_area_(Shape const * const me); 8 static void Shape_draw_(Shape const * const me); 9 10 // 建構函式 11 void Shape_ctor(Shape * const me, int16_t x, int16_t y) 12 { 13 // Shape 類的虛表 14 static struct ShapeVtbl const vtbl = 15 { 16 &Shape_area_, 17 &Shape_draw_ 18 }; 19 me->vptr = &vtbl; 20 me->x = x; 21 me->y = y; 22 } 23 24 25 void Shape_moveBy(Shape * const me, int16_t dx, int16_t dy) 26 { 27 me->x += dx; 28 me->y += dy; 29 } 30 31 32 int16_t Shape_getX(Shape const * const me) 33 { 34 return me->x; 35 } 36 int16_t Shape_getY(Shape const * const me) 37 { 38 return me->y; 39 } 40 41 // Shape 類的虛擬函式實現 42 static uint32_t Shape_area_(Shape const * const me) 43 { 44 assert(0); // 類似純虛擬函式 45 return 0U; // 避免警告 46 } 47 48 static void Shape_draw_(Shape const * const me) 49 { 50 assert(0); // 純虛擬函式不能被呼叫 51 } 52 53 54 Shape const *largestShape(Shape const *shapes[], uint32_t nShapes) 55 { 56 Shape const *s = (Shape *)0; 57 uint32_t max = 0U; 58 uint32_t i; 59 for (i = 0U; i < nShapes; ++i) 60 { 61 uint32_t area = Shape_area(shapes[i]);// 虛擬函式呼叫 62 if (area > max) 63 { 64 max = area; 65 s = shapes[i]; 66 } 67 } 68 return s; 69 } 70 71 72 void drawAllShapes(Shape const *shapes[], uint32_t nShapes) 73 { 74 uint32_t i; 75 for (i = 0U; i < nShapes; ++i) 76 { 77 Shape_draw(shapes[i]); // 虛擬函式呼叫 78 } 79 }
註釋比較清晰,這裡不再多做解釋。
4.3 繼承 vtbl 和 過載 vptr
上面已經提到過,基類包含 vptr,子類會自動繼承。但是,vptr 需要被子類的虛表重新賦值。並且,這也必須發生在子類的建構函式中。下面是 Rectangle 的建構函式。
1 #include "rect.h" 2 #include <stdio.h> 3 4 // Rectangle 虛擬函式 5 static uint32_t Rectangle_area_(Shape const * const me); 6 static void Rectangle_draw_(Shape const * const me); 7 8 // 建構函式 9 void Rectangle_ctor(Rectangle * const me, int16_t x, int16_t y, 10 uint16_t width, uint16_t height) 11 { 12 static struct ShapeVtbl const vtbl = 13 { 14 &Rectangle_area_, 15 &Rectangle_draw_ 16 }; 17 Shape_ctor(&me->super, x, y); // 呼叫基類的建構函式 18 me->super.vptr = &vtbl; // 過載 vptr 19 me->width = width; 20 me->height = height; 21 } 22 23 // Rectangle's 虛擬函式實現 24 static uint32_t Rectangle_area_(Shape const * const me) 25 { 26 Rectangle const * const me_ = (Rectangle const *)me; //顯示的轉換 27 return (uint32_t)me_->width * (uint32_t)me_->height; 28 } 29 30 static void Rectangle_draw_(Shape const * const me) 31 { 32 Rectangle const * const me_ = (Rectangle const *)me; //顯示的轉換 33 printf("Rectangle_draw_(x=%d,y=%d,width=%d,height=%d)\n", 34 Shape_getX(me), Shape_getY(me), me_->width, me_->height); 35 }
4.4 虛擬函式呼叫
有了前面虛表(Virtual Tables)和虛指標(Virtual Pointers)的基礎實現,虛擬呼叫(後期繫結)就可以用下面程式碼實現了。
1 uint32_t Shape_area(Shape const * const me) 2 { 3 return (*me->vptr->area)(me); 4 }
這個函式可以放到.c檔案裡面,但是會帶來一個缺點就是每個虛擬呼叫都有額外的呼叫開銷。為了避免這個缺點,如果編譯器支援行內函數(C99)。我們可以把定義放到標頭檔案裡面,類似下面:
1 static inline uint32_t Shape_area(Shape const * const me) 2 { 3 return (*me->vptr->area)(me); 4 }
如果是老一點的編譯器(C89),我們可以用巨集函式來實現,類似下面這樣:
1 #define Shape_area(me_) ((*(me_)->vptr->area)((me_)))
1
看一下例子中的呼叫機制:
4.5 main.c
1 #include "rect.h" 2 #include "circle.h" 3 #include <stdio.h> 4 5 int main() 6 { 7 Rectangle r1, r2; 8 Circle c1, c2; 9 Shape const *shapes[] = 10 { 11 &c1.super, 12 &r2.super, 13 &c2.super, 14 &r1.super 15 }; 16 Shape const *s; 17 18 // 例項化矩形物件 19 Rectangle_ctor(&r1, 0, 2, 10, 15); 20 Rectangle_ctor(&r2, -1, 3, 5, 8); 21 22 // 例項化圓形物件 23 Circle_ctor(&c1, 1, -2, 12); 24 Circle_ctor(&c2, 1, -3, 6); 25 26 s = largestShape(shapes, sizeof(shapes)/sizeof(shapes[0])); 27 printf("largetsShape s(x=%d,y=%d)\n", Shape_getX(s), Shape_getY(s)); 28 29 drawAllShapes(shapes, sizeof(shapes)/sizeof(shapes[0])); 30 31 return 0; 32 }
輸出結果:
largetsShape s(x=1,y=-2)
Circle_draw_(x=1,y=-2,rad=12)
Rectangle_draw_(x=-1,y=3,width=5,height=8)
Circle_draw_(x=1,y=-3,rad=6)
Rectangle_draw_(x=0,y=2,width=10,height=15)
5、總結
還是那句話,面向物件程式設計是一種方法,並不侷限於某一種程式語言。用 C 語言實現封裝、單繼承,理解和實現起來比較簡單,多型反而會稍微複雜一點,如果打算廣泛的使用多型,還是推薦轉到 C++ 語言上,畢竟這層複雜性被這個語言給封裝了,你只需要簡單的使用就行了。但並不代表,C 語言實現不了多型這個特性。