C/C++实践:用C语言实现面向对象编程

个人原创文章,转载请注明出处,谢谢合作

1 前言

在面向对象编程中,类是一个最基础也是最核心的概念,不严格的说,一个类的组成就是数据加方法,数据可被隐藏起来,保证对外不可见,然后通过暴露一组精心设计的方法对这些数据进行各种操作,由此从微观到宏观,从局部到整体,精细的实现简单的接口耦合。面向对象带来的好处不言而喻,而在C语言中,我们通过一些简单的技巧,也可以将面向对象的一些基本特性进行落地。

首先,static 关键字,在C语言中,作用于全局变量和函数,则意味着该全局变量或者函数只能在其声明的文件中被使用,不能被其它文件使用,如果我们将一个 .c 源文件看作一个类的话,static 关键字就起到了 private 的作用。

例如下述代码,我们在 foo.c 中声明了用 static 修饰的一个变量和一个函数,分别是 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 的符号,发现导出符号中只有 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 静态类

对于静态类而言,由于无需创建实例,因此,我们可以直接将成员变量以 static 修饰直接定义在 .c 文件中,私有方法同样以 static 修饰,公有方法需对外暴露,则不用 static 修饰,并且在对应的 .h 文件中进行声明。

通常,我们以 init 作为静态类的初始化函数,以 dispose 作为静态类的反初始化函数,在命名方面,成员变量以 m_ 作为变量名前缀,以 {类名}_{方法名} 来作为对外接口的规范,而私有方法可以以 __ 作为前缀。

例如编写一个 surface 的静态类,可以按照以下展示的范式来进行编写,可以通过定义 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() { /*...*/ }

/* 静态类的初始化 */

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

/* 静态类的反初始化 */

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

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

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

/*...*/

如此,.c 文件作为类的实现部分,而 .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 非静态类

非静态类意味着可以通过 new 关键字创建实例,不严格的说,同一个类的多个实例就是该类的成员变量的集合在堆上的多个副本,类的方法作用于各个实例各自的副本,互不干扰。

为了实现多实例的创建,我们可以将类的成员变量以结构体的形式进行定义,实例的构造函数可以定义为 {类名}_create,析构函数可以定义为 {类名}_destroy,例如,第二章中的 surface 类,我们改造为非静态类如下。

头文件 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 的有效指针,构造和析构函数并不负责 surface_t 这个结构体对象的内存分配和释放,而是将该部分工作交由用户来完成,用户可以自由的选择将 surface_t 定义为全局变量,或者通过动态内存分配来创建,或者如果仅限于一个函数内部使用,则可以直接定义为栈上的局部变量,而该类暴露的各种公有方法的第一个参数均是 surface_t 的指针,这就充当了 C++ 中隐含的 this 指针的能力,而 POSIX pthread 的 mutex、condition variable 正是这种风格。

进一步的,我们也可以将内存分配的能力合入构造和析构中,从而实现与 newdelete 类似的形式,如下所示,构造函数中可以通过 malloc 分配内存,直接通过返回 surface_t 的指针来实现构造,而析构函数中,则需要进行相应的 free 操作


/* surface.h */

surface_t* surface_create();

如果构造或者其它公有方法涉及多种带参形态,即函数重载,那我们可以通过添加适当的后缀来进行函数名的区分,当然,这也是在 C 语言中的无奈之举,如下所示,后缀 exextended 的缩写,是一种添加后缀的常用实践。


/* surface.h */

surface_t* surface_create();

surface_t* surface_create_ex(int width, int height);

基于上述,我们借助结构体对象实现了对类的实例化,而类的聚合或组合,即是简单的将一个类的结构体声明在另一个类的结构体之中的成员变量即可。

4 非静态类进阶

之前我们提到过我们定义类最重要的目的是隐藏无需暴露的信息,将接口和界面尽可能的简化,从而通过简单的接口隔离实现更好的内聚和更低的耦合。而上一章节的实践中将非静态类的成员定义为结构体直接暴露给用户,显然违背了我们的初衷,因此,我们可以有更好的办法,即将结构体定义下放到 .c 文件中,从而实现对调用者的隐藏,而调用者只需要持有一个抽象的实例指针即可。

针对之前的 surface 类的定义,我们改造如下所示,我们定义一个 handle 指针来指向被隐藏的 surface_t 结构体,从而实现对数据结构的隐藏,而用户持有的就是一个单纯的对象指针。


/* 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 结构体,在每一个成员函数中,第一步要做的就是将 handle 指针转换为 surface_t 结构体指针,而为了避免调用者传入错误的对象,我们可以通过一个简单的签名来对对象的合法性进行判别。如下所示,在此实践方法中,任何一个类的实例,均为 handle 指针,而每一个类的结构体中均定义一个 m_sig 变量,在构造时赋值为唯一标记该类的一个数值,而类的公有方法和析构函数,都首先默认检查该签名数值是否正确,以便确认调用者传入的是正确的对象。


/* 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;

  }

  /* ... */

}

