《C语言接口与实现:创建可重用软件的技术》一2.4 客户程序的职责

简介:

本节书摘来自异步社区《C语言接口与实现:创建可重用软件的技术》一书中的第2章,第2.4节,作者 傅道坤,更多章节内容可以访问云栖社区“异步社区”公众号查看

2.4 客户程序的职责

接口是其实现和其客户程序之间的一份契约。实现必须提供接口中规定的功能,而客户程序必须根据接口中描述的隐式和显式的规则来使用这些功能。程序设计语言提供了一些隐式规则,来支配接口中声明的类型、函数和变量的使用。例如,C语言的类型检查规则可以捕获接口函数的参数的类型和数目方面的错误。

C语言的用法没有规定的或编译器无法检查的规则,必须在接口中详细说明。客户程序必须遵循这些规则,实现必须执行这些规则。接口通常会规定未检查的运行时错误(unchecked runtime error)、已检查的运行时错误(checked runtime error)和异常(exception)。未检查的和已检查的运行时错误是非预期的用户错误,如未能打开一个文件。运行时错误是对客户程序和实现之间契约的破坏,是无法恢复的程序bug。异常是指一些可能的情形,但很少发生。程序也许能从异常恢复。内存耗尽就是一个例子。异常在第4章详述。

未检查的运行时错误是对客户程序与实现之间契约的破坏,而实现并不保证能够发现这样的错误。如果发生未检查的运行时错误,可能会继续执行,但结果是不可预测的,甚至可能是不可重复的。好的接口会在可能的情况下避免未检查的运行时错误,但必须规定可能发生的此类错误。例如,Arith必须指明除以零是一个未检查的运行时错误。Arith虽然可以检查除以零的情形,但却不加处理使之成为未检查的运行时错误,这样接口中的函数就模拟了C语言内建的除法运算符的行为(即,除以零时其行为是未定义的)。使除以零成为一种已检查的运行时错误,也是一种合理的方案。

已检查的运行时错误是对客户程序与实现之间契约的破坏,但实现保证会发现这种错误。这种错误表明,客户程序未能遵守契约对它的约束,客户程序有责任避免这类错误。Stack接口规定了三个已检查的运行时错误:

(1) 向该接口中的任何例程传递空的Stack_T类型的指针;

(2) 传递给Stack_free的Stack_T指针为NULL指针;

(3) 传递给Stack_pop的栈为空。

接口可以规定异常及引发异常的条件。如第4章所述,客户程序可以处理异常并采取校正措施。未处理的异常(unhandled exception)被当做是已检查的运行时错误。接口通常会列出自身引发的异常及其导入的接口引发的异常。例如,Stack接口导入了Mem接口,它使用后者来分配内存空间,因此它规定Stack_new和Stack_push可能引发Mem_Failed异常。本书中大多数接口都规定了类似的已检查的运行时错误和异常。

在向Stack接口添加这些之后,我们可以继续进行其实现:

〈stack.c〉≡ 
 #include < stddef.h> 
 #include "assert.h" 
 #include "mem.h" 
 #include "stack.h" 

 #define T Stack_T 
 〈types 18〉 
 〈functions 18〉

define指令又将T定义为Stack_T的缩写。该实现披露了Stack_T的内部结构,它是一个结构,一个字段指向一个链表,链表包含了栈上的各个指针,另一个字段统计了指针的数目。

〈types  18〉≡ 
 struct T {
   int count;
   struct elem {
     void *x;
     struct elem *link; 
   } *head;
};

Stack_new分配并初始化一个新的T:

〈functions  18〉≡ 
 T Stack_new(void) {
   T stk; 

   NEW(stk);
   stk->count = 0;
   stk->head = NULL;
   return stk; 
}

NEW是Mem接口中一个用于分配内存的宏。NEW(p)为p指向的结构分配一个实例,因此Stack_new中使用它来分配一个新的Stack_T结构实例。

如果count字段为0,Stack_empty返回1,否则返回0:

〈functions  18〉+≡ 
 int Stack_empty(T stk) {
   assert(stk);
   return stk->count == 0; 
 }

assert(stk)实现了已检查的运行时错误,即禁止对Stack接口函数中的Stack_T类型参数传递NULL指针。assert(e)是一个断言,声称对任何表达式e,e都应该是非零值。如果e非零,它什么都不做,否则将中止程序执行。assert是标准库的一部分,但第4章的Assert接口定义了自身的assert,其语义与标准库类似,但提供了优雅的程序终止机制。assert用于所有已检查的运行时错误。

