« 区分一个包含汉字的字符串是 UTF-8 还是 GBK | 返回首页 | 游戏多服务器架构的一点想法 »

C 语言中统一的函数指针

有时候,我们需要把多个模块粘合在一起。而这些模块的接口参数上有少许的不同。在 C 语言中,参数(或是返回值)不同的函数指针属于不同的类型,如果混用,编译器会警告你类型错误。

在 C 语言中,函数定义是可以不写参数的。比如:

void foo();

这个函数定义表示了一个返回 void 的函数,参数未定。也就是说,它是个弱类型,诸如:

void foo(int);

void foo(void *);

这些类型都可以无害的转换成它。正如在 C 语言中,具体的指针类型如 int * ,char * 都可以转换为 void * 一样。

注1:如果要严格定义一个无参数的函数,应该写成 void foo(void);

注2:如果有部分参数固定,而其后的参数可变,则定义看起来是这样: void foo(int , ...); 这表示第一个参数为 int ,从第 2 个参数开始可变。

不过,C 语言的这种语法,实际上不太使用。因为用 C 语言无法主动控制函数调用的参数压栈。我们很难根据程序的上下文来决定如何传入参数去调用某个函数。如果需要逐级传递多个函数的参数,用的更多的是 va_list

比如,你很难对 printf 做封装,通常为了方便做封装,还提供了形为 vprintf 的接口。

C++ 解决此类问题的方案是用类去模拟一个函数,通过重载 () 操作符的方法,让函数调用看起来和普通函数一致(并美其名曰 functor/仿函数)。当然,也有撕破语法糖的伪装,用更直白的类继承的方式来定义出接口。

这里想说的是,C 语言里也还有一种有趣的方案来在保证类型安全的基础上解决类似问题。

在 X-Window 的消息定义中就可以看到这样的手法。

在 Windows 的接口中,Windows 的消息携带的数据通常用两个参数来表示:WPARAM 和 LPARAM ,均为 32bit 整数。我们知道,消息本质上等同于对象的方法。在更早的面向对象语言如 smalltalk 中,调用对象的方法即被看成向对象发送一个消息。Windows 如此把所有消息处理相关函数的接口都以 WPARAM 与 LPARAM 的形式传递参数,正是为了方便统一其接口形式。各种五花八门的参数都蕴涵于这 64 bit 数据中。

Xlib 处理类似的问题,对 C 程序员的亲合力则大的多。至少更为类型安全。

Xlib 定义了一个叫做 XEvent 的结构体(实际是一个 union)。然后把各种可能的消息类型放在这个 union 中。例如,我想取键盘消息,则可以用 event.xkey.keycode 。

一般说来,我们可以把模块的对外接口看成是接收一组输入参数并加以处理。如果需要粘合多个不同的模块,他们需要处理不同的输入参数的话,可以借鉴 XLib 的这个方法。在粘合层定义一个 union ,把所有可能的参数组,每组定义成一个 struct 然后定义在同一个 union 中。这个粘合层的统一接口则为这个 union 指针。有必要的话,所有的参数组 struct 的头部都留下 type 字段。这样比较容易分发消息。

这样做的本质是:把函数调用时由编译生成的、将调用参数逐个压栈的代码,改由程序员主动填写(填写参数结构体)。利用结构的类型安全,保证了函数调用时的参数类型安全。再利用 union 的语法,把不同的参数组联合到一起变成同一类型。

给 api 传递一个 struct 或 union 指针而不是逐个参数传递,是 C 接口设计的一种常见手法。除了 XLib 的设计,还能找到很多耳熟能详的例子。例如,我们在 socket api 上也可以看到类似的东西。例如 connect 的参数中有一 sockaddr 结构,就适用于各种不同的网络底层协议。

Comments

C语言本来就有"统一的函数指针", 任何一种函数指针类型都是。 任何一种函数指针都可以转型为其他类型的函数指针, 当转型回来时, 与原函数指针相同。 类型安全也是完全一样。 即使是union, 存入什么, 也必须取出什么 —— 必须由程序员自己(或者用另外某个变量)记住究竟是什么类型。 取错了, 或者转型错了, union或者cast都完蛋。 union的扩展性反而不如函数指针好。 定义一个union也麻烦。 只是使用时少写一个转型比较方便而已。
分析得不错,学习了。
回去试试看...
学习了,()和(void)不一样啊
Test
最近正好看到C指针了,呵呵
浏览过,谢谢分享
装b要装成功
9楼是人渣中的人渣
9楼是人渣中的人渣
9楼的,互联网世界是平等的,也不压制言论,不要总把自己放在装或不装的状态。
楼主又来装B了
“并美其名曰 functor/仿函数”用不着这么偏激吧
云风,你的googlereader以后尽量少引用一些需要翻墙才能看的连接吧 每次都翻墙有点烦啊 不翻墙又会导致reader半分钟的无法阅读 唉 墙啊
int fun( const char* evName, void* arg );
"把各种可能的消息类型放在这个 union 中。" 这样用起来是很方便的,但似乎有个实际问题,大量的event类型必须写在同一个声明里(同一文件),导致一个巨大的结构,union声明;对于消息分类不很清晰;另一种方式是面向对象的消息类型继承,也是传统c/c++对消息处理的风格不同之处
好是好,就是要添加一些新类型的时候,全部源码都要重新编译过。
呵呵,SDL中的SDL_Event也是这么干的。
这样就有运行时类型信息了…… 有点像pyobject实现继承的作法
这个方法对静态的很好, 但有时即使是静态的, 也不方便在一个地方声明所有的可能结构体为一个 union 啊.

Post a comment

非这个主题相关的留言请到:留言本