面向对象编程不是银弹。大部分场合,我对面向对象的使用非常谨慎,能不用则不用。相关的讨论就不展开了。

但是,某些场合下,采用面向对象的确是比较好的方案。比如 UI 框架,又比如 3d 渲染引擎中的场景管理。C 语言对面向对象编程并没有原生支持,但没有原生支持并不等于不适合用 C 写面向对象程序。反而,我们对具体实现方式有更多的选择。

大部分用 C 写面向对象程序的程序员受 C++ 影响颇深。企图用宏模拟出一个常见 C++ 编译器已经实现的对象模型。于我愚见,这并不是一个好的方向。C++ 的对象模型,本质上是为了追求实现层的性能,并直接体现出来。就有如在 C++ 中被滥用的 inline ,的确有效,却破坏了分离原则。C++ 的继承是过紧的耦合。

我所理解的面向对象,是让不同的数据元有共同的操作方式,适合成组的处理。根据操作方式的不同,我们会对数据元做不同的分组。一个数据可能出现在这个组里,也可以出现在那个组里。这取决于你从不同的方面提取的共性。这些可供统一操作的共性称之为接口(Interface),接口在 C 语言中,表现为一组函数指针的集合。放在 C++ 中,即为虚表。

我所偏爱的面向对象实现方式(使用 C 语言)是这样的:

若有一组数据,我们需要让他们看起来都有一种叫作 foo 的共性。把符合这样的数据都称为 foo_object 。通常,我们会有如下 api 去操控 foo_object 。

struct foo_object;

struct foo_object * foo_create();
void foo_release(struct foo_object *);
void foo_dosomething(struct foo_object *);

在具体实现时,会在一个叫 foo.c 的实现文件中,定义出 foo_object 结构,里面有一些 foo_dosomething 所需的数据成员。

但是,以上还不能满足要求。因为,我们会有不同的数据,他们只是表现出 foo_object 某些方面的特性。对于不同的数据,它们在 dosomething 时,实际所做的操作也有所区别。这时,我们需要定义出一个接口,供 foo.c 内部使用。那么,以上的头文件就需要做一些修改,把接口 i_foo 的定义加进去,并修改 create 函数。

struct i_foo {
void (*foobar)(void *);
}; struct foo_object * foo_create(struct i_foo *iface, void *data);

这里稍做解释。i_foo 是供 foo_dosomething 内部使用的一组接口。构造 foo_object 时,我们把一个外部数据 data 和为 foo_object 相关特性定义出的 i_foo 接口捆绑在一起,传入构造函数 foo_create 。一般,我还会会每个符合foo_object 特性的对象实现一个方法来得到对应的 i_foo ,如:

struct foobar;

struct i_foo * foobar_foo(void);
struct foobar * foobar_create(void);
void foobar_release(struct foobar *);

创建一个 foo_object 对象的代码看起来是这样:

struct foobar *foobar = foobar_create();
struct foo_object * fobj = foo_create(foobar_foo() , foobar);

struct foo_object 的定义中,必然要记录 i_foo 的接口指针和 data 数据指针。从 C++ 的观点看,foo_object 是基类,它也会有一些基类成员和非虚的成员函数。具体的派生类在实现时,改写了虚表 i_foo 的内容(重载了虚函数)。data 数据是在对基类 foo_object 继承时扩展的数据成员。但,在这里,我们使用了组合的方式来扩展成员。这增加了一层间接性,但提供了更低的耦合。其中的优劣暂且不讨论了。

通常看起来会是这样:

struct foo_object {
struct i_foo * vtbl;
void * data;
void * others;
}; void
foo_dosomething(struct foo_object *fobj)
{
fobj->vtbl->foobar(fobj->data);
// do something else
}

此处还有另一个问题:data 的生命期该由谁来负责?

生命期管理是个很大的课题。也是大多数使用 C/C++ 开发的软件的复杂度重要来源。我个人倾向于把生命期管理独立出来解决。所以 foo_object 模块一般并不负责 data 的生命期管理。它只负责 struct foo_object 的资源释放。

自己经营自己,是我的 C 语言软件开发的观点之一。我倾向于采用混合语言编程来更好的解决这个问题。比如 C 和 Lua ,或者 C 和 C++ 。如果不采用混合语言编程,那么也可以在之后,增加一个同样用 C 语言编写的层次来管理。这个话题,留到下次来讲。

剥离出生命期管理,代码量可以减少很多,也不容易犯错误。

ps. C 语言是一个弱类型的语言。至少比 C++ 要弱一些。这表现在:

void * 在 C 语言中可以指代任意数据指针。你可以把任意数据指针赋值给一个 void * 变量,也可以把一个 void * 变量赋给特定的指针类型变量。(这在 C++ 中不推荐,并会被编译器警告)

C 语言中的函数指针也比较有趣。通常,不同类型的函数指针相互赋值是会引起编译器警告的(类型不同)。当然,我们可以用一个 void * 来解决问题。但有时候,我们期望让类型检查严格一些,至少我们不希望把一个数据指针赋值给一个函数指针。但希望编译器不要理会函数参数的差异。

在 C 语言中,void (*foo)() 可以被赋予任意返回 void 的函数指针。即,你可以把 void foobar(int) 的地址赋予前面的 foo 变量(这是由 C 标准的参数传递规则保证的)。

所以,在 C 语言编程中需要注意。如果你想定义一个不接受参数的函数,并让编译器帮你检查出那些错误的多传递了参数的语句。你必须在 .h 文件中严格定义 void foo(void) 以示 foo 函数不接受参数。

