《C语言接口与实现:创建可重用软件的技术》一第1章 引言1.1 文学程序

简介:

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

第1章 引言

C语言接口与实现:创建可重用软件的技术
一个大程序由许多小的模块组成。这些模块提供了程序中使用的函数、过程和数据结构。理想情况下,这些模块中大部分都是现成的并且来自于库,只有那些特定于现有应用程序的模块需要从头开始编写。假定库代码已经全面测试过,而只有应用程序相关的代码会包含bug,那么调试就可以仅限于这部分代码。

遗憾的是,这种理论上的理想情况实际上很少出现。大多数程序都是从头开始编写,它们只对最低层次的功能使用库,如I/O和内存管理。即使对于此类底层组件,程序员也经常编写特定于应用程序的代码。例如,将C库函数malloc和free替换为定制的内存管理函数的应用程序也是很常见的。

造成这种情况的原因无疑有诸多方面。其中之一就是,很少有哪个普遍可用的库包含了健壮、设计良好的模块。一些可用的库相对平庸,缺少标准。虽然C库自1989年已经标准化,但直至现在才出现在大多数平台上。

另一个原因是规模问题:一些库规模太大,从而导致对库本身功能的掌握变成了一项沉重的任务。哪怕这项工作的工作量似乎稍逊于编写应用程序所需的工作量,程序员可能都会重新实现库中他们所需的部分功能。最近出现颇多的用户界面库,通常会有这种问题。

库的设计和实现是困难的。在通用性、简单性和效率这3个约束之间,设计者必须如履薄冰,审慎前行。如果库中的例程和数据结构过于通用,那么库本身可能难以使用,或因效率较低而无法达到预定目标。如果库的例程和数据结构过于简单,又可能无法满足应用程序的需求。如果库太难于理解,程序员干脆就不会使用它们。C库本身就提供了一些这样的例子,例如其中的realloc函数,其语义混乱到令人惊讶的地步。

库的实现者面临类似的障碍。即使设计做得很好,糟糕的实现同样会吓跑用户。如果某个实现太慢或太庞大,或只是感觉上如此,程序员都将自行设计替代品。最糟的是,如果实现有bug,它将使上述的理想状况彻底破灭,从而使库也变得无用。

本书描述了一个库的设计和实现,它适应以C语言编写的各种应用程序的需求。该库导出了一组模块,这些模块提供了用于小规模程序设计(programming-in-the-small)的函数和数据结构。在几千行长的应用程序或应用程序组件中,这些模块适于用作零部件。

在后续各章中描述的大部分编程工具,都涵盖在大学本科数据结构和算法课程中。但在本书中,我们更关注将这些工具打包的方式,以及如何使之健壮无错。各个模块都以一个接口及其实现的方式给出。这种设计方法学在第2章中进行了解释,它将模块规格说明与其实现相分离,以提高规格说明的清晰度和精确性,而这有助于提供健壮的实现。

1.1 文学程序

本书并不是以“技巧”的形式来描述各个模块,而是通过例子描述。各章完整描述了一两个接口及其实现。这些描述以文学程序(literate program)的形式给出。接口及其实现的代码与对其进行解释的正文交织在一起。更重要的是,各章本身就是其描述的接口和实现的源代码。代码可以从本书的源文件文本中自动提取出来,所见即所得。

文学程序由英文正文和带标签的程序代码块组成。例如,

〈compute x • y〉≡ 
  sum = 0; 
  for (i = 0; i < n; i++) 
      sum += x[i]*y[i];

定义了名为〈compute x • y〉的代码块,其代码计算了数组x和y的点积。在另一个代码块中使用该代码块时,直接引用即可:

〈functiondotproduct〉≡ 
  int dotProduct(int x[], int y[], int n) { 
    int i, sum; 

  〈compute x • y〉 
    return sum; 
}

当〈function dotproduct〉代码块从本章对应的源文件中抽取出来时,将逐字复制其代码,用到代码块的地方都将替换为对应的代码。抽取〈function dotproduct〉的结果是一个只包含下述代码的文件:

int dotProduct(int x[], int y[], int n) { 
    int i, sum; 

    sum = 0; 
    for (i = 0; i < n; i++) 
        sum += x[i]*y[i]; 
    return sum; 
}

文学程序可以按各个小片段的形式给出,并附以完备的文档。英文正文包含了传统的程序注释,这些并不受程序设计语言的注释规范的限制。

代码块的这种特性将文学程序从编程语言强加的顺序约束中解放出来。代码可以按最适于理解的顺序给出,而不是按语言所硬性规定的顺序(例如,程序实体必须在使用前定义)。

本书中使用的文学编程系统还有另外一些特性,它们有助于逐点对程序进行描述。为说明这些特性并提供一个完整的C语言文学程序的例子,本节其余部分将描述double程序,该程序检测输入中相邻的相同单词,如“the the”。