Stack_push和Stack_pop分别在stk->head链表头部添加和删除元素:

〈functions 18〉+≡ 
 void Stack_push(T stk, void *x) {
   struct elem *t; 

   assert(stk);
   NEW(t);
   t->x = x;
   t->link = stk->head;
   stk->head = t;
   stk->count++; 
 } 

void *Stack_pop(T stk) {
   void *x;
   struct elem *t; 

   assert(stk);
   assert(stk->count > 0);
   t = stk->head;
   stk->head = t->link;
   stk->count--;
   x = t->x;
   FREE(t);
   return x; 
 }

FREE是Mem用于释放内存的宏,它释放其指针参数指向的内存空间,并将该参数设置为NULL指针,这与Stack_free的做法同理,都是为了避免悬挂指针。Stack_free也调用了FREE:

〈functions 18〉+≡  
 void Stack_free(T *stk) {
   struct elem *t, *u; 

   assert(stk && *stk); 
   for (t = (*stk)->head; t; t = u) {
     u = t->link;
     FREE(t); 
   }
   FREE(*stk);
 }

该实现披露了一个未检查的运行时错误,本书中所有的ADT接口都会受到该错误的困扰,因而并没有在接口中指明。我们无法保证传递到Stack_push、Stack_pop、Stack_empty的Stack_T值和传递到Stack_free的Stack_T*值都是Stack_new返回的有效的Stack_T值。习题2.3针对该问题进行了探讨,给出一个部分解决方案。

还有两个未检查的运行时错误,其效应可能更为微妙。本书中许多ADT通过void指针通信,即存储并返回void指针。在任何此类ADT中,存储函数指针(指向函数的指针)都是未检查的运行时错误。void指针是一个类属指针(generic pointer,通用指针),类型为void *的变量可以容纳指向一个对象的任意指针,此类指针可以指向预定义类型、结构和指针。但函数指针不同。虽然许多C编译器允许将函数指针赋值给void指针,但不能保证void指针可以容纳函数指针[1]。

通过void指针传递任何对象指针都不会损失信息。例如,在执行下列代码之后,

S *p, *q; 
void *t; 
... 
t = p; 
q = t;

对任何非函数的类型S,p和q都将是相等的。但不能用void指针来破坏类型系统。例如,在执行下列代码之后,

S *p; 
D *q; 
void *t; 
... 
t = p; 
q = t;

我们不能保证q与p是相等的,或者根据类型S和D的对齐约束,也不能保证q是一个指向类型D对象的有效指针。在标准C语言中,void指针和char指针具有相同的大小和表示。但其他指针可能小一些,或具有不同的表示。因而,如果S和D是不同的对象类型,那么在ADT中存储一个指向S的指针,将该指针返回到一个指向类型D的指针中,这是一个未检查的运行时错误。

在ADT函数并不修改被指向的对象时,程序员可能很容易将不透明指针参数声明为const。例如,Stack_empty可能有下述编写方式。

int Stack_empty(const T stk) { 
  assert(stk); 
  return stk->count == 0; 
}

const的这种用法是不正确的。这里的意图是将stk声明为一个“指向struct T的常量实例的指针”,因为Stack_empty并不修改stk。但const T stk将stk声明为一个“常量指针,指向一个struct T实例”,对T的typedef将struct T 打包到一个类型中,这一个指针类型成为了const的操作数[2]。无论对Stack_empty还是其调用者,const T stk都是无用的,因为在C语言中,所有的标量包括指针在函数调用时都是传值的。无论有没有const限定符,Stack_empty都无法改变调用者的实参值。

用struct T *代替T,可以避免这个问题:

int Stack_empty(const struct T *stk) { 
  assert(stk); 
  return stk->count == 0; 
}

这个用法说明了为什么不应该将const用于传递给ADT的指针:const披露了有关实现的一些信息,因而限制了可能性。对于Stack的这个实现而言,使用const不是问题,但它排除了其他同样可行的方案。假定某个实现预期可重用栈中的元素,因而延迟对栈元素的释放操作,但会在调用Stack_empty时释放它们。Stack_empty的这种实现需要修改 stk,但因为stk声明为const而无法进行修改。本书中的ADT都不使用const。

