龙空技术网

C语言的科学和艺术:软件工程技术中的分层抽象、库和模块化开发

小智雅汇 823

前言:

而今小伙伴们对“c语言程序设计基础实验总结”大概比较看重,朋友们都想要分析一些“c语言程序设计基础实验总结”的相关文章。那么小编在网上网罗了一些对于“c语言程序设计基础实验总结””的相关资讯,希望兄弟们能喜欢,看官们一起来学习一下吧!

美国人Eric S.Roberts的《C语言的科学和艺术》一书与一般C语言的书籍不同,有其独特的视角:站在计算机科学(computer science)的角度,以软件工程的思维(如分层抽象,接口、库与模块等)来描述C语言如何应对复杂问题或大规模问题。

书中集中讨论了分层抽象(函数分解与嵌套、数据结构的复合)的思想,以及接口、模块、库的构建,使用的问题,且在书中也使用了一个作者自己编写的函数库genlib

书中以一个打印日历的小实例来说明如何“stepwise refinement”,如何”top-down decomposition”、"bottom-up implementation”。用一个随机数库来说明接口设计和实现,库与模块化。以及细化、优化、泛化的思路。(另外一个优化的小实例是素数判断问题)

书中还引入了一个strlib库,介绍数据分层抽象的思路,以及一个queue抽象数据类型的实现(介绍了在保持接口不变的情况下修改数据表示和函数定义而不影响接口的使用)。

书中的一个PigLatin的实例,介绍了大问题如何分解为小问题,如何构建模块的思路。

以下介绍如何下载、构建和使用genlib函数库及《C语言的科学和艺术》一书的核心概念。

1 下载genlib函数库的源文件

genlib源代码下载:

中英文版《C语言的科学和艺术》下载:

全部源代码及英文文字版《C语言的科学和艺术》(有修正)下载:

链接:

提取码:wuhn

C语言的科学和艺术课后习题解答.pdf

链接:

提取码:wuhn

2 genlib库的源文件的使用2.1 直接通过路径include

不使用静态库,直接更改include的路径及“添加文件到工程”

① 新建一个源文件:test.c,其中include路径如下:

#include <stdio.h>#include "..\src\standard\genlib.h"#include "..\src\standard\graphics.h"main(){    InitGraphics ();    MovePen (1.5, 1.0);    DrawArc (0.5, 0, 360);}

② 用VC打开,点击“运行”,会提示新建一个工程,确认后会进行编译,连接,会提示错误。

③ 将需要的文件“添加文件到工程”即可(include的头文件与源文件)。

2.2 将源文件编译成静态lib库文件及使用

2.2.1 将源文件编译成静态lib库文件

将上述下载的源代码文件夹中的standard文件夹下的源文件(.h、.c文件)编译成.lib静态库文件。具体步骤为:

① 点击VC的“文件”→“新建”,打开“新建”对话框,选择“Win32 Static Library”,工程名称为cslib。

② 将standard文件夹下的所有.c文件添加到“Source Files”。

③ 将standard文件夹下的所有.h文件添加到“Header Files”。

④ 点击“组建”→“组建【cslib.lib】”,就完成了组建静态库的工作了。

在上述工程的Debug目录下即可找到lib静态库文件cslib.lib。

将cslib.lib重命名为cslibcs.lib,拷贝到VC的lib文件夹下。为什么要重命名呢?因为include文件夹中已经有一个CSLIB.LIB文件了。

2.2.2 静态库使用测试

至此我们就完成了大半工作,下面我们可以试验一下库是否可以正常使用。

① 新建一个源文件:test.c。

② 用VC打开,点击“运行”,会提示新建一个工程,确认后会进行编译,连接,会提示错误:

test.obj : error LNK2001: unresolved external symbol _GetReal Debug/test.exe : fatal error LNK1120: 1 unresolved externals 6.3

在工程设置中添加库即可。

单击“工程”→“设置”菜单项,选择“连接”选项卡,在“对象/库模块”的最后添加“ cslib.lib”,点击“确定”即可。

需要注意的是,第七章的图形库生成的是graphics.ps文件,这其实是一种PostScript格式。

