個人原創(chuàng)文章共屈,轉載請注明出處绑谣,謝謝合作
1 前言
在面向對象編程中,類是一個最基礎也是最核心的概念拗引,不嚴格的說借宵,一個類的組成就是數(shù)據(jù)加方法,數(shù)據(jù)可被隱藏起來寺擂,保證對外不可見暇务,然后通過暴露一組精心設計的方法對這些數(shù)據(jù)進行各種操作,由此從微觀到宏觀怔软,從局部到整體垦细,精細的實現(xiàn)簡單的接口耦合。面向對象帶來的好處不言而喻挡逼,而在C語言中括改,我們通過一些簡單的技巧,也可以將面向對象的一些基本特性進行落地家坎。
首先嘱能,static 關鍵字,在C語言中虱疏,作用于全局變量和函數(shù)惹骂,則意味著該全局變量或者函數(shù)只能在其聲明的文件中被使用,不能被其它文件使用做瞪,如果我們將一個 .c 源文件看作一個類的話对粪,static 關鍵字就起到了 private 的作用。
例如下述代碼装蓬,我們在 foo.c 中聲明了用 static 修飾的一個變量和一個函數(shù)著拭,分別是 n
和 foo
,然后聲明了一個非 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
蹂随,沒有 n
和 foo
爱榔,這正是 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)類,可以按照以下展示的范式來進行編寫设拟,可以通過定義 PRIVATE
和 PUBLIC
的宏來增加可讀性慨仿。
/* 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)與 new
和 delete
類似的形式吼和,如下所示,構造函數(shù)中可以通過 malloc
分配內存片仿,直接通過返回 surface_t
的指針來實現(xiàn)構造纹安,而析構函數(shù)中,則需要進行相應的 free
操作
/* surface.h */
surface_t* surface_create();
如果構造或者其它公有方法涉及多種帶參形態(tài)砂豌,即函數(shù)重載厢岂,那我們可以通過添加適當?shù)暮缶Y來進行函數(shù)名的區(qū)分,當然阳距,這也是在 C 語言中的無奈之舉塔粒,如下所示,后綴 ex
為 extended
的縮寫筐摘,是一種添加后綴的常用實踐卒茬。
/* 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ù)結構通過一個重定義為 handle
的 void*
指針暴露給用戶氓鄙,從而實現(xiàn)對內部數(shù)據(jù)結構的隱藏,此種形態(tài)业舍,在操作系統(tǒng)層面非常類同的普遍實踐抖拦,例如在 Windows
系統(tǒng)中,所有的內核對象在用戶態(tài)都以 HANDLE
類型的變量(即void*
)舷暮,稱之為句柄的概念來表示态罪,通過 CreateFile
、ReadFile
下面、WriteFile
复颈、DeviceIoControl
、CloseHandle
等函數(shù)來進行對象的創(chuàng)建诸狭、讀寫券膀、控制、銷毀驯遇。而相應的 Linux
平臺則通過簡單的 int 類型的變量芹彬,稱之為文件描述符的概念來標識內核對象, 通過open
叉庐、read
舒帮、write
、fcntl
、ioctl
玩郊、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)典實踐拂檩,以期拋磚引玉,共同進步嘲碧。