在该实践中,将一个复杂的数据结构通过一个重定义为 handlevoid* 指针暴露给用户,从而实现对内部数据结构的隐藏,此种形态,在操作系统层面非常类同的普遍实践,例如在 Windows 系统中,所有的内核对象在用户态都以 HANDLE 类型的变量(即void*),称之为句柄的概念来表示,通过 CreateFileReadFileWriteFileDeviceIoControlCloseHandle等函数来进行对象的创建、读写、控制、销毁。而相应的 Linux 平台则通过简单的 int 类型的变量,称之为文件描述符的概念来标识内核对象, 通过openreadwritefcntlioctlclose等函数来进行对象的创建、读写、控制、销毁。而在操作系统的内核态,无论是句柄还是文件描述符,都对应了一个由操作系统或者驱动程序所定义的跟设备相关的复杂数据结构。

5 多态

在 C 语言中要想实现多态是比较困难的,在此,我们就单继承场景来探讨一种典型的实践。

基于上述章节中的实践可以看出,定义类的成员函数时,第一个参数必须为 this 指针,以此才能将对象传入函数,以便函数中操作对象的成员变量。同样,在多态的场景下,我们有一个前提,即无论是父对象还是子对象,传入的 this 指针始终保持与当前对象变量一致,该前提在编程实操层面,也很合理,不容易出错。

为了简化表示,便于理解,下述例子中,我们就不再进行成员变量的隐藏和签名验证。

还是以 surface 为例,我们先声明一个基类,该基类中包含了一个名为 draw 的函数指针,我们定义它为虚函数,然后,surface_draw 是对基类对该虚函数的实现,通过构造函数 surface_create 完成对基类实例的初始化和函数的绑定。


#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;

}

紧接着,我们实现一个 surface 的派生类 mirror_surface,该派生类的结构体中首先声明了基类的实体,然后派生了一个 m_color 的成员,再次声明了 draw 的方法,然后 mirror_surface_draw 是派生类对 draw 的实现,在派生类的构造函数 mirror_surface_create 中,首先调用基类的构造方法对 super 进行初始化,然后再对派生成员进行初始化,最后,将基类的 draw 指针和派生类的 draw 指针均指向了派生类的 mirror_surface_draw 方法,由此实现派生类实体对基类虚函数的覆盖。


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

在上述的测试案例中,我们可以看到,基类的实体调用基类的函数,派生类的实体调用派生类的函数,指向派生类实体的基类指针,同样能够引用到派生类的变量,调用的是派生类的函数。

当基类指针 base_ptr 指向派生类实体后,base_ptr 调用的 draw 函数所引用的仍然是基类的 draw 指针,而由于派生类的构造中已经将基类的 draw 指针改写为指向派生类的函数,因此,基类指针仍然能够实现对派生类函数的调用,从而实现了多态特性中,派生类对基类虚函数的覆盖。

另一方面,由于基类是以实体形式直接声明在派生类的结构体之中,并且,基类的实体是派生类结构体中的第一个成员变量,因此,派生类中的基类的地址和派生类结构体的地址,是同一个地址,不存在偏移量,基于此,派生类实体无论是赋值给派生类指针还是基类指针,都是等价的,将基类实体声明为派生类的第一个成员变量,这是能够实现多态的一个关键技巧。

在上述的例子中,我们展示了基类虚函数被派生类重写的实现方式,如果基类需要定义纯虚函数,与该场景的实现完全一致,只是基类不需要对定义的纯虚函数进行实现,基类构造函数中将函数指针赋值为 NULL 即可,派生类中类似,除了将自身的函数指针赋值为对应的函数外,还需要将基类的函数指针赋值即可。

6 总结

经过这些年的实践,无论早期编写 Windows 内核驱动、日志文件系统以及在 Linux 内核上进行一些功能扩展,抑或近些年来在用户态上进行一些基础软件的设计和开发,无论是基于纯 C 语言的环境或者是 C/C++ 混合使用,我们都一直坚持适当的运用 Object Oriented in C 的方法来进行底层代码的组织和构建,这已经成为了我们的一种标准的实践,基于以功能内聚为核心的分层结构的设计和模块划分,让 C 语言的代码变得简单易懂且充满美感。

后续我抽时间将持续输出一系列文章,讲述我们这些年在 C/C++ 方面的一些经典实践,以期抛砖引玉,共同进步。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,997评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,603评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,359评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,309评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,346评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,258评论 1 300
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,122评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,970评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,403评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,596评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,769评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,464评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,075评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,705评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,848评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,831评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,678评论 2 354

推荐阅读更多精彩内容