3 核心概念3.1 抽象与接口

接口有时也被称为抽象边界(abstraction boundary)。理想情况下,库的实现中的所有的复杂性都位于抽象接口的实现这一边。如果接口能对客户方屏蔽复杂性,那么接口就是成功的。把细节限制在实现范畴内称为信息隐藏 (information hiding) 。接口的主要目的是使客户不需要了解实现的复杂性。

一个定义良好的接口必须具备同一性、简单性、充分性、通用性和稳定性。

I 在一个接口中定义的所有函数必须符合同一性的模式,在行为上必须尽可能一致。

II 接口表示的抽象必须有足够强大的功能,以满足它的客户的需要,否则也不屑去使用。

III 通用的接口使许多用户可以用同一软件包。

IV 客户必须能够依赖它们所用的接口的稳定性。改变接口是一个严重的问题,没人能轻易地修改接口。另一方面,维护一个稳定接口使实现者能自由修改基本的实现。

接口输出的定义称为接口项。最常见的接口项是函数原型、常量定义和类型定义。接口还应该包含每个项的注释(注释在预编译后会被替换为一个空格),以便客户能理解这些项如何使用。

为了保证编译器对每个接口只包含一次,每个接口(头文件)在第一个接口项前应该包含以下行:

#ifndef _name_h#define _name_h.// ……// 在接口文件的最后,包含以下行:#endif

抽象类型的行为是由能在这种类型的对象上实施的操作来定义的。特定抽象类型的合法操作被称为它的基本操作(primitive operation),被定义为与该类型相关的接口中的函数。这些操作的细节和数据的基本表示都被隐藏在该接口的实现中。不管客户何时要操作某个抽象类型的值,客户必须用该接口提供的函数。

A type defined in terms of its behavior rather than its representation is called an abstract data type, which is often abbreviated to ADT.

3.2 细化与模块化

把一个大的问题分解成小片段的策略的方法(庖丁解牛)在程序设计过程的很多阶段都会用到。当程序变得更长的时候,要在一个源文件中处理非常多的函数会变得困难。就像要一下子理解一个50行的函数也是比较困难的一样。在这两种情况下,引入一些额外的结构是很有用的。比如碰到一个50行的函数时,最好的方法是把它分成几个相互调用的小函数来完成任务。当碰到一个包含50个函数的程序时,最好的办法就是把程序再分成几个更小的源文件。每个源文件都包含一组相关的函数。由整个程序的一部分组成的较小的源文件称为模块(module)。每个独立的模块比整个程序要简单。而且,如果在设计的时候非常仔细的话,可以把同一模块作为许多不同的应用程序一部分。

当把一个程序分成模块的时候,选择合适的分解方法来减少模块之间相互依赖的程度是很重要的。包含main函数的模块叫主模块(main module),在分解层次中处于最高层。每个其他模块代表一个独立的抽象,其操作在一个接口中定义。

函数的嵌套调用可以实现程序逻辑结构上的分层抽象,也就是“stepwise refinement“和“top-down decomposition”的具体实现。

The complete solution has the form of a hierarchy in which high-level functions make calls to lower-level functions, which in turn call even lower-level functions make calls to lower-level functions, until the functions are simple enough to implement directly without making further calls.

3.3 数据的层次抽象,如字符串的分层抽象

程序中的数据结构可以形成一个层次结构。原子数据类型(如int, char, double和枚举类型)构成了层次中的最低层。为了表达更复杂的信息,必须把原子类型组合起来构成大一些的数据结构。而这大一些的结构又可以组合成为更大的数据结构,而且这种结合是开放式的,没有限制的。使用三种构造类型的基本元素(数组、指针、记录),可以在这个层次结构中加入新的层次。如果有一种已存在的类型,你可以定义这个类型的数组,或者把它当作一个记录的一个字段,或者声明一个指向它的指针。这三种工具是数据层次结构的粘合剂,你可以用它们建造具有任意复杂度的结构。

