1. 程式人生 > 實用技巧 >C語言面向物件程式設計

C語言面向物件程式設計

C 語言實現面向物件程式設計

轉載https://blog.csdn.net/onlyshi/article/details/81672279

C 語言實現面向物件程式設計
1、引言
面向物件程式設計(OOP)並不是一種特定的語言或者工具,它只是一種設計方法、設計思想。它表現出來的三個最基本的特性就是封裝、繼承與多型。很多面向物件的程式語言已經包含這三個特性了,例如 Smalltalk、C++、Java。但是你也可以用幾乎所有的程式語言來實現面向物件程式設計,例如 ANSI-C。要記住,面向物件是一種思想,一種方法,不要太拘泥於程式語言。

2、封裝
封裝就是把資料和方法打包到一個類裡面。其實C語言程式設計者應該都已經接觸過了,C 標準庫

中的 fopen(), fclose(), fread(), fwrite()等函式的操作物件就是 FILE。資料內容就是 FILE,資料的讀寫操作就是 fread()、fwrite(),fopen() 類比於建構函式,fclose() 就是解構函式。這個看起來似乎很好理解,那下面我們實現一下基本的封裝特性。

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 的屬性 typedefstruct{ int16_t x;
int16_t y; } Shape; // Shape 的操作函式,介面函式 voidShape_ctor(Shape *constme, int16_t x, int16_t y); voidShape_moveBy(Shape *constme, int16_t dx, int16_t dy); int16_t Shape_getX(Shapeconst*constme); int16_t Shape_getY(Shapeconst*constme); #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" // 建構函式 voidShape_ctor(Shape *constme, int16_t x, int16_t y) { me->x = x; me->y = y; } voidShape_moveBy(Shape *constme, int16_t dx, int16_t dy) { me->x += dx; me->y += dy; } // 獲取屬性值函式 int16_t Shape_getX(Shapeconst*constme) { returnme->x; } int16_t Shape_getY(Shapeconst*constme) { returnme->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 語言實現不了多型這個特性。