相关文章
|
7月前
|
存储 自然语言处理 编译器
【C语言】编译与链接:深入理解程序构建过程
【C语言】编译与链接:深入理解程序构建过程
130 1
|
1月前
|
Linux C语言 iOS开发
C语言结合AWTK开发HTTP接口访问界面
这样,我们就实现了在C语言中使用libcurl和AWTK来访问HTTP接口并在界面上显示结果。这只是一个基础的示例,你可以根据需要添加更多的功能和优化。例如,你可以添加错误处理机制、支持更多HTTP方法(如POST、PUT等)、优化用户界面等。
77 26
|
9月前
|
存储 算法 C语言
"揭秘C语言中的王者之树——红黑树:一场数据结构与算法的华丽舞蹈,让你的程序效率飙升,直击性能巅峰!"
【8月更文挑战第20天】红黑树是自平衡二叉查找树,通过旋转和重着色保持平衡,确保高效执行插入、删除和查找操作,时间复杂度为O(log n)。本文介绍红黑树的基本属性、存储结构及其C语言实现。红黑树遵循五项基本规则以保持平衡状态。在C语言中,节点包含数据、颜色、父节点和子节点指针。文章提供了一个示例代码框架,用于创建节点、插入节点并执行必要的修复操作以维护红黑树的特性。
183 1
|
9月前
|
NoSQL 编译器 程序员
【C语言】揭秘GCC:从平凡到卓越的编译艺术,一场代码与效率的激情碰撞,探索那些不为人知的秘密武器,让你的程序瞬间提速百倍!
【8月更文挑战第20天】GCC,GNU Compiler Collection,是GNU项目中的开源编译器集合,支持C、C++等多种语言。作为C语言程序员的重要工具,GCC具备跨平台性、高度可配置性及丰富的优化选项等特点。通过简单示例,如编译“Hello, GCC!”程序 (`gcc -o hello hello.c`),展示了GCC的基础用法及不同优化级别(`-O0`, `-O1`, `-O3`)对性能的影响。GCC还支持生成调试信息(`-g`),便于使用GDB等工具进行调试。尽管有如Microsoft Visual C++、Clang等竞品,GCC仍因其灵活性和强大的功能被广泛采用。
233 1
|
9月前
|
编译器 C语言 计算机视觉
C语言实现的图像处理程序
C语言实现的图像处理程序
|
6月前
|
C语言 开发者
C语言中的模块化编程思想,介绍了模块化编程的概念、实现方式及其优势,强调了合理划分模块、明确接口、保持独立性和内聚性的实践技巧
本文深入探讨了C语言中的模块化编程思想,介绍了模块化编程的概念、实现方式及其优势,强调了合理划分模块、明确接口、保持独立性和内聚性的实践技巧,并通过案例分析展示了其应用,展望了未来的发展趋势,旨在帮助读者提升程序质量和开发效率。
198 5
|
6月前
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
152 5
|
6月前
|
C语言
C语言编程中,错误处理至关重要,能提升程序的健壮性和可靠性
C语言编程中,错误处理至关重要,能提升程序的健壮性和可靠性。本文探讨了C语言中的错误类型(如语法错误、运行时错误)、基本处理方法(如返回值、全局变量、自定义异常处理)、常见策略(如检查返回值、设置标志位、记录错误信息)及错误处理函数(如perror、strerror)。强调了不忽略错误、保持处理一致性及避免过度处理的重要性,并通过文件操作和网络编程实例展示了错误处理的应用。
162 4
|
6月前
|
网络协议 物联网 数据处理
C语言在网络通信程序实现中的应用,介绍了网络通信的基本概念、C语言的特点及其在网络通信中的优势
本文探讨了C语言在网络通信程序实现中的应用,介绍了网络通信的基本概念、C语言的特点及其在网络通信中的优势。文章详细讲解了使用C语言实现网络通信程序的基本步骤,包括TCP和UDP通信程序的实现,并讨论了关键技术、优化方法及未来发展趋势,旨在帮助读者掌握C语言在网络通信中的应用技巧。
129 2
|
6月前
|
人工智能 安全 算法
基于C语言的嵌入式系统开发,涵盖嵌入式系统概述、C语言的优势、开发流程、关键技术、应用实例及面临的挑战与未来趋势。
本文深入探讨了基于C语言的嵌入式系统开发,涵盖嵌入式系统概述、C语言的优势、开发流程、关键技术、应用实例及面临的挑战与未来趋势。C语言因其高效、可移植、灵活及成熟度高等特点,在嵌入式系统开发中占据重要地位。文章还介绍了从系统需求分析到部署维护的完整开发流程,以及中断处理、内存管理等关键技术,并展望了嵌入式系统在物联网和人工智能领域的未来发展。
263 1