C/C++實踐:用C語言實現(xiàn)面向對象編程

個人原創(chuàng)文章共屈,轉載請注明出處绑谣,謝謝合作

1 前言

在面向對象編程中,類是一個最基礎也是最核心的概念拗引,不嚴格的說借宵,一個類的組成就是數(shù)據(jù)加方法,數(shù)據(jù)可被隱藏起來寺擂,保證對外不可見暇务,然后通過暴露一組精心設計的方法對這些數(shù)據(jù)進行各種操作,由此從微觀到宏觀怔软,從局部到整體垦细,精細的實現(xiàn)簡單的接口耦合。面向對象帶來的好處不言而喻挡逼,而在C語言中括改,我們通過一些簡單的技巧,也可以將面向對象的一些基本特性進行落地家坎。

首先嘱能,static 關鍵字,在C語言中虱疏,作用于全局變量和函數(shù)惹骂,則意味著該全局變量或者函數(shù)只能在其聲明的文件中被使用,不能被其它文件使用做瞪,如果我們將一個 .c 源文件看作一個類的話对粪,static 關鍵字就起到了 private 的作用。

例如下述代碼装蓬,我們在 foo.c 中聲明了用 static 修飾的一個變量和一個函數(shù)著拭,分別是 nfoo,然后聲明了一個非 static 變量 c牍帚,在 main.c 中引用了這三個變量儡遮,如下所示。


# renren @ ubuntu in /data/ooc_test [4:31:31]

$ cat foo.c

static int n = 0;

static int foo(int n) { return n; }

char c = 0;

# renren @ ubuntu in /data/ooc_test [4:31:33]

$ cat main.c

extern int n;

extern int foo(int);

extern char c;

int main()

{

foo(n + c);

return 0;

}

然后我們進行編譯暗赶,編譯結果如下所示鄙币,使用 nm 命令查看 foo.o 的符號,發(fā)現(xiàn)導出符號中只有 c蹂随,沒有 nfoo爱榔,這正是 static 在此種場景下的效用,而最后鏈接階段糙及,自然也因找不到符號而報錯。


# renren @ ubuntu in /data/ooc_test [4:31:36]

$ gcc -o foo.c

gcc: fatal error: no input files

compilation terminated.

# renren @ ubuntu in /data/ooc_test [4:33:45] C:1

$ gcc -c foo.c

# renren @ ubuntu in /data/ooc_test [4:33:51]

$ gcc -c main.c

# renren @ ubuntu in /data/ooc_test [4:33:55]

$ nm -g foo.o

0000000000000000 B c

# renren @ ubuntu in /data/ooc_test [4:34:00]

$ gcc -o test.out foo.o main.o

/usr/bin/ld: main.o: in function 'main':

main.c:(.text+0x14): undefined reference to 'n'

/usr/bin/ld: main.c:(.text+0x1d): undefined reference to 'foo'

collect2: error: ld returned 1 exit status

2 靜態(tài)類

對于靜態(tài)類而言筛欢,由于無需創(chuàng)建實例浸锨,因此唇聘,我們可以直接將成員變量以 static 修飾直接定義在 .c 文件中,私有方法同樣以 static 修飾柱搜,公有方法需對外暴露迟郎,則不用 static 修飾,并且在對應的 .h 文件中進行聲明聪蘸。

通常宪肖,我們以 init 作為靜態(tài)類的初始化函數(shù),以 dispose 作為靜態(tài)類的反初始化函數(shù)健爬,在命名方面控乾,成員變量以 m_ 作為變量名前綴,以 {類名}_{方法名} 來作為對外接口的規(guī)范娜遵,而私有方法可以以 __ 作為前綴蜕衡。

例如編寫一個 surface 的靜態(tài)類,可以按照以下展示的范式來進行編寫设拟,可以通過定義 PRIVATEPUBLIC 的宏來增加可讀性慨仿。


/* surface.c */

#define PRIVATE static

#define PUBLIC

/* 類的成員變量的定義 */

PRIVATE int    m_width = 0;

PRIVATE int    m_height = 0;

PRIVATE char*  m_panel = NULL;

PRIVATE int    m_pos_x = 0;

PRIVATE int    m_pos_y = 0;

/* 類的私有方法*/

PRIVATE int __init_pannel() { /*...*/ }

/* 靜態(tài)類的初始化 */