可以从归约和整体两方面考虑程序设计的问题。当你关心数据表示的内部细节时,可以采取归约的方法。从这个方面来讲,你的工作是理解字符在计算机的内存中是如何存储的,这些字符序列如何被存储为一个字符串,以及200个字符的字符串如何放入保存两个字符的字符串的变量中这样的问题。然而,当你从整体的观点考虑字符串时,你的工作是理解如何将字符串作为一个逻辑单位来操作。通过关注字符串的抽象行为,你可以学会如何有效地使用它,而不必沉溺于细节问题。

理想情况下,最好能将归约与整体分开,把每个方面搞清楚。然而,理解字符串的内部结构需要先熟悉一些更高级的主题,如数组和指针。想要同时掌握这些概念太困难 了。过早将注意力集中在字符串的表示上意味无法从抽象的角度(如字符串如何使用以及为 什么要有字符串等)理解字符串。

为了保证你能从整体上理解字符串,可以采用分阶段的方法。先利用字符串库了解字符串的抽象行为,这个库隐藏了许多复杂性。随着学习的深入,你将学到有关字符串表示的更细节的问题,最后你能自己写一个完整的字符串库。除了可以逐步了解字符串以外,这个方法也提供了另一个演示有效接口设计和信息隐藏原则的实例。

如以程序设计中更抽象的概念来看, 采用更高级方法可以通过一系列各个层次上的字符串抽象来实现。不同的抽象形成一个层次结构,底部是最原始的功能。每个新的抽象都是建立在前一层抽象的基础之上,提供字符串概念的更复杂的视点。

建立在不同层次结构上的抽象称为分层抽象(layered abstraction)。用来表示字符串的分层抽象结构如下图所示:

完成输入输出的硬件设备自动在ASCII代码和屏幕或键盘上的符号之间进行转换。计算机对表示字符的整数代码应用算术运算可以处理每个字符。这些功能组成了可用于字符串的机器级的操作,形成层次结构中的最低一层。

在由硬件提供的基本功能的上面,程序设计语言通常也包括一些对字符串操作的支持。 可用于字符串的内嵌操作形成了层次结构中的第二层。在许多语言中,这些功能是非常强大的,可以直接在语言级完成复杂的字符串操作。然而,ANSI C在语言本身几乎没有提供任何对字符串的支持。仅有的支持是提供定义字符串常量功能,所有其他的字符串操作都是由库提供的。

在ANSI C中,程序员可用的大多数字符串操作是由string.h接口提供的。这个库提供 了一组功能强大的字符串操作。然而,某些常用的操作用string. h接口难以完 成。例如,当你用string. h中的函数时,不能很容易地从函数返回字符串值,或直接为一个变量赋一个字符串值。因为使用string. h接口是ANSI C中字符串操作的标准方法, 最终你必须学会如何使用它。

为了让你有机会能用概念上较简单的模型处理字符串,可以引入一个更高抽象层次的字符串库,如strlib.h接口,形成抽象层次结构的最高层,使得字符串操作相对容易。

strlib.h接口的主要优势在于它能使字符串作为抽象类型来处理。可以把抽象类型 (abstract type)想象为按照它的行为而不是按照它的表示定义的类型。

正如程序中的控制语句和函数调用定义了算法控制结构一样,这种类型定义的层次构成了数据结构(data structure)。这两个概念(控制结构和数据结构)共同构成了现代程序设计的基础。

3.4 字符相关3.4.1 转义字符

能显示在屏幕上的字符称为可打印字 符(printing character)。然而,ASCII中也包括许多特殊字符(special character),它们用来完成某一特定的动作。

将转义序列作为字符常量的一部分就可以在字符常量中包含特殊字符。虽然每个转义序列由几个字符组成,但在机器内部,每个序列被转换为一个ASCII代码。例如,换行符的内部表示为整数10。

3.4.2 字符的算术运算

尽管对char类型的值应用任何算术运算都是合法的,但在它的值域内,不是所有运算都是有 意义的。例如,在程序中将'A'乘以'B'是合法的。为了得到这个结果,计算机取它的内部代 码,65和66,将它们乘起来,得到4290。问题在于这个整数作为字符毫无意义,事实上,它 超出了ASCII字符的范围。当对字符进行运算时,仅有很少的算术运算是有用的。这些有意义的运算通常有:

① 给一个字符加上一个整数。如果c是一个字符,n是一个整数,表达式c + n表示代码序列中c后面的第n个字符。例如,如果n在0~9之间,表达式,'0' + n得到的是第n个数字的字符代码。因此,'0' + 5 是 '5' 的字符代码。同样,如果n在1~26之间,那么 'A'+n-1表示字母表中第n个字母的字符代码。

② 从一个字符中减去一个整数。表达式c-n表示代码序列中c前面的第n个字符。例如,表 达式'Z'-2 的结果是'X'的字符代码。

③ 从一个字符中减去另一个字符。如果c1和c2都是字符,那么表达式c1-c2表示两个字符在代码序列中的距离。例如,'a'-'A' 是32,更重要的是,小写字母和与之对应的大写字母之间的距离 是固定的,因此,'z'-'Z' 也是32。

④ 比较两个字符。用某个关系运算比较两个字符的值是常用的运算,经常用来确定字母的次序。例如,如果在ASCII表中,c1在c2前面,那么表达式c1<c2是TRUE。

3.4.3 getc函数的返回值

初看之下,getc()函数返回值的类型是很奇怪的。函数原型指定getc返回一个整型值,虽然该函数在概念上返回的是一个字符。

int getc(FILE* infile);

这样设计的原因是因为返回一个字符会使得程序无法识别文件结束标记。字符编码一共只有256个,且一个数据文件中可能包含其中的任意值。因此没有一个值(至少没有char类型的值)可以用作文件结束标记。扩展定义,使得getc返回一个整型值,这样的实现可以返回一个合法字符数据以外的值作为文件结束标记。通常在stdio.h中这个值被称为EOF,尽管你不能依赖EOF有内部值的事实,但EOF还是有值-1。

请记住,getc() 返回一个整型值,而不是一个字符型的值。如果用字符型的变量存储getc的结果,你的程序就检测不到文件结束标记。

3.5 标量类型

在C语言中,枚举(enum)类型、字符型和各种整数的表示形式统一叫做标量类型(Scalar type)

相对应的概念是复合类型(Compound type),复合类型由基本类型(primitive built-in type)构成。

char是标量类型,因此字符可用的运算集合与整数是一样的。然而,理解这些运算在字符域中的含义需要更进一步认识字符在机器内部的表示。

enum定义的也是一种标量类型。与一般int类型不同的是,它限制了其值域只能是有限的枚举出来的命名整型常量(用做右值)。enum的定义形式虽然与结构体一样都使用大括号来定义类型,但结构体定义的项却是有类型的不同或相同的成员,但enum却只是在大括号内枚举命名的整型常量。

值不是数字的类型在计算机内部通常通过对该类型的值域中的元素编码,然后用这些编号作为原先值的代码来表示。通过对元素进行计数而定义的类型称为枚举类型(enum)。

3.6 数组与记录(结构体)

可以将数组和记录结合起来,组成具有任意复杂度的数据层次结构。用来表示数据集合的一个完整结构称为数据库,特别当它作为数据文件表示时更是如此。

3.7 递归

理解递归的核心在于理解由编译器实现的函数调用机制。每一次函数调用都保留了一个栈帧,每一个栈帧上都保存了函数的参数值、返回地址、局部变量值、返回值(或保存在寄存器)。递归函数在基准条件满足时即可返回,栈帧上保存的数据按“后进先出”的顺序读取。这样就形成了一个与循环对应的关系:循环的迭代变量通常放在递归函数的参数中,循环的结束条件对应递归的基准条件。如果不是尾递归,要用循环来替换的话,就涉及到一个栈帧数据的保存问题,需要一个栈结构来辅助。

if (test for simple case) {    return (simple solution computed without using recursion);} else {    return (recursive solution involving a call to the same function);}

"stepwise refinement"的思想与递归的思想有异曲同工之妙,"top-down decomposition"来逐层分解函数,直到最低层的函数很容易实现,然后再"bottom-up implementation"。

ref:

在Win7和Linux下的编程环境下使用库文件/a.html

-End-

标签: #c语言程序设计基础实验总结