浅谈 C 语言中模块化设计的范式

今天继续谈模块化的问题。这个想慢慢写成个系列,但是不一定连续写。基本是想起来了,就整理点思路出来。主要还是为以后集中整理做点铺垫。

我们都知道,层次分明的代码最容易维护。你可以轻易的换掉某个层次上的某个模块,而不用担心对整个系统造成很大的副作用。

层次不清的设计中,最糟糕的一种是模块循环依赖。即,分不清两个模块谁在上,谁在下。这个时候,最容易牵扯不清,其结果往往是把两者看做一体去维护算了。这里面还涉及一些初始化次序等繁杂的细节。

其次,就是越层的模块联系。当模块 A 是模块 B 的上层,而模块 B 又是模块 C 的上层,这个时候,让模块 C 对模块 A 可见,在模块 A 中有对 C 导出接口的直接调用,对于清晰的设计是很忌讳的一件事。虽然,我们很难完全避免这个问题,去让 A 对 C 的调用完全通过 B 。但通常应尽力为之。(注:以后写书的话,我争取补充一些实际的例子来说明)不过,对语言不原生支持的数据类型,以及基础设施,但却有必要创造出来给系统用的。可以有些例外。比如内存管理,log 管理,字符串(C 语言用原始库函数管理比较麻烦)等等,我们可能以基础模块的形式提供。但却可能被不同层次的模块直接使用。但,上到一定层次后,还是需要去隐藏它们的。

下面来一点更实际的分析。

以 C 语言为例,由于 C 语言缺乏 namespace 的原生支持,我们通常给 api 加上统一前缀来区分。这倒也不麻烦。

那么模块 A 看起来就是一堆 ‘A_xxxxx‘ 为名字的方法。我个人主张单个模块不宜过大,在实现时适合放在同一个 .c 文件里即可。通常,一个模块会围绕一类对象处理。这些对象可以用整数 handle 来表示,也可以用一个特定类型的对象指针。两种方案各有千秋。先来谈对象指针的方案。

一个模块 A 的接口描述文件很可以是这样的(希望以后能补上更现实的代码):

 1 #ifndef _A_h
 2 #define _A_h
 3
 4 struct A;
 5 struct B;
 6
 7 struct A* A_create(void);
 8 void A_release(struct A *self);
 9 void A_bind(struct A *self , struct B *b);
10 void A_commit(struct A *self);
11 void A_update(void);
12
13 int A_init(void);
14
15 #endif

这里,我们定义了 A 这种数据类型。我个人反对用 typedef 或宏来减少代码输入。除非有特别的理由,都写上 struct 前缀,而不是定义出新类型。尤其是在较底层的模块设计时更是如此。在接口描述时,struct A 的细节是绝对不应该暴露出来的,它的数据结构应该仅存在于实现的文件 a.c 中。

关于 A 的接口通常分两类,一类是对 struct A* 做一些处理的,那么就让第一个参数传入 self 指针。这相当于 C++ 的 this 指针。比如上例中的 A_commit ;另一类接近于 C++ 类的静态成员函数,通常用于对这一类对象全部做一个处理,如 A_update 。

注:我无意用 C 去模拟 C++ ,但基于一类数据类型做一些处理的方法,对于 C ,这样的写法也是一个常规的范式而已。至于面向对象等在构建复杂系统时常用到的方法,以后我会谈谈我自己常用的另一些范式。或许像 C++ ,也可以不像。怎么写更好,是个见任见智的问题。不用过于拘泥。

这里的例子中,我们还提到了另一个数据类型 B 。显然,它是放在 B 模块中的。

我们通常不会在 a.h 中去 include b.h ,而只是声明一下 struct B 。(对于 C 语言来说,这并不必要,但写上是个好习惯)。这是因为,如果 B 是位于 A 之下的模块,既在 A 模块的实现中,会用到 B 的方法,我们通常不会让用到 A 模块的人,可以看见 B 的接口。包含 a.h 的同时隐式包含 b.h 就是不必要的了。

