引言
面向對象是一個老生常談的話題胁后,其基本思想為封裝、繼承嗦枢、多態(tài)择同。
最近在學習 Linux 系統(tǒng)源碼時,發(fā)現(xiàn)雖然系統(tǒng)是使用面向過程的 C 語言編寫净宵,但是還是可以體現(xiàn)出面向對象的思想敲才,如文件系統(tǒng)中的文件操作 file_operation
結構坦辟,每種 inode 都有其對應的操作結構暴区,這正是面向對象中多態(tài)的體現(xiàn)。
所以玉组,經(jīng)過小小的研究敏储,我使用 C 語言成功實現(xiàn)了類似面向對象化的編程阻星。
這也充分說明,面向對象的編程思想和范式,可以不受語言的拘泥妥箕。正所謂金庸筆下的獨孤求敗滥酥,「四十歲後,不滯於物畦幢,草木竹石均可為劍坎吻。自此精修,漸進於無劍勝有劍之境宇葱∈菡妫」
不多廢話,我們一起來看是如何實現(xiàn)的吧黍瞧。
封裝
封裝也叫作信息隱藏或者數(shù)據(jù)訪問保護诸尽。類通過暴露有限的訪問接口,授權外部僅能通過類提供的方式來訪問內部信息或者數(shù)據(jù)印颤。
封裝特性存在的意義您机,一方面是保護數(shù)據(jù)不被隨意修改,提高代碼的可維護性年局;另一方面是僅暴露有限的必要接口往产,提高類的易用性。
我們嘗試封裝一個簡單的Point“類”某宪,并且定義相關的 getter/setter 方法。當然 C語言沒有辦法在聲明中隱藏其數(shù)據(jù)結構锐朴,并且由于沒有訪問控制功能兴喂,沒有辦法做到完美的封裝。
//Point.h
typedef struct{
int x;
int y;
}Point;
int Point_getX(Point const p);
int Point_getY(Point const p);
void Point_init(Point *p, int x, int y);
然后我們在 .c 文件中實現(xiàn)前面定義的方法焚志。
//Point.c
#include "Point.h"
int Point_getX(Point const p) {
return p.x;
}
int Point_getY(Point const p) {
return p.y;
}
void Point_init(Point *p, int x, int y) {
p->x = x;
p->y = y;
}
大功告成衣迷!
我們來體驗一下剛才封裝的 Point
“類”。
創(chuàng)建一個 Point
“對象”酱酬,使用 setter 設置它的“屬性”壶谒,然后通過 getter 將值取出打印出來。
//main.c
#include <stdio.h>
#include "Point.h"
int main(int argc, const char * argv[]) {
Point p;
Point_init(&p, 10, 20);
printf("point x:%d,y:%d\n", Point_getX(p), Point_getY(p));
return 0;
}
控制臺輸出
point x:10,y:20
繼承
繼承是用來表示類之間的 is-a 關系膳沽,用于解決代碼復用的問題汗菜。
那 C 語言如何實現(xiàn)繼承呢?其實在 C 語言里面實現(xiàn)繼承也非常簡單挑社,只要把基類放到繼承類的數(shù)據(jù)成員中就行了陨界。
我們來實現(xiàn)一個 Square
“類”,添加一個邊長的“屬性”痛阻。
//Square.h
#include "Point.h"
typedef struct {
Point super;
int sideLen;
}Square;
int Square_getSideLen(Square const s);
void Square_init(Square *s, int x, int y, int sideLen);
//Square.c
#include "Square.h"
int Square_getSideLen(Square const s) {
return s.sideLen;
}
void Square_init(Square *s, int x, int y, int sideLen) {
//Point_init((Point*)s, x, y);
Point_init(&(s->super), x, y);
s->sideLen = sideLen;
}
實現(xiàn)完畢菌瘪,趕快來體驗一下。
//main.c
int main(int argc, const char * argv[]) {
Square s;
Square_init(&s, 10, 20, 10);
printf("square x:%d,y:%d,sideLen:%d\n", s.super.x, s.super.y, s.sideLen);
return 0;
}
控制臺輸出
square x:10,y:20,sideLen:10
可能會有人對上面的實現(xiàn)有疑問阱当,為什么 //Point_init((Point*)s, x, y);
這句也可以生效呢俏扩?那就要從結構體的內存布局說起了糜工。
//Point內存布局
┏━━━━━━┳━━━━━━┓
┃int x ┃int y ┃
┗━━━━━━┷━━━━━━┛
//Square內存布局
┏━━━━━━┳━━━━━━┳━━━━━━━━━━━┓
┃int x ┃int y ┃int sideLen┃
┗━━━━━━┷━━━━━━┷━━━━━━━━━━━┛
Square
的首個成員是 Point
,然后是邊長录淡,則其內存布局如上捌木。
因為有這樣的內存布局,所以你可以很安全的傳一個指向 Square
對象的指針到一個期望傳入 Point
對象的指針的函數(shù)中赁咙,就是一個函數(shù)的參數(shù)是 Point *
钮莲,你可以傳入 Square *
,并且這是非常安全的彼水。這樣的話崔拥,基類的所有屬性和方法都可以被繼承類繼承!
多態(tài)
多態(tài)是指子類可以替換父類凤覆,在實際的代碼運行過程中链瓦,調用子類的方法實現(xiàn)。多態(tài)可以提高代碼的擴展性和復用性盯桦,是很多設計模式慈俯、設計原則、編程技巧的代碼實現(xiàn)基礎拥峦。
我們在 Point
類中贴膘,添加方法列表 Operations
,然后在初始化時綁定 print
方法略号。同樣的刑峡,要實現(xiàn)多態(tài)的效果,我們需要在 Square
初始化時玄柠,替換原來的方法突梦。
//Point.h
struct Operations;
typedef struct{
int x;
int y;
struct Operations *op;
}Point;
struct Operations{
void (*print)(Point*);
};
int Point_getX(Point const p);
int Point_getY(Point const p);
void Point_init(Point *p, int x, int y);
//Point.c
#include "Point.h"
#include <stdio.h>
int Point_getX(Point const p) {
return p.x;
}
int Point_getY(Point const p) {
return p.y;
}
void Point_print(Point *p) {
printf("point x:%d,y:%d\n", Point_getX(*p), Point_getY(*p));
}
void Point_init(Point *p, int x, int y) {
p->x = x;
p->y = y;
static struct Operations op = {Point_print};
p->op = &op;
}
//Square
#include "Square.h"
#include <stdio.h>
int Square_getSideLen(Square const s) {
return s.sideLen;
}
void Square_print(Square *s) {
printf("square x:%d,y:%d,sideLen:%d\n", Point_getX((*s).super), Point_getY((*s).super), Square_getSideLen(*s));
}
void Square_init(Square *s, int x, int y, int sideLen) {
// Point_init((Point*)s, x, y);
Point_init(&(s->super), x, y);
s->sideLen = sideLen;
struct Operations op = {(void (*)(Point*))Square_print};
s->super.op = &op;
}
大功告成!試一下是否真的可以有多態(tài)的效果羽利。
int main(int argc, const char * argv[]) {
Point p;
Point_init(&p, 10, 20);
p.op->print(&p);
Square s;
Square_init(&s, 10, 20, 10);
Point* _s = (Point *)&s;
s.super.op->print(_s);
return 0;
}
控制臺輸出
point x:10,y:20
square x:10,y:20,sideLen:10