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

但是,某些场合下,采用面向对象的确是比较好的方案。比如 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. 开始慢慢学习这本书了。。Python编程实战:运用设计模式、并发和程序库创建高质量程序

    没办法,不到设计模式,算法组合这些,在写大一点程序的时候,总是力不从心...:( 一开始可能要花很多时间来慢慢理解吧,,这毕竟和<大话设计模式>用的C#语言有点不太一样... 书上代码是3 ...

  2. python的工作记录B

    这次很强了.就差最后一步,判断一下SVN的版本是否是真的库里的. #!/usr/bin/env python # -*- coding: utf-8 -*- import os,sys,command ...

  3. LeetCode_Best Time to Buy and Sell Stock III

    Say you have an array for which the ith element is the price of a given stock on day i. Design an al ...

  4. jQuery 弹出div层

    目的:使用jQuery弹出一个div窗口:这种效果经常应用于页面内容的展示,登录效果的实现.其实,实现这种效果有好多种方式: 效果如下: 代码如下: <html> <head> ...

  5. 深入理解linux网络技术内幕读书笔记(一)--简介

    Table of Contents 1 基本术语 1.1 本书常用的缩写 2 引用计数 2.1 引用计数函数 3 垃圾回收 3.1 异步 3.2 同步 4 函数指针 4.1 缺点 5 goto语句 5 ...

  6. js bom中浏览器兼容问题判断代码

    var btn = document.getElementById('d1');if(addEventListener===undefined){ btn.attachEvent('onclick', ...

  7. mysql 1449 : The user specified as a definer (&#39;root&#39;@&#39;%&#39;) does not exist 解决方法

    权限问题,授权 给 root  全部sql 权限 mysql> grant all privileges on *.* to root@"%" identified by & ...

  8. swing入门例子

    // a simple exmple that can show the basis of swing------------------------------------------------- ...

  9. LRU Cache的简单c++实现

    什么是 LRU LRU Cache是一个Cache的置换算法,含义是“最近最少使用”,把满足“最近最少使用”的数据从Cache中剔除出去,并且保证Cache中第一个数据是最近刚刚访问的,因为这样的数据 ...

  10. JavaScript总结(二) 系统分析

    学习Javascript的时候.遇到了一个问题. 看了牛腩的视频,感觉讲的太简单和基础,像认识一位新朋友.但仅有一面之缘就结束了. 再看姜浩的视频.又看得迷迷糊糊,让我找到了当初看两位台湾专家讲VB. ...