% double intro.txt inter.txt 
intro.txt:10: the 
inter.txt:110: interface 
inter.txt:410: type 
inter.txt:611: if

上述UNIX命令结果说明,“the”在intro.txt文件中出现了两次,第二次出现在第10行;而在inter.txt文件中,interface、type和if也分别在给出的行出现第二次。如果调用double时不指定参数,它将读取标准输入,并在输出时略去文件名。例如:

% cat intro.txt inter.txt | double 
10: the 
143: interface 
343: type 
544: if

在上述例子和其他例示中,由用户键入的命令显示为斜代码体,而输出则显示为通常的代码体。

我们先从定义根代码块来实现double,该代码块将使用对应于程序各个组件的其他代码块:

〈double.c3〉≡
 〈includes4〉
 〈data 4〉
 〈prototypes4〉
 〈functions 3〉

按照惯例,根代码块的标签设置为程序的文件名,提取〈double.c3〉代码块,即可提取整个程序。其他代码块的标签设置为double的各个顶层组件名。这些组件按C语言规定的顺序列出,但也可以按任意顺序给出。

〈double.c 3〉中的3是页码,表示该代码块的定义从书中哪一页开始。〈double.c 3〉中使用的代码块中的数字也是页码,表示该代码块的定义从书中哪一页开始。这些页码有助于读者浏览代码时定位。

main函数处理double的参数。它会打开各个文件,并调用doubleword扫描文件:

〈functions 3〉≡ 
 int main(int argc, char *argv[]) {
   int i; 

   for (i = 1; i < argc; i++) {
     FILE *fp = fopen(argv[i], "r");
     if (fp == NULL) { 
       fprintf(stderr, "%s: can't open '%s' (%s)\n",
         argv[0], argv[i], strerror(errno));
       return EXIT_FAILURE; 
     } else {
         doubleword(argv[i], fp);
         fclose(fp); 
       } 
     }
     if (argc == 1) doubleword(NULL, stdin);
     return EXIT_SUCCESS; 
   } 

  〈includes 4〉≡ 
  #include < stdio.h>
  #include < stdlib.h>
  #include < errno.h>

doubleword函数需要从文件中读取单词。对于该程序来说,一个单词由一个或多个非空格字符组成,不区分大小写。getword从打开的文件读取下一个单词,复制到buf [0..size -1]中,并返回1;在到达文件末尾时该函数返回0。

〈functions 3〉+≡ 
 int getword(FILE *fp, char *buf, int size) { 
   int c; 

   c = getc(fp); 
   〈scan forward to a nonspace character or EOF 5〉
   〈copy the word intobuf[0..size-1] 5〉   
   if (c != EOF)     
     ungetc(c, fp);   
   return〈found a word? 5〉; 
 } 

〈prototypes 4〉≡ 
 int getword(FILE *, char *, int);

该代码块说明了另一个文学编程特性:代码块标签〈functions 3〉后接的+≡表示将getword的代码附加到代码块〈functions 3〉的代码的后面,因此该代码块现在包含main和getword的代码。该特性允许分为多次定义一个代码块中的代码,每次定义一部分。对于一个“接续”代码块来说,其标签中的页码指向该代码块的第一次定义处,因此很容易找到代码块定义的开始处。

因为getword在main之后定义,在main中调用getword时就需要一个原型,这就是〈prototypes 4〉代码块的用处。该代码块在一定程度上是对C语言“先声明后使用”(declaration- before-use)规则的让步,但如果该代码定义得一致并在根代码块中出现在〈functions 3〉之前,那么函数可以按任何顺序给出。

getword除了从输入获取下一个单词之外,每当遇到一个换行字符时都对linenum加1。doubleword输出时将使用linenum。

〈data 4〉≡ 
 int linenum; 

〈scan forward to a nonspace character or EOF 5〉≡ 
 for ( ; c != EOF && isspace(c); c = getc(fp)) 
   if (c == '\n') 
     linenum++; 

〈includes 4〉+≡ 
 #include < ctype.h>

linenum的定义,也例证了代码块的顺序不必与C语言的要求相同。linenum在其第一次使用时定义,而不是在文件的顶部或getword定义之前,后两种做法才是合乎C语言要求的。

size的值限制了getword所能存储的单词的长度,getword函数会丢弃过多的字符并将大写字母转换为小写:

〈copy the word intobuf[0..size-1] 5〉≡ 
 { 
   int i = 0; 
   for ( ; c != EOF && !isspace(c); c = getc(fp)) 
     if (i < size - 1) 
       buf[i++] = tolower(c); 
   if (i < size) 
     buf[i] = '\0'; 
 }

索引i与size-1进行比较,以保证单词末尾有空间存储一个空字符。在size为0时,if语句保护了对缓存的赋值操作。在double中不会出现这种情况,但这种防性程序设计(defensive programming)有助于捕获“不可能发生的bug”。