从范例代码中,我们可以猜想,struct A 是对 struct B 的某种封装,可以通过对 A 的操作,间接操作到其中的 B 类型。在 A 的模块初始化 A_init 中一定就会初始化 B 了。如果是这样,B 的层次就位于 A 之下。

往往 struct B 中还会保留一个 struct A 类型的引用。首先,我们应该尽力避免这种情况。即:位于下层的 B 应该对上层的 A 一无所知是最好的。如果在 B 模块中必须出现 struct A,那么我们应该至少保证,仅仅是 struct A * ,一个引用,而绝对不能出现任何对 A 模块内接口的调用。不要认为使用巧妙的方法,绕过循环依赖初始化问题就够了。这应该是一个设计原则,不要去违反。

btw, 草率的接口设计往往是日后系统脆弱的根源。图一时之快,随意暴露一些接口,或是自以为聪明的用一些“巧妙”的方法,甚至是语法糖来绕过设计原则,都是很危险的。

一个常见的难处理的问题是:如果 struct A 和 struct B 相互有双向引用。怎样建立这个引用关系?这个建立的过程,到底是 A 的方法,还是 B 的方法?我的答案是,谁在上层,就是谁的方法。

但是 A 和 B 相互都看不见内部数据布局的细节,让 B 的内部对 A 类型做一个引用,比如也需要从 B 模块中暴露一个接口出来。这个接口,可能仅供 A 使用。在这个例子里,就是仅供 A_bind 这个方法去使用。

如果是 C++ ,我们或许会采用 friend 。也可能使用其它一些技巧。反正 C++ 里可以挖掘的语法太多了。但 C 怎么办?下面给个我自己的方案。

原本,我们在 B 中导出的 api 是这样的:

void B_set_A(struct B *self,struct A * a);

现在写成:

struct i_A;

void B_set_A(struct B *self,struct i_A *a);

在 b.c 的实现中,加一个函数用于 struct i_A * 到 struct A * 的转换。

static inline struct A * A(struct i_A *a) { return (struct A *)a; }

然后在 a.c 的实现中,加一个类似函数用于转换 struct A * 到 struct i_A * 。

这样,在 a.c 之外,其它模块因为不能得到任何 struct i_A 类型,而不会错误的使用 B_set_A 这个接口了。

原文链接:http://blog.codingnow.com/2010/01/modularization_in_c_1.html

时间: 2024-10-10 14:04:23

浅谈 C 语言中模块化设计的范式的相关文章

浅谈C语言中的联合体(转载)

联合体union 当多个数据需要共享内存或者多个数据每次只取其一时,可以利用联合体(union).在C Programming Language 一书中对于联合体是这么描述的: 1)联合体是一个结构: 2)它的所有成员相对于基地址的偏移量都为0: 3)此结构空间要大到足够容纳最"宽"的成员: 4)其对齐方式要适合其中所有的成员: 下面解释这四条描述: 由于联合体中的所有成员是共享一段内存的,因此每个成员的存放首地址相对于于联合体变量的基地址的偏移量为0,即所有成员的首地址都是一样的.为

浅谈C语言中的强符号、弱符号、强引用和弱引用

摘自http://www.jb51.net/article/56924.htm 浅谈C语言中的强符号.弱符号.强引用和弱引用 投稿:hebedich 字体:[增加 减小] 类型:转载 时间:2014-10-31 我要评论 这篇文章主要介绍了C语言中的强符号.弱符号.强引用和弱引用的定义及相关内容,非常的简单易懂,有需要的朋友可以参考下 首先我表示很悲剧,在看<程序员的自我修养--链接.装载与库>之前我竟不知道C有强符号.弱符号.强引用和弱引用.在看到3.5.5节弱符号和强符号时,我感觉有些困惑

浅谈c语言中的堆