PUBLIC int surface_init(int width, int height){ /*...*/ }

/* 靜態(tài)類的反初始化 */

PUBLIC int surface_dispose() { /*...*/ }

PUBLIC int surface_get_width()  { /*...*/ }

PUBLIC int surface_set_width(int width)  { /*...*/ }

/*...*/

如此,.c 文件作為類的實現(xiàn)部分纳胧,而 .h 文件作為類的公有方法的聲明部分镰吆,.h 文件作為一個類對外暴露的接口和界面,務必限定其僅包含需要暴露的結構和方法跑慕,需要對外隱藏的信息都應避免在 .h 中聲明万皿,如下所示。


/* surface.h */

#ifndef SURFACE_H

#define SURFACE_H

int surface_init(int width, int height);

int surface_dispose();

int surface_get_width();

int surface_set_width(int width);

#endif

3 非靜態(tài)類

非靜態(tài)類意味著可以通過 new 關鍵字創(chuàng)建實例相赁,不嚴格的說相寇,同一個類的多個實例就是該類的成員變量的集合在堆上的多個副本,類的方法作用于各個實例各自的副本钮科,互不干擾唤衫。

為了實現(xiàn)多實例的創(chuàng)建,我們可以將類的成員變量以結構體的形式進行定義绵脯,實例的構造函數(shù)可以定義為 {類名}_create佳励,析構函數(shù)可以定義為 {類名}_destroy,例如蛆挫,第二章中的 surface 類赃承,我們改造為非靜態(tài)類如下。

頭文件 surface.h 如下所示:


/* surface.h */

typedef struct

{

  int    m_width;

  int    m_height;

  char*  m_panel;

  int    m_pos_x;

  int    m_pos_y; 

}surface_t;

int surface_create(surface_t* surf);

int surface_destroy(surface_t* surf);

int surface_get_width(surface_t* surf);

int surface_set_width(surface_t* surf, int width);

在上述的定義中悴侵,構造和析構均需要傳入 surface_t 的有效指針瞧剖,構造和析構函數(shù)并不負責 surface_t 這個結構體對象的內存分配和釋放,而是將該部分工作交由用戶來完成,用戶可以自由的選擇將 surface_t 定義為全局變量抓于,或者通過動態(tài)內存分配來創(chuàng)建做粤,或者如果僅限于一個函數(shù)內部使用,則可以直接定義為棧上的局部變量捉撮,而該類暴露的各種公有方法的第一個參數(shù)均是 surface_t 的指針怕品,這就充當了 C++ 中隱含的 this 指針的能力,而 POSIX pthread 的 mutex巾遭、condition variable 正是這種風格肉康。

進一步的,我們也可以將內存分配的能力合入構造和析構中灼舍,從而實現(xiàn)與 newdelete 類似的形式吼和,如下所示,構造函數(shù)中可以通過 malloc 分配內存片仿,直接通過返回 surface_t 的指針來實現(xiàn)構造纹安,而析構函數(shù)中,則需要進行相應的 free 操作


/* surface.h */

surface_t* surface_create();

如果構造或者其它公有方法涉及多種帶參形態(tài)砂豌,即函數(shù)重載厢岂,那我們可以通過添加適當?shù)暮缶Y來進行函數(shù)名的區(qū)分,當然阳距,這也是在 C 語言中的無奈之舉塔粒,如下所示,后綴 exextended 的縮寫筐摘,是一種添加后綴的常用實踐卒茬。


/* surface.h */

surface_t* surface_create();

surface_t* surface_create_ex(int width, int height);

基于上述,我們借助結構體對象實現(xiàn)了對類的實例化咖熟,而類的聚合或組合圃酵,即是簡單的將一個類的結構體聲明在另一個類的結構體之中的成員變量即可。

4 非靜態(tài)類進階

之前我們提到過我們定義類最重要的目的是隱藏無需暴露的信息馍管,將接口和界面盡可能的簡化郭赐,從而通過簡單的接口隔離實現(xiàn)更好的內聚和更低的耦合。而上一章節(jié)的實踐中將非靜態(tài)類的成員定義為結構體直接暴露給用戶确沸,顯然違背了我們的初衷捌锭,因此,我們可以有更好的辦法罗捎,即將結構體定義下放到 .c 文件中观谦,從而實現(xiàn)對調用者的隱藏,而調用者只需要持有一個抽象的實例指針即可桨菜。