剩下的代码逻辑是,如果buf中保存了一个单词则返回1,否则返回0:

〈found a word? 5〉≡ 
 buf[0] != '\0'

该定义表明,代码块不必对应于C语言中的语句或任何其他语法单位,代码块只是文本而已。

doubleword读取各个单词,并将其与前一个单词比较,发现重复时输出。它只查看以字母开头的单词:

〈functions 3〉+≡ 
 void doubleword(char *name, FILE *fp) { 
   char prev[128], word[128]; 

   linenum = 1; 
   prev[0] = '\0'; 
   while (getword(fp, word, sizeof(word)) { 
     if (isalpha(word[0]) && strcmp(prev, word)==0) 
       〈word is a duplicate 6〉 
     strcpy(prev, word); 
   } 
 } 
〈prototypes 4〉+≡ 

 void doubleword(char *, FILE *); 

〈includes 4〉+≡ 
 #include < string.h>

输出是很容易的,但仅当name不为NULL时才输出文件名及后接的冒号:

〈word is a duplicate 6〉≡
 {
    if (name)
      printf("%s:", name);
    printf("%d: %s\n", linenum, word);
  }

该代码块被定义为一个复合语句,因而可以作为结果用在它所处的if语句中。

相关文章
|
7月前
|
存储 自然语言处理 编译器
【C语言】编译与链接:深入理解程序构建过程
【C语言】编译与链接:深入理解程序构建过程
131 1
|
1月前
|
Linux C语言 iOS开发
C语言结合AWTK开发HTTP接口访问界面
这样,我们就实现了在C语言中使用libcurl和AWTK来访问HTTP接口并在界面上显示结果。这只是一个基础的示例,你可以根据需要添加更多的功能和优化。例如,你可以添加错误处理机制、支持更多HTTP方法(如POST、PUT等)、优化用户界面等。
235 82
|
9月前
|
存储 算法 C语言
"揭秘C语言中的王者之树——红黑树:一场数据结构与算法的华丽舞蹈,让你的程序效率飙升,直击性能巅峰!"
【8月更文挑战第20天】红黑树是自平衡二叉查找树,通过旋转和重着色保持平衡,确保高效执行插入、删除和查找操作,时间复杂度为O(log n)。本文介绍红黑树的基本属性、存储结构及其C语言实现。红黑树遵循五项基本规则以保持平衡状态。在C语言中,节点包含数据、颜色、父节点和子节点指针。文章提供了一个示例代码框架,用于创建节点、插入节点并执行必要的修复操作以维护红黑树的特性。
184 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语言中的模块化编程思想,介绍了模块化编程的概念、实现方式及其优势,强调了合理划分模块、明确接口、保持独立性和内聚性的实践技巧,并通过案例分析展示了其应用,展望了未来的发展趋势,旨在帮助读者提升程序质量和开发效率。
199 5
|
6月前
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
152 5
|
6月前
|
C语言
C语言编程中,错误处理至关重要,能提升程序的健壮性和可靠性
C语言编程中,错误处理至关重要,能提升程序的健壮性和可靠性。本文探讨了C语言中的错误类型(如语法错误、运行时错误)、基本处理方法(如返回值、全局变量、自定义异常处理)、常见策略(如检查返回值、设置标志位、记录错误信息)及错误处理函数(如perror、strerror)。强调了不忽略错误、保持处理一致性及避免过度处理的重要性,并通过文件操作和网络编程实例展示了错误处理的应用。
163 4
|
6月前
|
网络协议 物联网 数据处理
C语言在网络通信程序实现中的应用,介绍了网络通信的基本概念、C语言的特点及其在网络通信中的优势
本文探讨了C语言在网络通信程序实现中的应用,介绍了网络通信的基本概念、C语言的特点及其在网络通信中的优势。文章详细讲解了使用C语言实现网络通信程序的基本步骤,包括TCP和UDP通信程序的实现,并讨论了关键技术、优化方法及未来发展趋势,旨在帮助读者掌握C语言在网络通信中的应用技巧。
129 2
|
6月前
|
人工智能 安全 算法
基于C语言的嵌入式系统开发,涵盖嵌入式系统概述、C语言的优势、开发流程、关键技术、应用实例及面临的挑战与未来趋势。
本文深入探讨了基于C语言的嵌入式系统开发,涵盖嵌入式系统概述、C语言的优势、开发流程、关键技术、应用实例及面临的挑战与未来趋势。C语言因其高效、可移植、灵活及成熟度高等特点,在嵌入式系统开发中占据重要地位。文章还介绍了从系统需求分析到部署维护的完整开发流程,以及中断处理、内存管理等关键技术,并展望了嵌入式系统在物联网和人工智能领域的未来发展。
263 1