操作系统堆管理器管理: 堆管理器是操作系统的一个模块,堆管理内存分配灵活,按需分配. 大块内存: 堆内存管理者总量很大的操作系统内存块,各进程可以按需申请使用,使用完释放. 程序手动申请&释放: 手工意思是需要写代码去申请malloc和释放free. 脏内存: 堆内存也是反复使用的,而且使用者用完释放前不会清除,因此也是脏的. 临时性: 堆内存只在malloc和free之间属于我这个进程,而可以访问.在malloc之前和free之后      都不能再访问,否则会有不可预料的后果. 堆内存使用范

浅谈C语言中的联合体

联合体union 当多个数据需要共享内存或者多个数据每次只取其一时,可以利用联合体(union).在C Programming Language 一书中对于联合体是这么描述的: 1)联合体是一个结构: 2)它的所有成员相对于基地址的偏移量都为0: 3)此结构空间要大到足够容纳最"宽"的成员: 4)其对齐方式要适合其中所有的成员: 下面解释这四条描述: 由于联合体中的所有成员是共享一段内存的,因此每个成员的存放首地址相对于于联合体变量的基地址的偏移量为0,即所有成员的首地址都是一样的.为

浅谈C#语言中的各种数据类型,与数据类型之间的转换

什么是数据类型? 数据类型,百度百科是这样解释的:数据类型在数据结构中的定义是一个值的集合以及定义在这个值集上的一组操作.这样的解释对于一个初学者来说未必太过于深奥. 简单点说,数据类型就是不同长度的数据的归类.数据类型的出现就是为了解决计算机中对不同长度的数据合理保存的问题.为了把数据分成所需内存大小不同的数据,编程的时候需要用大数据的时候才需要申请大内存,就可以充分利用内存.达到资源合理优化利用,减少浪费内存的目的. 数据类型有哪些? 类型            描述            

浅谈C语言中结构体的初始化

转自:http://www.jb51.net/article/37246.htm <代码大全>建议在变量定义的时候进行初始化,但是很多人,特别是新人对结构体或者结构体数组定义是一般不会初始化,或者不知道怎么初始化.1.初始化 复制代码代码如下: typedef struct _TEST_T {        int i;        char c[10];}TEST_T;TEST_T gst  = {1, “12345”};//可以初始化,设置i为1,s为一个字符串.TEST_T gst 

转: 浅谈C/C++中的指针和数组(二)

转自:http://www.cnblogs.com/dolphin0520/archive/2011/11/09/2242419.html 浅谈C/C++中的指针和数组(二) 前面已经讨论了指针和数组的一些区别,然而在某些情况下,指针和数组是等同的,下面讨论一下什么时候指针和数组是相同的. C语言标准对此作了说明: 规则1:表达式中的数组名被编译器当做一个指向该数组第一个元素的指针: 注:下面几种情况例外 1)数组名作为sizeof的操作数 2)使用&取数组的地址 规则2:下标总是与指针的偏移量

浅谈深度学习中潜藏的稀疏表达

浅谈深度学习中潜藏的稀疏表达 “王杨卢骆当时体,轻薄为文哂未休. 尔曹身与名俱灭,不废江河万古流.” — 唐 杜甫<戏为六绝句>(其二) [不要为我为啥放这首在开头,千人千面千理解吧] 深度学习:概述和一孔之见 深度学习(DL),或说深度神经网络(DNN), 作为传统机器学习中神经网络(NN).感知机(perceptron)模型的扩展延伸,正掀起铺天盖地的热潮.DNN火箭般的研究速度,在短短数年内带来了能“读懂”照片内容的图像识别系统,能和人对话到毫无PS痕迹的语音助手,能击败围棋世界冠军.引

浅谈SQL Server中的事务日志(一)----事务日志的物理和逻辑构架

浅谈SQL Server中的事务日志(一)----事务日志的物理和逻辑构架 简介 SQL Server中的事务日志无疑是SQL Server中最重要的部分之一.因为SQL SERVER利用事务日志来确保持久性(Durability)和事务回滚(Rollback).从而还部分确保了事务的ACID属性.在SQL Server崩溃时,DBA还可以通过事务日志将数据恢复到指定的时间点.当SQL Server运转良好时,多了解一些事务日志的原理和概念显得并不是那么重要.但是,一旦SQL SERVER发生崩