針對之前的 surface 類的定義豁状,我們改造如下所示捉偏,我們定義一個 handle 指針來指向被隱藏的 surface_t 結構體,從而實現(xiàn)對數(shù)據(jù)結構的隱藏替蔬,而用戶持有的就是一個單純的對象指針告私。


/* surface.h */

typedef void* handle;

handle surface_create();

int surface_destroy(handle surf);

int surface_get_width(handle surf);

int surface_set_width(handle surf, int width);

在 .c 文件中,我們定義 surface_t 結構體承桥,在每一個成員函數(shù)中,第一步要做的就是將 handle 指針轉換為 surface_t 結構體指針根悼,而為了避免調用者傳入錯誤的對象凶异,我們可以通過一個簡單的簽名來對對象的合法性進行判別。如下所示挤巡,在此實踐方法中剩彬,任何一個類的實例,均為 handle 指針矿卑,而每一個類的結構體中均定義一個 m_sig 變量喉恋,在構造時賦值為唯一標記該類的一個數(shù)值,而類的公有方法和析構函數(shù)母廷,都首先默認檢查該簽名數(shù)值是否正確轻黑,以便確認調用者傳入的是正確的對象。


/* surface.c */

#include "surface.h"

#define SURFACE_SIG 0xfacedead

typedef struct

{

  int    m_sig;

  int    m_width;

  int    m_height;

  char*  m_panel;

  int    m_pos_x;

  int    m_pos_y; 

}surface_t;

handle surface_create() {

  surface_t* ptr = (surface_t*)malloc(sizeof(surface_t));

  if(NULL == ptr) {

    return NULL;

  }

  ptr->m_sig = SURFACE_SIG;

  /* ... */

  return (handle)ptr;

}

int surface_destroy(handle surf) {

  surface_t* ptr = (surface_t*)surf;

  if(ptr->sig != SURFACE_SIG) {

    return -1;

  }

  /* ... */

}

int surface_get_width(handle surf) {

  surface_t* ptr = (surface_t*)surf;

  if(ptr->sig != SURFACE_SIG) {

    return -1;

  }

  /* ... */

}

int surface_set_width(handle surf, int width) {

  surface_t* ptr = (surface_t*)surf;

  if(ptr->sig != SURFACE_SIG) {

    return -1;

  }

  /* ... */

}

在該實踐中琴昆,將一個復雜的數(shù)據(jù)結構通過一個重定義為 handlevoid* 指針暴露給用戶氓鄙,從而實現(xiàn)對內部數(shù)據(jù)結構的隱藏,此種形態(tài)业舍,在操作系統(tǒng)層面非常類同的普遍實踐抖拦,例如在 Windows 系統(tǒng)中,所有的內核對象在用戶態(tài)都以 HANDLE 類型的變量(即void*)舷暮,稱之為句柄的概念來表示态罪,通過 CreateFileReadFile下面、WriteFile复颈、DeviceIoControlCloseHandle等函數(shù)來進行對象的創(chuàng)建诸狭、讀寫券膀、控制、銷毀驯遇。而相應的 Linux 平臺則通過簡單的 int 類型的變量芹彬,稱之為文件描述符的概念來標識內核對象, 通過open叉庐、read舒帮、writefcntlioctl玩郊、close等函數(shù)來進行對象的創(chuàng)建肢执、讀寫、控制译红、銷毀预茄。而在操作系統(tǒng)的內核態(tài),無論是句柄還是文件描述符侦厚,都對應了一個由操作系統(tǒng)或者驅動程序所定義的跟設備相關的復雜數(shù)據(jù)結構耻陕。

5 多態(tài)

在 C 語言中要想實現(xiàn)多態(tài)是比較困難的,在此刨沦,我們就單繼承場景來探討一種典型的實踐诗宣。

基于上述章節(jié)中的實踐可以看出,定義類的成員函數(shù)時想诅,第一個參數(shù)必須為 this 指針召庞,以此才能將對象傳入函數(shù),以便函數(shù)中操作對象的成員變量来破。同樣篮灼,在多態(tài)的場景下,我們有一個前提讳癌,即無論是父對象還是子對象穿稳,傳入的 this 指針始終保持與當前對象變量一致,該前提在編程實操層面晌坤,也很合理逢艘,不容易出錯。

為了簡化表示骤菠,便于理解它改,下述例子中,我們就不再進行成員變量的隱藏和簽名驗證商乎。

