本节书摘来自异步社区《C语言接口与实现:创建可重用软件的技术》一书中的第2章,第2.1节,作者 傅道坤,更多章节内容可以访问云栖社区“异步社区”公众号查看
第2章 接口与实现
C语言接口与实现:创建可重用软件的技术
模块分为两个部分,即模块的接口与实现。接口规定了模块做什么。接口会声明标识符、类型和例程,提供给使用模块的代码。实现指明模块如何完成其接口规定的目标。对于给定的模块,通常只有一个接口,但可能有许多实现提供了接口规定的功能。每个实现可能使用不同的算法和数据结构,但它们都必须合乎接口的规定。
客户程序(client)是使用模块的一段代码。客户程序导入接口,实现则导出接口。客户程序只需要看到接口即可。实际上,它们可能只有实现的目标码。多个客户程序共享接口和实现,因而避免了不必要的代码重复。这种方法学也有助于避免bug,接口和实现编写并调试一次后,可以经常使用。
2.1 接口
接口仅规定客户程序可能使用的那些标识符,而尽可能隐藏不相关的表示细节和算法。这有助于客户程序避免依赖特定实现的具体细节。客户程序和实现之间的这种依赖性称之为耦合(coupling),在实现改变时耦合会导致bug,当依赖性被与实现相关的隐藏或隐含的假定掩盖时,这种bug可能会特别难于改正。设计完善且陈述准确的接口可以减少耦合。
对于接口与实现相分离,C语言只提供了最低限度的支持,但通过一些简单的约定,我们即可获得接口/实现方法学的大多数好处。在C语言中,接口通过一个头文件指定,头文件的扩展名通常为.h。这个头文件会声明客户程序可能使用的宏、类型、数据结构、变量和例程。客户程序用C预处理器指令#include导入接口。
以下例子说明了本书中的接口使用的约定。下述接口
〈arith.h〉≡
extern int Arith_max(int x, int y);
extern int Arith_min(int x, int y);
extern int Arith_div(int x, int y);
extern int Arith_mod(int x, int y);
extern int Arith_ceiling(int x, int y);
extern int Arith_floor (int x, int y);
声明了6个整数算术运算函数。该接口的实现需要为上述每一个函数提供定义。
该接口命名为Arith,接口头文件命名为arith.h。在接口中,接口名称表现为每个标识符的前缀。这种约定并不优美,但C语言几乎没有提供其他备选方案。所有文件作用域中的标识符,包括变量、函数、类型定义和枚举常数,都共享同一个命名空间。所有的全局结构、联合和枚举标记则共享另一个命名空间。在一个大程序中,在本来无关的模块中,很容易使用同一名称表示不同的目的。避免这种名称冲突(name collision)的一个方法是使用前缀,如模块名。一个大程序很容易有数千全局标识符,但通常只有几百个模块。模块名不仅提供了适当的前缀,还有助于使客户程序代码文档化。
Arith接口中的函数提供了标准C库缺失的一些有用功能,并对除法和模运算提供了良定义的结果,而标准则将这些操作的行为规定为未定义(undefined)或由具体实现来定义(implementation-defined)。
Arith_min和Arith_max函数分别返回其整型参数的最小值和最大值。
Arith_div返回x除以y获得的商,而Arith_mod则返回对应的余数。当x和y都为正或都为负时,Arith_div(x,y)等于x/y,而Arith_mod(x,y)等于x%y。然而当两个操作数符号不同时,由C语言内建运算符所得出的返回值取决于具体编译器的实现。当y为零时,Arith_div和Arith_mod的行为与x/y和x%y相同。
C语言标准只是强调,如果x/y是可表示的,那么(x/y)y + x%y必须等于x。当一个操作数为负数时,这种语义使得整数除法可以向零舍入,也可以向负无穷大舍入。例如,如果-13/5的结果定义为-2,那么标准指出,-13%5必须等于-13 - (-13/5)5 = -13 - (-2)5 = -3。但如果-13/5定义为-3,那么-13%5的值必须是-13 - (-3)5 = 2。
因而内建的运算符只对正的操作数有用。标准库函数div和ldiv以两个整数或长整数为输入,并计算二者的商和余数,在一个结构的quot和rem字段中返回。这两个函数的语义是良定义的:它们总是向零舍入,因此div(-13,5).quot总是等于-2。Arith_div和Arith_mod同样是良定义的。它们总是向数轴的左侧舍入,当其操作数符号相同时向零舍入,当其符号不同时向负无穷大舍入,因此Arith_div(-13,5)返回-3。
Arith_div和Arith_mod的定义可以用更精确的数学术语来表达。Arith_div(x,y)定义为不超过实数z的最大整数,而zy=x。因而,对x=-13和y=5(或者x = 13和y= -5),z为-2.6,因此Arith_div(-13,5)为-3。Arith_mod(x,y)定义为等于x - yArith_div(x,y),因此Arith_mod(-13,5)为-13 -5*(-3) - 2。
Arith_ceiling和Arith_floor函数遵循类似的约定。Arith_ceiling(x,y)返回不小于x/y的实数商的最小整数,而Arith_floor(x,y)返回不大于x/y的实数商的最大整数。对所有操作数x和y来说,Arith_ceiling返回数轴在x/y对应点右侧的整数,而Arith_floor返回x/y对应点左侧的整数。例如:
Arith_ceiling( 13,5) = 13/5 = 2.6 = 3
Arith_ceiling(-13,5) =-13/5 = -2.6 = -2
Arith_floor ( 13,5) = 13/5 = 2.6 = 2
Arith_floor (-13,5) =-13/5 = -2.6 = -3
即便简单如Arith这种程度的接口仍然需要这么费劲的规格说明,但对大多数接口来说,Arith的例子很有代表性和必要性(很让人遗憾)。大多数编程语言的语义中都包含漏洞,某些操作的精确含义定义得不明确或根本未定义。C语言的语义充满了这种漏洞。设计完善的接口会塞住这些漏洞,将未定义之处定义完善,并对语言标准规定为未定义或由具体实现定义的行为给出明确的裁决。
Arith不仅是一个用来显示C语言缺陷的人为范例,它也是有用的,例如对涉及模运算的算法,就像是哈希表中使用的那些算法。假定i从零到N - 1,其中N大于1,并对i加1和i减1的结果模N。即,如果i为N-1,i+1为0,而如果i为0,i-1为N-1。下述表达式
i = Arith_mod(i + 1, N);
i = Arith_mod(i - 1, N);
正确地对i进行了加1模N和减1模N的操作。表达式i = (i+1) % N可以工作,但i = ( i-1) % N无法工作,因为当i为0时,(i-1) % N可能是-1或N-1。程序员在(-1) % N返回N-1的计算机上可以使用(i-1) %N,但如果依赖这种由具体实现定义的行为,那么在将代码移植到(-1) % N返回-1的计算机上时,就可能遭遇到非常出人意料的行为。库函数div(x,y)也无济于事。它返回一个结构,其quot和rem字段分别保存x/y的商和余数。在i为零时,div(i-1, N).rem总是-1。使用i = (i-1+N) % N是可以的,但仅当i-1+N不造成溢出时才行。