在传统的 C 语言中,对结构初始化需要非常小心。这里,我们的 i_foo 接口定义就使用了 C 里的结构。这需要非常谨慎小心。(没有 C++ 编译器帮你做这件事)

C99 新增加的语法增强了这点(在初始化结构时,可以不依赖次序,而写出成员的名字)。值得采用。

云风:我所偏爱的C语言面向对象编程范式的更多相关文章

  1. 零基础入门该如何实现C 语言面向对象编程(很有帮助)

    零基础如果更快更好的入门C语言,如何在枯燥的学习中找到属于自己的兴趣,如果把学习当成一种事务性的那以后的学习将会很难有更深入的进步,如果带着乐趣来完成学习那将越学越有意思这样才会让你有想要更深入学习的 ...

  2. Go语言的编程范式

    由于比较古怪的语言特性,感觉代码的封装性是一种不同的思路. 包管理的火热程度居然没有nodejs高,这是为什么 package form import ( "encoding/gob&quo ...

  3. R语言面向对象编程:S3和R6

    一.基于S3的面向对象编程 基于S3的面向对象编程是一种基于泛型函数(generic function)的实现方式. 1.S3函数的创建 S3对象组成:generic(generic FUN)+met ...

  4. C语言面向对象编程(五):单链表实现(转)

    这里实现的单链表,可以存储任意数据类型,支持增.删.改.查找.插入等基本操作.(本文提供的是完整代码,可能有些长.) 下面是头文件: #ifndef SLIST_H #define SLIST_H # ...

  5. Go语言基础之结构体(面向对象编程上)

    1 自定义类型和类型别名 1.1 自定义类型 Go语言中可以基于一些基本的数据类型,使用type关键字定义自定义类型的数据 . 自定义类型定义了一个全新的类型,该新类型具有基本数据类型的特性.自定义类 ...

  6. 转:云风skynet服务端框架研究

    转:  http://forthxu.com/blog/skynet.html skynet是云风编写的服务端底层管理框架,底层由C编写,配套lua作为脚本使用,可换python等其他脚本语言.sky ...

  7. 对云风 cstring 第二次解析

    前言 从明天起 关心粮食和蔬菜 我有一所房子 面朝大海 春暖花开 本文前提条件 1.了解 posix 线程 2.了解 原子操作 3.具备简单C基础,或者 你也敲一遍. 如果上面不太清楚,你可以翻看我以 ...

  8. 在Quick-cocos2dx中使用云风pbc解析Protocol Buffers,支持win、mac、ios、android

    本例主要介绍 如何将 pbc 集成到quick-cocos2dx框架中,让我们的cocos2dx客户端Lua拥有编解码Protocol Buffers能力. 参考: 云风pbc的用法: http:// ...

  9. 融云开发漫谈:你是否了解Go语言并发编程的第一要义?

    2007年诞生的Go语言,凭借其近C的执行性能和近解析型语言的开发效率,以及近乎完美的编译速度,席卷全球.Go语言相关书籍也如雨后春笋般涌现,前不久,一本名为<Go语言并发之道>的书籍被翻 ...

随机推荐

  1. Map的三种遍历

    import java.util.*;/*** Map的三种遍历方式* @author Administrator**/public class m {public static void main( ...

  2. 解决ScrollView 与ListView共存显示不完全的问题

    ScrollView与ListView共存会存在滚动的问题,并且ListView只显示一个半Item. 当ListView的高度设定一定的值时,ListView同样地会显示对应的高度的Item. 因此 ...

  3. 调用Android系统设置中的Intent

    开发Android软件时,常常需要打开系统设置或信息界面,来设置相关系统项或查看系统的相关信息,这时我们就可以使用以下语句来实现:(如打开“无线和网络设置”界面) Intent intent = ne ...

  4. POJ 1225 Substrings

    http://poj.org/problem?id=1226 题意:给定n个串.求一个最长的串,使得这个串或者其反串在每个串中都出现过? 思路:先在大串里面加入正反串,然后二分,判定即可. #incl ...

  5. 数据库范式(1NF 2NF 3NF BCNF)详解

    数据库的设计范式是数据库设计所需要满足的规范,满足这些规范的数据库是简洁的.结构明晰的,同时,不会发生插入(insert).删除(delete)和更新(update)操作异常.反之则是乱七八糟,不仅给 ...

  6. SDN,NFV

    CAPEX,capital expenditures  投资成本OPEX,Operating Expense 运营费用space & power consumption 图解NFV与SDN关系

  7. c语言二维数组变色龙之死字的打印

    1 #include <stdio.h> #include <stdlib.h> void main() { ][]= { {'#','#','#',' ','#','#',' ...

  8. Java中判断集合类为空的方法

    *****需要引入Spring的核心Jar包***** 工具类: org.springframework.util.CollectionUtils 方法: public static boolean ...

  9. 初识前端HTML

    HTML 超文本标记语言 HTML的解析 顾名思义,HTML就是由一个个的标签组成的,组成后,HTML可被浏览器直接识别以及处理成我们想给用户展示的样子. 下面我们就来解析HTML的一个个标签. &l ...

  10. 住javaWeb分页实现(模拟百度首页)

    本文来源于 http://blog.csdn.net/tjpu_lin/article/details/41050475 近期在开发一个项目,项目中有非常多数据展示的模块.所以要用到分页,网上搜了非常 ...