還是以 surface 為例央拖,我們先聲明一個基類,該基類中包含了一個名為 draw 的函數(shù)指針鹉戚,我們定義它為虛函數(shù)鲜戒,然后,surface_draw 是對基類對該虛函數(shù)的實現(xiàn)抹凳,通過構造函數(shù) surface_create 完成對基類實例的初始化和函數(shù)的綁定遏餐。


#include <stdio.h>

#define VIRTUAL

typedef void* handle;

typedef struct

{

  int    m_width;

  int    m_height;

VIRTUAL int (*draw)(handle);

}surface_t;

int surface_draw(handle h)

{

surface_t* p = (surface_t*)h;

printf("base draw : w %d, h %d\n", p->m_width, p->m_height);

return 0;

}

int surface_create(surface_t* surf)

{

surf->m_width = 1024;

surf->m_height = 768;

surf->draw = surface_draw;

return 0;

}

緊接著,我們實現(xiàn)一個 surface 的派生類 mirror_surface赢底,該派生類的結構體中首先聲明了基類的實體失都,然后派生了一個 m_color 的成員柏蘑,再次聲明了 draw 的方法,然后 mirror_surface_draw 是派生類對 draw 的實現(xiàn)粹庞,在派生類的構造函數(shù) mirror_surface_create 中咳焚,首先調用基類的構造方法對 super 進行初始化,然后再對派生成員進行初始化庞溜,最后革半,將基類的 draw 指針和派生類的 draw 指針均指向了派生類的 mirror_surface_draw 方法,由此實現(xiàn)派生類實體對基類虛函數(shù)的覆蓋流码。


typedef struct

{

surface_t super;

int m_color;

VIRTUAL int (*draw)(handle);

}mirror_surface_t;

int mirror_surface_draw(handle h)

{

mirror_surface_t* p = (mirror_surface_t*)h;

printf("mirror draw : w %d, h %d, c %d\n",

p->super.m_width, p->super.m_height, p->m_color);

return 0;

}

int mirror_surface_create(surface_t* surf)

{

surface_create(&surf->super);

surf->m_color = 0xff;

surf->draw = mirror_surface_draw;

surf->super.draw = surf->draw;

return 0;

}

基于上述首先督惰,我們編寫測試案例如下:


int main()

{

surface_t base;

mirror_surface_t derived;

surface_t* base_ptr;

surface_create(&base);

mirror_surface_create(&derived);

base_ptr = (surface_t*)&derived;

base.draw(&base);

derived.draw(&derived);

base_ptr->draw(base_ptr);

return 0;

}

編譯后,輸出結果如下所示:


# renren @ ubuntu in /data/ooc_test [7:25:10]

$ ./test.out

base draw : w 1024, h 768

mirror draw : w 1024, h 768, c 255

mirror draw : w 1024, h 768, c 255

在上述的測試案例中旅掂,我們可以看到,基類的實體調用基類的函數(shù)访娶,派生類的實體調用派生類的函數(shù)商虐,指向派生類實體的基類指針,同樣能夠引用到派生類的變量崖疤,調用的是派生類的函數(shù)秘车。

當基類指針 base_ptr 指向派生類實體后,base_ptr 調用的 draw 函數(shù)所引用的仍然是基類的 draw 指針劫哼,而由于派生類的構造中已經(jīng)將基類的 draw 指針改寫為指向派生類的函數(shù)叮趴,因此,基類指針仍然能夠實現(xiàn)對派生類函數(shù)的調用权烧,從而實現(xiàn)了多態(tài)特性中眯亦,派生類對基類虛函數(shù)的覆蓋。

另一方面般码,由于基類是以實體形式直接聲明在派生類的結構體之中妻率,并且,基類的實體是派生類結構體中的第一個成員變量板祝,因此宫静,派生類中的基類的地址和派生類結構體的地址,是同一個地址券时,不存在偏移量孤里,基于此,派生類實體無論是賦值給派生類指針還是基類指針橘洞,都是等價的捌袜,將基類實體聲明為派生類的第一個成員變量,這是能夠實現(xiàn)多態(tài)的一個關鍵技巧震檩。

