华章程序员书库
点击查看第二章
点击查看第三章
C编程技巧:117个问题解决方案示例
C Recipes: A Problem-Solution Approach
希里什·查万(Shirish Chavan)
卢涛 译
第1章
欢迎学习C语言
C是一门过程式编程语言。C的早期历史与UNIX非常接近。这是因为C是专门为编写UNIX操作系统而开发的,UNIX操作系统由贝尔实验室于1969年推出,用来取代PDP-7计算机的Multics操作系统。UNIX的原始版本是用汇编语言编写的,但用汇编语言编写的程序比用高级语言编写的程序可移植性差。因此,AT&T的人们决定用高级语言重写此操作系统。做出这个决定之后,他们开始寻找合适的语言,但是当时没有合适的允许位级编程的高级语言。
在同一时期(1970年),Kenneth Thompson开发了一种系统编程语言,按照其母语言BCPL(由Martin Richards于1967年开发)命名为B语言。1972年,C语言作为B语言的改进版本首次亮相。C语言由Dennis Ritchie开发,其名字来自B(即字母表中,字母C跟着字母B,并且在BCPL的名字中,字母C也跟着字母B)。
Ritchie和贝尔实验室的一组研究人员一起为C语言创建了一个编译器。与B语言不同,C语言配备了大量标准类型。1973年,新版本的UNIX发布了,其中90%以上的UNIX源代码都是用C语言重写的,这增强了它的可移植性。随着这个新版本UNIX的到来,计算社区意识到了C语言的强大功能。随着Brian Kernighan和Dennis Ritchie在1978年的《C程序设计语言》一书的出版,C语言一举成名。
1983年,美国国家标准协会(ANSI)成立了一个名为X3J11的委员会,以创建C语言的标准规格说明。1989年,该标准被批准为ANSI X3.159—1989“Programming Language C”。这个版本的C语言通常称为ANSI C、标准C或C89。1990年,国际标准化组织(ISO)采纳ANSI C标准(稍作修改),把它作为ISO/IEC 8999:1990发布。这个版本通常称为C90。1995年,X3J11委员会修改了C89,并增加了一个国际字符集。1999年,它被进一步修改并发布为ISO 9899:1999。该标准通常称为C99。2000年,它被采纳为ANSI标准。
1.1 程序、软件和操作系统
在继续之前,先来解释计算机程序一词的含义(以下简称程序)。程序只不过是要送到计算机上的一组指令,这样计算机就可以完成一些人们需要它完成的工作。程序和软件之间的关系可以表示如下:
程序+可移植性+文档+维护=软件
可移植性是指程序在不同平台(例如Windows平台、UNIX平台等)上运行的能力。文档表示用户手册和插入程序中的注释。维护意味着根据用户的请求调试和修改程序。
Microsoft Windows是一种操作系统。它包含一个图形用户界面(GUI)。图形意味着图像,界面意味着中间人,因此GUI是用户和帮助用户的计算机的内部机器(意味着计算机用户)之间的图像中间人。在酒店,服务员接受你的订单,走进厨房,收集你点的菜肴,并为你服务。同样,操作系统接受你的命令,接近计算机的内部机器,然后为你服务。
1.2 机器语言和汇编语言
微处理器可以恰当地描述为个人计算机的大脑。微处理器只不过是一个芯片。有各种微处理器可供选择。微处理器和中央处理单元(CPU)是同义词。微处理器包含一个称为算术逻辑单元(ALU)的重要组件,它执行所有计算。ALU的一个显著特征是它只能理解机器语言,而机器语言又只包含两个字母,即0和1(相比之下英语由26个字母组成)。这是典型的机器语言指令:
几十年前,程序员确实使用机器语言来编写程序。键盘只包含两个键,标着0和1。编写一个机器语言程序,然后在计算机中键入它是一项费力而乏味的工作。之后出现了汇编语言,它减轻了程序员的负担。汇编语言是低级语言。以下是典型的汇编语言语句(执行两个数字的乘法运算),这肯定比前面给出的机器语言指令更具可读性:
如果机器语言程序包含50个语句,那么相应的汇编语言程序也将包含大约50个语句。由于ALU仅理解机器语言,因此人们开发了专用软件(称为汇编器),以将汇编语言程序转换为机器语言程序。
1.3 过程式语言
典型的过程式语言比汇编语言更接近英语。例如,下面是过程式语言Pascal中的语句:
这个语句的含义非常明显:如果rollNumber的值为147,则在屏幕上显示消息“Entry denied.”。为了将过程式语言程序翻译成机器语言程序,人们使用称为编译器的软件。过程式语言是高级语言。
程序员将过程式语言与结构化编程技术结合使用。什么是结构化编程?从广义上讲,结构化编程这一术语指的是将编程艺术转化为理性科学的运动。这一切都始于Edsger Dijkstra在1968年3月出版的《Communications of the ACM》期刊上发表的“Go To Statement Considered Harmful”(Go To语句是有害的)。结构化编程依赖于以下基石:
□模块化:不是编写一个大程序,而是将程序拆分为多个子程序或模块。
□信息隐藏:模块的接口应仅显示尽可能少的信息。例如,考虑一个计算数字平方根的模块。该模块的接口将接受一个数字并返回此数字的平方根。此模块的详细信息将对此模块的用户隐藏。
□抽象:抽象是隐藏细节的过程,以便于理解复杂的系统。在某种程度上,抽象与信息隐藏有关。
然而,随着程序越来越大,很明显结构化编程技术虽然是必要的,但还不够。因此,计算机科学家转向面向对象编程,以便管理更复杂的项目。
1.4 面向对象的语言
我们使用计算机程序来解决现实问题。结构化范式的问题在于,无法使用它方便地在计算机上模拟实际问题。在结构化范式中,使用数据结构来模拟现实生活中的对象,但这些数据结构在模拟真实对象方面远远不够。汽车、房屋、狗和树是现实生活中对象的例子,我们期望编程语言能够模拟这些对象以解决现实生活中的问题。面向对象的范式简单地通过提供软件对象来模拟现实生活中的对象,从根本上解决了这个问题。面向对象范式提供的对象是类的实例,并拥有像现实生活中的对象那样的身份、属性和行为。例如,如果Bird(鸟)是一个类,那么parrot、peacock、 sparrow 和 eagle(鹦鹉、孔雀、麻雀和鹰)就是对象或Bird类的实例。此外,如果Mammal(哺乳动物)是一个类,那么cat、dog、lion和 tiger(猫、狗、狮子和老虎)是对象或Mammal类的实例。与结构化范式相比,面向对象范式更能够重用现有代码。代码是指程序或其中的一部分。
面向对象的范式与结构化范式一样古老。结构化范式的运动开始于1968年Dijkstra的著名的文章“Go To Statement Considered Harmful”,而面向对象范式自己的编程语言SIMULA 67出现于1967年,然而,SIMULA 67的面向对象能力不是很强。第一个真正面向对象的语言是Smalltalk。事实上,面向对象这个术语正是通过Smalltalk文献创造的。C不是面向对象的语言,它只是一种过程式语言。1983年,Bjarne Stroustrup为C语言添加了面向对象的功能,并将这种新语言命名为C++,这是计算机行业广泛使用和重视的第一种面向对象语言。今天,最流行的面向对象语言是Java。面向对象语言是高级语言。
1.5 计算机术语
在几乎所有科学中,术语都源自希腊语或拉丁语等语言。为什么?如果你从英语中导出术语,则存在技术含义与该术语的当前用法之间出现混淆的风险。然而,计算机科学中的术语源自英语,这会使初学者混淆。诸如树(tree)、内存(memory)、核心(core)、根(root)、文件夹(folder)、文件(file)、目录(directory)、病毒(virus)、蠕虫(worm)、垃圾(garbage)等英语单词被用作计算机领域中的技术术语。你可能不知道除了当前的非技术含义之外,特定术语还附带了一些技术含义。为避免混淆,请始终在桌面上备一本好的计算机词典。无论何时有疑问,都请查词典。
1.6 编译和解释语言
当计算机科学家设计新的编程语言时,主要问题是在各种平台上实现该语言。实现语言有两种基本方法:
□编译:高级语言的代码被翻译成低级语言。创建一个文件来存储编译或翻译后的代码。然后,你需要通过提供适当的命令来执行已编译的代码。
□解释:代码中的指令由虚拟机(或解释器)逐条解释(执行)。不创建文件。
现在详细讨论这两种方法。
编译
在编译方法中,高级语言的源代码被翻译成实际机器的机器语言。FORTRAN、Pascal、Ada、PL/1、COBOL、C和C++都是编译语言。例如,考虑一个在屏幕上显示文本“Hello”的C程序。假设hello.c是包含此程序源代码的文件(C源代码文件的扩展名为.c)。C编译器编译(或翻译)源代码并生成可执行文件hello.exe。文件hello.exe包含实际机器的机器语言指令。你现在需要通过提供适当的命令来执行文件hello.exe,并且执行文件hello.exe不是编译过程的一部分。在Windows平台上准备的可执行文件hello.exe只能在Windows平台上执行,根本无法在UNIX平台或Linux平台上执行此文件。但是,可以使用适用于所有平台的C编译器。因此,可以在UNIX或Linux平台上加载适当的C编译器编译文件hello.c,以生成可执行文件hello.exe,然后在该平台上执行它。
编译语言的主要好处是编译程序的执行速度很快。编译语言的主要缺点是程序的可执行版本依赖于平台。
解释
在解释方法中,通过添加期望数量的软件层来创建虚拟机,使得高级语言的源代码是该虚拟机的“机器语言代码”。例如,BASIC语言是一种解释语言。考虑一个在屏幕上显示文本“Hello”的BASIC程序。假设此程序的源代码存储在hello.bas文件中。hello.bas中的源代码被送到BASIC虚拟机,并且BASIC虚拟机逐条解释(执行)hello.bas中的指令。另请注意,hello.bas中的编程语句是BASIC虚拟机的机器语言指令。在解释过程中不会创建新文件。
解释语言的主要好处是程序与平台无关。解释语言的主要缺点是程序的解释(执行)很慢。BASIC、LISP、SNOBOL4、APL和Java都是解释语言。
在实践中,很少使用纯粹的解释,如BASIC的情况。在几乎所有解释语言(例如Java)中,都使用编译和解释的组合。首先,使用编译器将高级语言中的源代码转换为中间级代码。其次,创建虚拟机,使得上述中间级代码是该虚拟机的机器语言代码。然后将中间级代码送到虚拟机以进行解释(执行)。最后,请注意所有脚本语言(例如Perl、JavaScript、VBScript、AppleScript等)都是纯粹的解释语言。
1.7 第一个C程序
作为一种传统,典型的C编程书中的第一个程序通常是“Hello, world”程序。我们遵循这一传统,创建并运行(执行)第一个程序。此程序将在屏幕上显示“Hello, world”文本。在C文件中键入以下文本(程序)并将其保存在文件夹C:Code中名为hello.c的文件中:
编译并执行此程序,屏幕上会显示以下文本:
如果语言的编译器或解释器区分大写和小写字母,则称该语言为区分大小写的。Pascal和BASIC不是区分大小写的语言。C和C++是区分大小写的语言。
□C是区分大小写的语言,因此不应混淆大写和小写字母。例如,如果键入Main而不是main,则会导致错误。
□不要混淆文件名和程序名。这里,hello.c是包含程序源代码的文件的名称,而hello是程序名称。
为了解释这个程序(或者任何其他程序)是如何工作的,需要引用这个程序中的代码行(LOC),因此,需要对代码行进行编号。我重写了程序hello,其中添加了行号作为注释(这些是多行注释),如下所示。此程序产生与程序hello相同的输出。
C中有两种类型的注释:多行注释(也称为块注释)和单行注释(也称为行注释)。单行注释来自C++,自C99起正式引入C语言。
现在注意用插入单行注释重写的程序hello,如下所示。此程序产生与程序hello相同的输出。
传统上,C语言教科书仅使用多行注释并避免单行注释。我将在本书中遵循这一惯例。
1.8 C的突出特点
C是一种流行语言。它大受欢迎归功于以下功能:
□C是一种小型语言。它只有32个关键字。因此,可以很快学会它。
□它具有强大的内置函数库。
□它是一种可移植的语言。为一种平台(例如,Windows)编写的C程序可以移植到另一种具有微小变化的平台(例如,Solaris)。
□C程序执行速度快。因此,C程序用在效率很重要的地方。
□结构化编程所需的所有构造都可以在C中获得。
□C语言中提供了低级编程所需的大量构造,因此C可用于系统编程。
□C语言中提供指针,这增强了它的功能。
□在C中递归功能可用于解决棘手的问题。
□C具有扩展自身的能力。程序员可以将自己编写的函数添加到函数库中。
□C几乎是一种强类型语言。
1.9 隐式类型转换
在赋值语句中,右侧显示的量称为右值(r-value),左侧显示的量称为左值(l-value)。在每个赋值语句中,都要确保左值的数据类型与右值的数据类型相同。有关示例请参阅此处给出的赋值语句(假设intN为int变量):
这里,L1表示LOC 1,为了节省空间,我使用字母L来表示代码中的LOC。在LOC 1中,左值为intN,右值为350,它们的数据类型是相同的:int。当编译器编译这样的语句时,它从不会忘记检查赋值语句两边的类型。编译器的这个任务称为类型检查(typechecking)。如果双方的类型不一样会怎样?会发生类型转换!在类型转换中,右侧的值的类型在赋值之前被更改为左侧的值的类型。类型转换可以分为两类。
□隐式或自动类型转换
□显式类型转换
注意这里给出的LOC(假设dblN是double变量):
在此LOC中,dblN的类型为double,数值常量35的类型为int。这里,编译器将数据类型35从int(源类型)提升为double(目标类型),然后将double类型常量35.000000赋给dblN。这称为隐式类型转换或自动类型转换。在隐式(或自动)类型转换中,类型转换是自动发生的。
在类型转换中,右值的类型称为源类型,左值的类型称为目标类型。如果目标类型的范围宽于源类型的范围,则此类型的转换称为扩大类型转换。如果目标类型的范围窄于源类型的范围,则此类型的转换称为缩小类型转换。LOC 2中的类型转换是扩大类型转换,因为double(目标类型)的范围比int(源类型)的范围宽。
这是隐式类型转换的另一个例子(假设intN是int变量):
在此LOC中,数字常量14.85的类型是double,intN的类型是int。这里,编译器将14.85的数据类型从double降级为int,它截断并丢弃其小数部分,然后将整数部分14赋值给intN。LOC 3中的类型转换是缩小类型转换。
这是隐式类型转换的另一个例子:
在此LOC中,右值是一个表达式,该表达式又由数值常量2除以数值常量4.0组成。但是数值常量2的类型是int,而数值常量4.0的类型是double。这里,编译器将数值常量2的类型从int提升为double,然后执行浮点数2.0 / 4.0的除法。结果0.5被赋值给dblN。
■注意 在表达式或赋值语句中混合使用不同类型时,编译器会在计算表达式或赋值时执行自动类型转换。在执行类型转换时,编译器会尽力防止信息丢失。但有时候信息丢失是不可避免的。
例如,在LOC 3中,存在信息丢失(double类型数值常量14.85转换为int类型数值常量14)。在扩大类型转换中没有信息丢失,但在缩小类型转换时存在一些信息丢失。编译器总是允许扩大类型转换。编译器也允许缩小类型转换,但有时编译器会显示警告。不允许无意义的转换。某些类型转换在编译期间允许,但在运行期间会报告错误。例如,请注意这里给出的代码段:
编译器成功编译了这段代码,没有任何警告。但是,当你执行这段代码时,屏幕上会显示以下文本行而不是预期的输出:
程序在执行LOC M期间“崩溃”,这行代码尝试缩小类型转换。当一个程序在运行时突然终止时,我们用程序员的语言说程序崩溃了。
不同的语言允许将类型混合到不同的程度。允许不受限制地混合不同类型的语言称为弱类型的(weakly typed)语言或具有弱类型的语言。不允许混合不同类型的语言称为强类型的(strongly typed)语言或具有强类型的语言。
■注意 C几乎是一种强类型语言。
C的强类型检查在函数调用中很明显。如果函数需要一个int类型参数,并且将一个字符串作为参数(而不是int类型参数)传递给该函数,那么编译器会报告错误并停止编译程序,这证实了C是一种强类型语言。
请注意,前面使用了几乎这个术语,因为在某种程度上,C语言中允许隐式类型转换,这使得C成为“几乎”强类型的语言,而不是完全强类型的语言。
1.10 显式类型转换
你可以显式执行类型转换,而不是让类型转换完全依赖编译器。此操作称为显式类型转换、强制转换或强制。强制转换中使用的运算符称为强制转换运算符。注意这里给出的LOC(假设intN是一个int变量):
在此LOC中,对数值常量14.85执行强制转换操作。强制转换运算符是“(int)”。在此操作中,14.85的类型从double被更改为int,其小数部分被截断并丢弃,整数部分14作为int类型的数值常量返回,而int又被赋值给intN。以下是强制转换操作或显式类型转换的通用语法:
(desiredType) 表达式
这里,desiredType是任何有效的类型,如char、short int、int、long int、float、double等。在这种语法中,强制转换运算符是“(desiredType)”。请注意,括号是必需的,并且是转换运算符的一部分。此转换操作的效果是将表达式的类型更改为desiredType。
在LOC 1中,强制转换操作对数值常量执行,但是它也可以对变量执行。注意这里给出的代码片段:
执行后,这段代码在屏幕上显示以下文本行:
在这段代码中,对变量dblN执行了两次强制转换操作,第一次在LOC 4中执行,第二次在LOC 7中执行。请注意,在dblN上执行转换操作后,dblN的值不受影响。实际上,强制转换操作不在dblN上执行,存储在dblN中的值被获取,然后对该获取的值(即在数值常量3.7上)执行强制转换操作。也因此,在LOC 4中使用运算符“(int)”对dblN执行强制转换操作后,变量dblN在执行LOC 6后仍然不受影响。LOC 6的执行将dblN的值显示为3.7。在LOC 7中,printf()函数的参数不是变量而是表达式,如下所示:
在这一章里,我们讨论了与C语言相关的各种问题。在本书的其余章节中,你将看到所有的C技巧。本书的目的是为你提供现成的解决方案,在本书中,你还可以找到满足各种水平读者需求的现成解决方案。