在上述的例子中琢蛤,我們展示了基類虛函數(shù)被派生類重寫的實現(xiàn)方式蜓堕,如果基類需要定義純虛函數(shù),與該場景的實現(xiàn)完全一致博其,只是基類不需要對定義的純虛函數(shù)進行實現(xiàn)套才,基類構造函數(shù)中將函數(shù)指針賦值為 NULL 即可,派生類中類似慕淡,除了將自身的函數(shù)指針賦值為對應的函數(shù)外背伴,還需要將基類的函數(shù)指針賦值即可。

6 總結

經(jīng)過這些年的實踐峰髓,無論早期編寫 Windows 內核驅動傻寂、日志文件系統(tǒng)以及在 Linux 內核上進行一些功能擴展,抑或近些年來在用戶態(tài)上進行一些基礎軟件的設計和開發(fā)携兵,無論是基于純 C 語言的環(huán)境或者是 C/C++ 混合使用疾掰,我們都一直堅持適當?shù)倪\用 Object Oriented in C 的方法來進行底層代碼的組織和構建,這已經(jīng)成為了我們的一種標準的實踐徐紧,基于以功能內聚為核心的分層結構的設計和模塊劃分静檬,讓 C 語言的代碼變得簡單易懂且充滿美感。

后續(xù)我抽時間將持續(xù)輸出一系列文章并级,講述我們這些年在 C/C++ 方面的一些經(jīng)典實踐拂檩,以期拋磚引玉,共同進步嘲碧。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末稻励,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子愈涩,更是在濱河造成了極大的恐慌望抽,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,816評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件钠署,死亡現(xiàn)場離奇詭異糠聪,居然都是意外死亡,警方通過查閱死者的電腦和手機谐鼎,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,729評論 3 385
  • 文/潘曉璐 我一進店門舰蟆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人狸棍,你說我怎么就攤上這事身害。” “怎么了草戈?”我有些...
    開封第一講書人閱讀 158,300評論 0 348
  • 文/不壞的土叔 我叫張陵塌鸯,是天一觀的道長。 經(jīng)常有香客問我唐片,道長丙猬,這世上最難降的妖魔是什么涨颜? 我笑而不...
    開封第一講書人閱讀 56,780評論 1 285
  • 正文 為了忘掉前任,我火速辦了婚禮茧球,結果婚禮上庭瑰,老公的妹妹穿的比我還像新娘。我一直安慰自己抢埋,他們只是感情好弹灭,可當我...
    茶點故事閱讀 65,890評論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著揪垄,像睡著了一般穷吮。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上饥努,一...
    開封第一講書人閱讀 50,084評論 1 291
  • 那天捡鱼,我揣著相機與錄音,去河邊找鬼酷愧。 笑死堰汉,一個胖子當著我的面吹牛,可吹牛的內容都是我干的伟墙。 我是一名探鬼主播,決...
    沈念sama閱讀 39,151評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼滴铅,長吁一口氣:“原來是場噩夢啊……” “哼戳葵!你這毒婦竟也來了?” 一聲冷哼從身側響起汉匙,我...
    開封第一講書人閱讀 37,912評論 0 268
  • 序言:老撾萬榮一對情侶失蹤拱烁,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后噩翠,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體戏自,經(jīng)...
    沈念sama閱讀 44,355評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,666評論 2 327
  • 正文 我和宋清朗相戀三年伤锚,在試婚紗的時候發(fā)現(xiàn)自己被綠了擅笔。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,809評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡屯援,死狀恐怖猛们,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情狞洋,我是刑警寧澤弯淘,帶...
    沈念sama閱讀 34,504評論 4 334
  • 正文 年R本政府宣布,位于F島的核電站吉懊,受9級特大地震影響庐橙,放射性物質發(fā)生泄漏假勿。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 40,150評論 3 317
  • 文/蒙蒙 一态鳖、第九天 我趴在偏房一處隱蔽的房頂上張望转培。 院中可真熱鬧,春花似錦郁惜、人聲如沸堡距。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽羽戒。三九已至,卻和暖如春虎韵,著一層夾襖步出監(jiān)牢的瞬間易稠,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,121評論 1 267
  • 我被黑心中介騙來泰國打工包蓝, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留驶社,地道東北人。 一個月前我還...
    沈念sama閱讀 46,628評論 2 362
  • 正文 我出身青樓测萎,卻偏偏與公主長得像亡电,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子硅瞧,可洞房花燭夜當晚...
    茶點故事閱讀 43,724評論 2 351

推薦閱讀更多精彩內容