C---编程学习手册-全-
C++ 编程学习手册(全)
一、基本编程概念
Electronic supplementary material The online version of this chapter (doi:10.1007/978-1-4842-1371-1_1) contains supplementary material, which is available to authorized users.
在本章中,我们将解释以下内容:
- 计算机如何解决问题
- 计算机程序开发的各个阶段:从问题定义到完成程序
- 计算机如何执行程序
- 什么是“数据类型”及其在编写程序中的基本作用
- 角色——所有程序的基本构件
- 常量和变量的概念
- 语法错误和逻辑错误的区别
- 如何使用
printf
语句产生 C 语言的基本输出 - 什么是转义序列
- 如何在你的程序中加入描述性或解释性的注释
- 什么是赋值语句以及如何用 C 语言编写一个赋值语句
1.1 程序、语言和编译器
我们都熟悉计算机执行各种任务的能力。例如,我们可以用它来玩游戏、写信或写书、为公司做会计工作、学习外语、听 CD 上的音乐、发传真或在互联网上搜索信息。这怎么可能,都在同一台机器上?答案在于编程——创建一系列计算机可以执行的指令(我们称之为“执行”)来完成每项任务。这个指令序列被称为程序。每个任务需要不同的程序:
- 要玩游戏,我们需要一个玩游戏的程序。
- 要写一封信或一本书,我们需要一个文字处理程序。
- 要做账,我们需要一个会计程序。
- 为了学习西班牙语,我们需要一个教授西班牙语的项目。
- 要听 CD,我们需要一个音乐播放程序。
- 要发送传真,我们需要一个传真发送程序。
- 要使用互联网,我们需要一个叫做“网络浏览器”的程序
对于我们想要执行的每一项任务,我们都需要一个合适的程序。为了让计算机运行一个程序,这个程序必须被存储(我们有时称之为加载)在计算机的内存中。
但是程序的本质是什么呢?首先,我们需要知道计算机是用来执行用所谓的机器语言编写的指令的。在机器语言中,一切都用二进制数字系统来表示——1 和 0。每台计算机都有自己的机器语言,并且计算机只能执行用那种语言编写的指令。
指令本身非常简单:例如,将两个数字相加或相减,将一个数字与另一个数字进行比较,或者将一个数字从一个地方复制到另一个地方。那么,计算机怎么能用如此简单的指令完成如此多种多样的任务,解决如此多种多样的问题呢?
答案是,无论一项活动看起来有多复杂,通常都可以分解成一系列简单的步骤。分析一个复杂问题并以简单的计算机指令表达其解决方案的能力是优秀程序员的标志之一。
机器语言被认为是一种低级编程语言。在计算的早期(20 世纪 40 年代和 50 年代),程序员必须用机器语言编写程序,也就是说,用 1 和 0 来表达他们所有的指令。
为了让他们的生活稍微轻松一点,汇编语言被开发出来。这与机器语言密切相关,但它允许程序员使用助记指令代码(如ADD
和存储位置名称(如sum
)而不是二进制数字串(位)。例如,程序员可以通过sum
引用一个数字,而不必记住这个数字存储在内存位置1000011101101011
。
一种叫做汇编程序的程序被用来把汇编语言程序转换成机器语言。然而,这种编程方式有几个缺点:
- 这非常乏味,而且容易出错。
- 它迫使程序员从机器的角度去思考,而不是从他的问题的角度去思考。
- 用一台计算机的机器语言编写的程序不能在使用不同机器语言的计算机上运行。更换你的电脑可能意味着你必须重写所有的程序。
为了克服这些问题,在 20 世纪 50 年代末和 60 年代开发了高级语言或面向问题的语言。其中最流行的是 FORTRAN(公式翻译)和 COBOL(面向商业的通用语言)。FORTRAN 是为解决涉及大量数值计算的科学和工程问题而设计的。COBOL 是为解决商业团体的数据处理问题而设计的。
这个想法是让程序员用他熟悉的和与问题相关的术语来思考问题,而不是担心机器。例如,如果他想知道两个量中较大的一个,A
和B
,他可以写
IF A IS GREATER THAN B THEN BIGGER = A ELSE BIGGER = B
而不是摆弄几个机器或汇编语言指令来得到相同的结果。因此,高级语言使程序员能够集中精力解决手头的问题,而不必担心特定机器的特性。
然而,计算机仍然只能执行用机器语言编写的指令。一种叫做编译器的程序被用来把用高级语言编写的程序翻译成机器语言。
因此,我们说 FORTRAN 编译器或 COBOL 编译器分别用于翻译 FORTRAN 和 COBOL 程序。但这还不是故事的全部。由于每台计算机都有自己的机器语言,我们必须有,比如说,一台 FORTRAN 编译器用于联想 ThinkPad 计算机,一台 FORTRAN 编译器用于 MacBook 计算机。
1.2 计算机如何解决问题
在计算机上解决问题涉及以下活动:
Define the problem. Analyze the problem. Develop an algorithm (a method) for solving the problem. Write the computer program that implements the algorithm. Test and debug (find the errors in) the program. Document the program. (Explain how the program works and how to use it.) Maintain the program.
这些活动通常有些重叠。例如,对于一个大的程序,一部分可以在另一部分被写入之前被写入和测试。此外,文件应与所有其他活动同时进行;每项活动都有自己的文档,这些文档将成为最终项目文档的一部分。
定义问题
假设我们想帮助一个孩子算出正方形的面积。这就定义了一个要解决的问题。然而,一个简短的分析表明,这个定义不够完整或具体,不足以继续开发一个程序。与孩子交谈可能会发现她需要一个程序,要求她输入正方形一边的长度;然后程序打印出正方形的面积。
1.2.2 分析问题
我们进一步分析这个问题,以
- 确保我们对它有最清晰的理解。
- 确定一般要求,如程序的主要输入和程序的主要输出。例如,对于更复杂的程序,我们还需要决定可能需要的文件种类。
如果有几种解决这个问题的方法,我们应该考虑这些选择,并选择最好或最合适的一个。
在这个例子中,程序的输入是正方形一边的长度,输出是正方形的面积。我们只需要知道如何计算面积。如果边长为s
,则面积a
计算如下:
a = s × s
1.2.3 开发解决问题的算法
算法是一组指令,如果忠实地执行,将产生给定问题的解决方案或执行一些特定的任务。当一个指令被遵循时,我们说它被执行了。我们可以谈论在字典中查找单词、更换扎破的轮胎或玩视频游戏的算法。
对于任何问题,通常都有不止一种算法来解决。每种算法都有自己的优缺点。当我们在字典中查找一个单词时,一种方法是从开头开始,依次查看每个单词。第二种方法是从末尾开始向后搜索。这里,第一种方法的优点是,如果单词在开头,它会更快地找到该单词,而如果单词接近结尾,第二种方法会更快。
另一种搜索单词的方法是利用字典中的单词是按字母顺序排列的这一事实,这是我们在字典中查找单词时都使用的方法。在任何情况下,程序员通常都可以选择算法,而决定哪种算法是最好的,以及为什么这样,是她更重要的工作之一。
在我们的例子中,我们必须以这样一种方式编写算法中的指令,使得它们可以容易地转换成计算机可以遵循的形式。计算机指令分为三大类:
Input instructions, used for supplying data from the “outside world” to a program; this is usually done via the keyboard or a file. Processing instructions, used for manipulating data inside the computer. These instructions allow us to add, subtract, multiply, and divide; they also allow us to compare two values, and act according to the result of the comparison. Also, we can move data from one location in the computer’s memory to another location. Output instructions, used for getting information out of the computer to the outside world.
1.2.3.1 数据和变量
所有的计算机程序,除了最琐碎的程序,都是用来操作数据的。例如:
- 动作游戏的数据可能是按下的键或鼠标点击时光标的位置。
- 文字处理程序的数据是你打字时按下的键。
- 会计程序的数据包括支出和收入等。
- 教授西班牙语的程序的数据可以是你在回答问题时键入的一个英语单词。
回想一下,程序必须存储在计算机的内存中才能运行。当数据被提供给程序时,该数据也被存储在存储器中。因此,我们认为内存是保存程序和数据的地方。用高级语言(相对于机器语言)编程的一个好处是,你不必担心哪些内存位置用于存储数据。但是,假设内存中可能有许多数据项,我们如何引用一个数据项呢?
把内存想象成一组盒子(或存储位置)。每个盒子可以容纳一项数据,例如一个数字。我们可以给一个盒子起一个名字,并且我们可以通过这个名字来引用这个盒子。在我们的例子中,我们将需要两个盒子:一个用来保存正方形的边,一个用来保存面积。我们将分别称这些盒子为s
和a
。
如果我们愿意,我们可以随时改变盒子里的值;由于值可以变化,s
和a
被称为变量名,或简称为变量。因此,变量是一个与特定内存位置相关的名称,或者,如果你愿意,它是一个内存位置的标签。我们可以说给一个变量一个值,或者给一个变量设置一个特定的值,比如1
。需要记住的要点是:
- 一个盒子一次只能容纳一个值;如果我们输入一个新值,旧值就会丢失。
- 除非我们明确地在盒子中存储一个值,否则我们不能假设盒子中包含任何值。特别是,我们不能假定盒子里装的是零。
变量是计算机程序的一个常见特征。很难想象没有它们编程会是什么样子。在日常生活中,我们经常使用变量。例如,我们说一个“地址”在这里,“地址”是一个变量,其值取决于所考虑的人。其他常见的变量有电话号码、学校名称、学科、人口规模、汽车类型、电视型号等。(这些变量有哪些可能的值?)
1.2.3.2 示例—开发算法
使用算法的概念和变量的概念,我们开发了以下算法,用于计算给定一边的正方形的面积:
给定一条边,计算正方形面积的算法:
Ask the user for the length of a side. Store the value in the box s
. Calculate the area of the square (s
× s
). Store the area in the box a
. Print the value in box a
, appropriately labeled. Stop.
当一个算法被开发出来时,必须对它进行检查,以确保它能正确地完成预期的工作。我们可以通过“玩电脑”来测试一个算法,也就是说,我们用适当的数据值手工执行指令。这个过程被称为模拟运行或案头检查算法。它用于在计算机程序实际编写之前查明任何逻辑错误。除非我们确信算法是正确的,否则我们永远不应该开始编写编程代码。
1.2.4 为算法编写程序
我们已经使用英语语句指定了算法。然而,这些语句足够“面向计算机”,计算机程序可以直接从它们中编写。在我们这样做之前,让我们从用户的角度看看我们期望程序如何工作。
首先,程序将输入一条边的长度要求;我们说程序提示用户提供数据。屏幕显示可能如下所示:
Enter length of side:
然后,计算机将等待用户输入长度。假设用户输入12
。显示将如下所示:
Enter length of side: 12
然后,程序将接受(我们称之为读取)输入的数字,计算面积,并打印结果。显示可能如下所示:
Enter length of side: 12
Area of square is 144
这里我们指定了程序的输出应该是什么样子。例如,在提示行和给出答案的行之间有一个空行;我们还指定了答案的确切形式。这是一个简单的输出设计的例子。这是必要的,因为程序员无法编写程序,除非他知道所需的精确输出。
为了根据算法编写计算机程序,必须选择合适的编程语言。我们可以把一个程序想象成一套用编程语言编写的指令,当执行时,它将产生一个给定问题的解决方案或执行一些特定的任务。
算法和程序之间的主要区别是,算法可以用非正式语言编写,而不必遵循任何特殊规则(尽管通常遵循一些约定),而程序是用编程语言编写的,必须遵循该语言的所有规则(语法规则)。(同样,如果我们希望写出正确的英语,我们必须遵循英语的语法规则。)
在本书中,我们将向您展示如何用 C 语言编写程序,C 语言是由贝尔实验室的 Ken Thompson 和 Dennis Ritchie 开发的编程语言,也是当今最流行和广泛使用的语言之一。
程序 P1.1 是一个 C 程序,要求用户输入一条边的长度并打印正方形的面积:
Program P1.1
#include <stdio.h>
int main() {
int a, s;
printf("Enter length of side: ");
scanf("%d", &s); //store length in s
a = s * s; //calculate area; store in a
printf("\nArea of square is %d\n", a);
}
此时,你是否了解这个项目并不太重要。但是你可以观察到一个 C 程序有一个叫做main
的东西(一个函数)后面跟有开括号和闭括号。在左括号{
和右括号}
之间,我们有所谓的函数体。声明
int a, s;
叫做宣言。//
后的部分是帮助解释程序但在程序运行时没有作用的注释。而*
用来表示乘法。
所有这些术语将在适当的时候详细解释。
最后,用高级语言编写的程序通常被称为源程序或源代码。
1.2.5 测试和调试程序
写完程序后,接下来的工作是测试它,看看它是否完成了预定的工作。测试程序包括以下步骤:
Compile the program: recall that a computer can execute a program written in machine language only. Before the computer can run our C program, the latter must be converted to machine language. We say that the source code must be converted to object code or machine code. The program that does this job is called a compiler. Appendix D tells you how you can acquire a C compiler for writing and running your programs. Among other things, a compiler will check the source code for syntax errors—errors that arise from breaking the rules for writing statements in the language. For example, a common syntax error in writing C programs is to omit a semicolon or to put one where it is not required. If the program contains syntax errors, these must be corrected before compiling it again. When the program is free from syntax errors, the compiler will convert it to machine language and we can go on to the next step. Run the program: here we request the computer to execute the program and we supply data to the program for which we know the answer. Such data is called test data. Some values we can use for the length of a side are 3
, 12
, and 20
. If the program does not give us the answers 9, 144, and 400, respectively, then we know that the program contains at least one logic error. A logic error is one that causes a program to give incorrect results for valid data. A logic error may also cause a program to crash (come to an abrupt halt). If a program contains logic errors, we must debug the program; we must find and correct any errors that are causing the program to produce wrong answers.
举例来说,假设计算面积的语句被(不正确地)写成:
a = s + s;
当程序运行时,输入10
作为长度。(下面,10
加下划线表示是用户键入的。)假设我们知道这个地区应该是100
。但是当程序运行时,它会打印如下内容:
Enter length of side:
10
Area of square is 20
由于这不是我们期望的答案,我们知道程序中有一个错误(可能不止一个)。因为面积是错误的,所以从计算面积的语句开始查找错误是合乎逻辑的。如果我们仔细观察,应该会发现输入的是+
而不是*
。当这个修正完成后,程序运行良好。
记录该计划
最后的工作是完成程序的文档。到目前为止,我们的文档包括以下内容:
- 问题的陈述。
- 解决问题的算法。
- 节目单。
- 测试数据和程序产生的结果。
这些是构成程序技术文档的一些项目。这是对程序员有用的文档,也许是为了在以后的阶段修改程序。
另一种必须编写的文档是用户文档。这使得非技术人员无需了解程序的内部工作就能使用程序。其中,用户需要知道如何在计算机中加载程序,以及如何使用程序的各种功能。如果合适的话,用户还需要知道如何处理程序使用过程中可能出现的异常情况。
维护程序
除了像课堂作业这样的事情,程序通常意味着要使用很长一段时间。在此期间,可能会发现以前没有注意到的错误。错误也可能因为以前从未出现过的条件或数据而出现。不管什么原因,这种错误必须纠正。
但是程序可能因为其他原因需要修改。也许编写程序时所做的假设现在已经由于公司政策的改变或者甚至由于政府法规的改变(例如,所得税税率的改变)而改变了。也许公司正在改变它的计算机系统,程序需要“移植”到新系统。我们说程序必须被“维护”
这是否容易做到很大程度上取决于原始程序是如何编写的。如果它设计良好并有适当的文档记录,那么维护程序员的工作就会变得容易得多。
1.3 计算机如何执行程序
首先,回想一下,计算机只能执行用机器语言编写的程序。为了让计算机执行这样一个程序的指令,这些指令必须被载入计算机的内存(也称为主存储器),就像这样:
你可以把内存想象成一系列的存储单元,从0
开始连续编号。因此,你可以说内存位置27
或内存位置31548
。与一个存储单元相关的数字叫做它的地址。
计算机通过执行第一条指令来运行程序,然后是第二条,第三条,依此类推。一条指令可能会说要跳过几条指令,跳到一条特定的指令,然后从那里继续执行。另一个人可能会说回到上一条指令并再次执行它。
无论指令是什么,计算机都会忠实地按照指定的方式执行它们。这就是为什么程序精确地指定必须做什么是如此重要。计算机无法知道你的意图,它只能执行你实际写的东西。如果你给计算机下了错误的指令,它就会盲目地按照你指定的方式执行。
1.4 数据类型
每天我们都会遇到名字和数字——在家里,在工作中,在学校,或者在玩耍中。人名是一种数据类型;一个数字也是。因此,我们可以称这两种数据类型为“名称”和“数字”考虑以下陈述:
Caroline bought 3 dresses for $199.95
在这里,我们可以发现:
- 一个名字的例子:
Caroline
。 - 数字的两个例子:
3
和199.95.
通常,我们发现把数字分成两种很方便:
Whole numbers, or integers. Numbers with a decimal point, so-called real or floating-point numbers.
在示例中,3
是整数,199.95
是实数。
Exercise: Identify the data types—names, integers, and real numbers—in the followingBill’s batting average was 35.25
with a highest score of 99
. Abigail, who lives at 41
Third Avenue, worked 36
hours at $11.50
per hour. In his 8
subjects, Richard’s average mark was 68.5
.
一般来说,编写程序是为了处理各种类型的数据。我们使用数字这个术语来指代数字(整数或浮点)。我们使用术语字符串来指代非数字数据,如姓名、地址、工作描述、歌曲名称或车辆号码(就计算机而言,这并不是真正的数字,它通常包含字母,例如PAB6052
)。
一般来说,编程语言,特别是 C,精确地定义了用这些语言编写的程序可以操作的各种类型的数据。整数、实数(或浮点)、字符(由单个字符组成的数据,如'K'
或'%'
)和字符串数据类型是最常见的。
每种数据类型都定义了该类型的常量。例如,
- 一些整数常量有
3
、-52
、0
和9813
。 - 一些实(或浮点)常量是
3.142
、-5.0
、345.21
和1.16
。 - 一些字符常量有
't'
、'+'
、'8'
和'R'
。 - 一些字符串常量是
"Hi there", "Wherefore art thou, Romeo?"
和"C World"
。
请注意,在 C 中,字符常量由单引号分隔,字符串常量由双引号分隔。
当我们在程序中使用一个变量时,我们必须说明我们打算在这个变量中存储什么类型的数据(常量的种类)——我们说我们必须声明这个变量。如果我们将一个变量声明为一种类型,然后试图在其中存储不同类型的值,这通常是错误的。例如,试图在整数变量中存储字符串常量是错误的。c 数据类型在第二章中详细讨论。
1.5 个字符
在计算机术语中,我们使用术语“字符”来指代以下任一项:
- 从
0
到9
的一个数字。 - 从
A
到Z
的一个大写字母。 - 从
a
到z
的小写字母。 - 特殊符号如(,,$,=,,+,-,/,*等。
以下是常用术语:
- 字母–从
a
到z
或从A
到Z
中的一个 - 小写字母–从
a
到z
中的一个 - 大写字母–从
A
到Z
中的一个 - 数字–为
0
、1
、2
、3
、4
、5
、6
、7
、8
、9
中的一种 - 特殊字符–除字母或数字之外的任何符号,例如
+
、<
、$
、&
、*
、/
、=
- 字母——用来指一个字母
- 数字–用来指一个数字
- 字母数字–用于指代字母或数字
字符是编写程序时使用的基本构件。
我们把字符放在一起形成变量和常量。
我们将变量、常量和特殊字符组成表达式,如
(a + 2.5) * (b – c);
我们添加一些特殊的词,如if
、else
和while
来形成陈述,如
if (a > 0) b = a + 2; else b = a – 2;
我们把语句放在一起组成程序。
1.6 欢迎学习 C 编程
我们通过编写一个打印消息的程序来快速浏览一下 C 编程语言
Welcome to Trinidad & Tobago
一个解决方案是程序 P1.2。
Program P1.2
#include <stdio.h>
int main() {
printf("Welcome to Trinidad & Tobago");
}
The statement
#include <stdio.h>
称为编译器指令。这仅仅意味着它提供了编译器编译你的程序所需的信息。在 C 语言中,输入输出指令是通过存储在标准库中的标准函数来提供的。这些函数使用存储在名为stdio.h
的特殊头文件中的变量(和其他)声明。任何使用输入/输出指令(如printf
)的程序必须通知编译器将声明包含在程序的文件stdio.h
中。如果不这样做,编译器将不知道如何解释程序中使用的输入/输出语句。
一个 C 程序由一个或多个函数(或子程序)组成,其中一个函数必须被称为main
。我们的解决方案只包含一个函数,所以它必须被称为main
。main
后面的(圆)括号是必要的,因为在 C 中,函数名后面是一列参数,用括号括起来。如果没有参数,括号必须仍然存在。这里,main
没有参数,所以只有括号。main
前的int
表示main
返回的值的类型。稍后我们将对此进行更详细的解释。
每个函数都有一个叫做函数体的部分。身体是执行功能工作的地方。左右括号{
和}
分别用于定义主体的起点和终点。在 C 语言中,由{
和}
括起来的一条或多条语句称为块语句或复合语句。
main
的正文包含一条语句:
printf("Welcome to Trinidad & Tobago");
printf
是一个标准的输出函数,在这个例子中,它接受一个参数,一个字符串常量"Welcome to Trinidad & Tobago"
。请注意,与所有函数一样,该参数用圆括号括起来。分号用于表示语句的结束。我们说分号终止语句。执行时,该语句将打印
Welcome to Trinidad & Tobago
关于“标准输出”现在,把这个理解为屏幕。
Programming Note
正如在序言中提到的,生活中令人兴奋的事情之一是编写你的第一个程序,并让它在计算机上成功运行。不要错过它。有关如何获得 C 编译器的说明,请参见附录 D。
运行程序
把程序写在纸上后,下一个任务是让它在真正的计算机上运行。如何做到这一点在不同的计算机系统之间有所不同,但一般来说,必须执行以下步骤:
Type the program to a file. The file could be named welcome.c
; it is good practice to use .c
as the filename extension to those files that contain C source code. Invoke your C compiler to compile the program in the file welcome.c
. For instance, you may have to start up your C compiler and open the file welcome.c
from the File
menu or you may simply have to double-click on the file welcome.c
to start up the compiler. Once the file is open, typically there will be a menu command to Compile
or Run
the program. (Generally, Run
implies Compile
and Run
). If any (syntax) errors are detected during the compile phase, you must correct these errors and try again. When all errors have been corrected and the program is Run
, it will print
Welcome to Trinidad & Tobago
1.6.2 程序布局概述
c 不要求程序像示例中那样进行布局。一个等效的程序是
#include <stdio.h>
int main() { printf("Welcome to Trinidad & Tobago"); }
或者
#include <stdio.h>
int main()
{
printf("Welcome to Trinidad & Tobago");
}
对于这个小程序,我们使用哪个版本可能并不重要。然而,随着程序大小的增加,程序的布局突出程序的逻辑结构变得非常必要。
这提高了它的可读性,使它更容易理解。缩进和清楚地指出哪个{
匹配哪个}
在这方面会有帮助。随着我们的项目越来越大,我们将会看到这一原则的价值。
1.7 用printf
写输出
假设我们想写一个程序来打印罗宾德拉纳特·泰戈尔的《吉檀迦利》中的以下几行:
Where the mind is without fear
And the head is held high
我们最初的尝试可能是这样的:
#include <stdio.h>
int main() {
printf("Where the mind is without fear");
printf("And the head is held high");
}
但是,当运行时,该程序将打印:
Where the mind is without fearAnd the head is held high
请注意,这两个字符串是连接在一起的(我们说字符串是连接在一起的)。发生这种情况是因为printf
不会将输出放在新行上,除非明确指定。换句话说,printf
在打印其参数后不会自动提供换行符。换行符会导致后续输出从下一行的左边开始。
在这个例子中,在打印出fear
之后没有提供换行符,因此And the head...
与fear
打印在同一行,并且紧接在它之后。
1.7.1 换行符,\n
(反斜杠 n)
为了获得想要的效果,我们必须告诉printf
在打印...without fear
之后提供一个换行符。我们通过使用程序 P1.3 中的字符序列\n
(反斜杠n
)来做到这一点
Program P1.3
#include <stdio.h>
int main() {
printf("Where the mind is without fear\n");
printf("And the head is held high\n");
}
第一个\n
表示终止当前输出线;后续输出将从下一行的左边距开始。这样,And the...
将被打印在新的一行上。第二个\n
具有终止第二行的效果。如果它不存在,输出仍然正确,但只是因为这是最后一行输出。
一个程序在终止前打印所有的挂起输出。(这也是我们第一个程序没有\n
也能工作的原因。)
作为修饰,假设我们想要在两行输出之间放置一个空行,如下所示:
Where the mind is without fear
And the head is held high
下面的每一组语句都将实现这一点:
printf("Where the mind is without fear\n\n");
printf("And the head is held high\n");
printf("Where the mind is without fear\n");
printf("\nAnd the head is held high\n");
printf("Where the mind is without fear\n");
printf("\n");
printf("And the head is held high\n");
我们只需要确保在fear
和And
之间打印两个\n
。第一个\n
结束第一行;第二行结束第二行,实际上是打印一个空行。在如何编写语句以产生预期效果方面,c 语言给了我们很大的灵活性。
练习:写一个程序打印你最喜欢的歌曲的歌词
转义序列
在printf
的字符串参数中,反斜杠(\
)表示此时需要一个特殊效果。反斜杠后面的字符指定要做什么。这种组合(\
后跟另一个字符)被称为转义序列。以下是一些可以在printf
语句的字符串中使用的转义序列:
\n issue a newline character
\f issue a new page (form feed) character
\t issue a tab character
\" print "
\\ print \
例如,使用转义序列是将双引号作为输出的一部分打印出来的唯一方法。假设我们想打印这一行
Use " to begin and end a string
如果我们打字
printf("Use " to begin and end a string\n");
然后 C 会假设Use
后的双引号结束字符串(当它想不出如何处理to
时会导致后续错误)。使用转义序列\"
,我们可以正确地打印出这样一行:
printf("Use \" to begin and end a string\n");
练习:编写一条语句来打印该行:转义序列以\
打印变量的值
到目前为止,我们已经使用了printf
来打印一个字符串常量的值(即字符串中不包括引号的字符)。我们现在展示如何打印变量的值,暂时忽略变量是如何获得其值的。(我们将在第二章中看到。)假设整数变量m
的值为52
。声明:
printf("The number of students = %d\n", m);
将打印以下内容:
The number of students = 52
这个printf
和我们目前看到的有些不同。这个有两个参数——一个字符串和一个变量。这个字符串称为格式字符串,包含一个格式规范%d
。(在我们前面的例子中,格式字符串不包含格式规范。)在这种情况下,除了用第二个参数m
的值替换了%d
之外,格式字符串的输出和以前一样。因此,%d
被替换为52
,给出如下结果:
The number of students = 52
我们将在第二章中更详细地解释printf
和格式规范,但是,现在请注意,如果我们想要打印一个整数值,我们将使用规范%d
。
如果我们想要打印多个值呢?只要每个值都有相应的格式规范,就可以做到这一点。例如,假设a
的值为14
,而b
的值为25
。考虑以下陈述:
printf("The sum of %d and %d is %d\n", a, b, a + b);
这个printf
有四个参数——格式字符串和三个要打印的值:a
、b,
和a+b
。格式字符串必须包含三个格式说明:第一个对应a
,第二个对应b
,第三个对应a+b
。当打印格式字符串时,每个%d
将被其相应参数的值替换,给出如下:
The sum of 14 and 25 is 39
Exercise: What is Printed by the Following Statement?
printf("%d + %d = %d\n ",a,b,a+b);
1.8 意见
所有的编程语言都允许你在程序中加入注释。注释可以用来提醒你自己(和其他人)正在进行什么处理,或者某个特定变量的用途。它们可以用来解释或阐明一个程序的任何方面,这些方面可能仅仅通过阅读编程语句是难以理解的。这非常重要,因为程序越容易理解,你就越有信心它是正确的。添加任何使程序更容易理解的东西都是值得的。
请记住,注释(或没有注释)对程序的运行没有任何影响。如果你从一个程序中删除所有的注释,它将会以和注释完全一样的方式运行。
每种语言都有自己的方式来指定注释的书写方式。在 C 语言中,我们通过将注释包含在/*
和*/
中来编写注释,例如:
/* This program prints a greeting */
一个注释从/*
延伸到下一个*/
,可能跨越一行或多行。以下是有效的注释:
/* This program reads characters one at a time
and counts the number of letters found */
c 也允许你使用//
来写一行注释。注释从//
延伸到行尾,例如:
a = s * s; //calculate area; store in a
在本书中,我们将主要使用单行注释。
1.9 使用变量编程
为了加强到目前为止所讨论的观点,让我们编写一个程序,将数字14
和25
相加并打印出总和。
我们需要两个数和总和的存储位置。存储在这些位置的值是整数值。为了指代这些地点,我们编造了一些名字,比如说a
、b
和sum
。(其他名字都可以。与所有编程语言一样,在 C 语言中,组成变量名也有一些规则要遵循,例如,名称必须以字母开头,不能包含空格。我们将在下一章看到 C 规则。)
一种可能的算法如下所示:
set a to 14
set b to 25
set sum to a + b
print sum
该算法由四条语句组成。下面解释了每个语句的含义:
set a to 14
:将数字14
存储在存储位置a
;这是一个赋值语句的例子。set b to 25
:将数字25
存储在b
的存储位置。set sum to a + b
:将存储位置a
和b
中的数字相加,并将总和存储在位置sum
中。结果是39
被存储在sum
中。print sum
:打印(在屏幕上)出sum
中的数值,即39
。
程序 P1.4 展示了我们如何将这个算法写成一个 C 程序。
Program P1.4
//This program prints the sum of 14 and 25\. It shows how
//to declare variables in C and assign values to them.
#include <stdio.h>
int main() {
int a, b, sum;
a = 14;
b = 25;
sum = a + b;
printf("%d + %d = %d\n", a, b, sum);
}
运行时,该程序将打印以下内容:
14 + 25 = 39
在 C 中,变量被声明为整数,使用必需的字int
。(在编程术语中,我们说int
是保留字。)因此,声明
int a, b, sum;
声明a
、b
和sum
是整数变量。在 C 语言中,所有变量在程序中使用之前都必须声明。请注意,变量由逗号分隔,最后一个变量后有分号。如果我们只需要声明一个变量(a
),我们将编写
int a;
声明
a = 14;
是 C 写赋值语句的方式
set a to 14
有时会读作“a 变成 14。”在 C 语言中,赋值语句包括一个变量(【示例中的 ),后面是一个等号(=
),后面是要赋给变量的值(示例中的14
),再后面是一个分号。一般来说,值可以是常量(如14
)、变量(如b
)或表达式(如a + b
)。因此,
set b to 25
被写成
b = 25;
和
set sum to a + b
被写成
sum = a + b;
最后一点:您可能已经从之前的练习中了解到,对于这个问题,变量sum
并不是真正必要的。例如,我们可以从程序中完全省略掉sum
,使用下面的代码:
int a, b;
a = 14;
b = 25;
printf("%d + %d = %d\n", a, b, a + b);
给出相同的结果,因为 C 让我们使用一个表达式(例如,a + b
)作为printf
的参数。然而,如果程序比较长,我们需要在其他地方使用这个总数,明智的做法是计算并存储一次总数(比如说在sum
)。每当需要求和时,我们就使用sum
,而不是每次都重新计算a + b
。
现在我们已经对编写程序涉及的内容有了一个大致的概念,我们准备开始深入 C 编程的本质。
Exercises 1What makes it possible to do such a variety of things on a computer? Computers can execute instructions written in what language? Give two advantages of assembly language over machine language. Give two advantages of a high-level language over assembly language. Describe two main tasks performed by a compiler. Describe the steps required to solve a problem on a computer. Distinguish between an algorithm and a program. Programming instructions fall into three main categories; what are they? Distinguish between a syntax error and a logic error. What is meant by “debugging a program”? Name five data types commonly used in programming and give examples of constants of each type. What are the different classes into which characters can be divided? Give examples in each class. What is the purpose of comments in a program? Write a program to print Welcome to C
on the screen. Write a program to print the following: There is a tide in the affairs of men
Which, taken at the flood, leads on to fortune
Write a program to print any four lines of your favorite song or poem. Same as exercise 16, but print a blank line after each line. If a
is 29
and b
is 5
, what is printed by each of the following statements? printf("The product of %d and %d is %d\n", a, b, a * b);
printf("%d + %d = %d\n", a, b, a + b);
printf("%d - %d = %d\n", a, b, a - b);
printf("%d x %d = %d\n", a, b, a * b);
If a
is 29
and b
is 14
, what is printed by the following statements? printf("%d + \n", a);
printf("%d\n", b);
printf("--\n");
printf("%d\n", a + b);
If rate
= 15
, what is printed by (a) printf("rate\n")?
(b) printf("%d\n", rate)?
二、C 基础知识
在本章中,我们将解释以下内容:
- 什么是字母表、字符集和令牌
- 什么是语法规则和语法错误
- 什么是保留字
- 如何在 C 中创建标识符
- 什么是符号常量
- C 数据类型—
int
、float
和double
- 如何写
int
和double
表达式 - 如何使用字段宽度打印整数
- 如何将浮点数打印到所需的小数位数
- 当
int
和double
值混合在同一个表达式中时会发生什么 - 当我们将
int
分配给double
并将double
分配给int
时会发生什么 - 如何声明变量来保存字符串
- 如何将字符串值赋给字符串变量
- 使用赋值语句时要避免的一些问题
2.1 导言
在这一章中,我们将讨论一些用 C 语言编写程序时你需要知道的基本概念。
编程语言在许多方面类似于口语。它有一个字母表(通常称为字符集),语言中的所有内容都是从这个字母表构建的。它有构成单词(也称为记号)的规则、构成语句的规则和构成程序的规则。这些被称为语言的语法规则,在编写程序时必须遵守。如果你违反了规则,你的程序将包含一个语法错误。当你试图编译程序时,编译器会通知你错误。您必须更正并重试。
成为优秀程序员的第一步是学习编程语言的语法规则。这是容易的部分,许多人错误地认为这使他们成为程序员。这就像说,学习一些英语语法规则,并能够写出一些正确形成的句子,使一个人成为小说家。小说写作技巧需要的不仅仅是学习一些语法规则。除此之外,它还需要洞察力、创造力和在特定情况下使用正确词汇的诀窍。
同样,一个好的程序员必须能够创造性地使用语言的特性,以优雅而高效的方式解决各种各样的问题。这是困难的部分,只有通过长期、艰苦地研究解决问题的算法和编写程序来解决广泛的问题才能实现。但是我们必须从小步开始。
2.2 字母表
在 1.4 节中,我们介绍了角色的概念。我们可以把 C 字母表看作是由所有可以在标准英语键盘上输入的字符组成的:例如,数字;大写和小写字母;以及+
、=
、<
、>
、&
、%
等特殊字符。
更正式的说法是,C 使用 ASCII(美国信息交换标准代码,发音为ass-key
)字符集。这是一种字符标准,包括标准键盘上的字母、数字和特殊字符。它还包括控制字符,如退格、制表符、换行符、换页符和回车符。每个字符被分配一个数字代码。ASCII 码从 0 到 127。
本书中的程序将使用 ASCII 字符集编写。ASCII 字符集中的字符如附录 b 所示。
角色处理将在第六章中详细讨论。
2.3 C 代币
语言的标记是可以放在一起构造程序的基本构件。令牌可以是保留字(如int
或while
)、标识符(如b
或sum
)、常量(如25
或"Alice in Wonderland"
)、分隔符(如}
或;
)或运算符(如+
或=
)。
例如,考虑上一章末尾给出的程序 P1.4 的以下部分:
int main() {
int a, b, sum;
a = 14;
b = 25;
sum = a + b;
printf("%d + %d = %d\n", a, b, sum);
}
从头开始,我们可以按顺序列出令牌:
| 代币 | 类型 | | --- | --- | | `int` | 预定字 | | `main` | 标识符 | | `(` | 左括号,分隔符 | | `)` | 右括号,分隔符 | | `{` | 左大括号,分隔符 | | `int` | 预定字 | | `a` | 标识符 | | `,` | 逗号,分隔符 | | `b` | 标识符 | | `,` | 逗号,分隔符 | | `sum` | 标识符 | | `;` | 分号,分隔符 | | `a` | 标识符 | | `=` | 等号,分隔符 | | `14` | 常量 | | `;` | 分号,分隔符 |等等。因此,我们可以把一个程序想象成一串符号,这正是编译器看待它的方式。因此,就编译器而言,上面的代码可以写成这样:
int main() { int a, b, sum;
a = 14; b = 25; sum = a + b;
printf("%d + %d = %d\n", a, b, sum); }
令牌的顺序完全相同;对编译器来说,它是同一个程序。对于计算机来说,只有令牌的顺序才是重要的。然而,布局和间距对于提高程序的可读性是很重要的。
2.3.1 程序内的间距
一般来说,C 程序可以用“自由格式”编写。例如,这种语言不要求我们在一行上写一个语句。甚至一个简单的声明,如
a = 14;
可以写成四行,像这样:
a
=
14
;
只有标记的顺序是重要的。然而,由于14
是一个令牌,所以1
不能与4
分开。你甚至不能在1
和4
之间留一个空格。
除了在字符串或字符常量中,空格在 c # 中并不重要。但是,合理使用空格可以极大地提高程序的可读性。一般的经验法则是,只要你能放一个空格,你就可以放任意数量的空格,而不影响你程序的意思。该声明
a = 14;
可以写成
a=14;
或者
a = 14 ;
或者
a= 14;
声明
sum = a + b;
可以写成
sum=a+b;
或者
sum= a + b ;
或者
sum = a+b;
当然,请注意,变量sum
中不能有空格。写s um
或者su m
就不对了。一般来说,一个令牌的所有字符必须在一起。
保留字
C 语言使用了许多关键字,如int, char
和while
。关键字在 C 程序的上下文中有特殊的含义,并且只能用于该目的。比如,int 只能用在那些我们需要指定某个项的类型是整数的地方。所有关键字都只用小写字母书写。因此int
是一个关键字,而Int
和INT
不是。关键字是保留的,也就是说,不能将它们用作标识符。因此,它们通常被称为保留字。附录 a 中给出了 C 关键字列表。
标识符
C 程序员需要为诸如变量、函数名(第七章)和符号常量(见下页)等事物起名字。他编造的名字被称为用户标识符。在命名标识符时,需要遵循一些简单的规则:
- 它必须以字母或下划线开头。
- 如果需要其他字符,它们可以是字母、数字或下划线的任意组合。
标识符的长度不能超过 63 个字符。
有效标识符的示例:
r
R
sumOfRoots1and2
_XYZ
maxThrowsPerTurn
TURNS_PER_GAME
R2D2
root1
无效标识符的示例:
2hotToHandle // does not start with a letter
Net Pay // contains a space
ALPHA;BETA // contains an invalid character ;
需要注意的要点:
- 标识符中不允许有空格。如果你需要一个由两个或更多单词组成的单词,使用大小写字母的组合(如
numThrowsThisTurn
)或使用下划线来分隔单词(如num_throws_this_turn
)。我们更喜欢大写/小写的组合。 - 通常,C 区分大小写(大写字母被认为不同于相应的小写字母)。因此
r
是不同于R
的标识符。而且sum
不同于Sum
不同于SUM
不同于SuM
。 - 不能使用 C 保留字作为标识符之一。
2.3.4 一些命名约定
除了创建标识符的规则之外,C 对于使用什么名字,或者使用什么格式(例如,大写或小写)没有任何限制。然而,良好的编程实践表明应该遵循一些常识性的规则。
标识符应该是有意义的。例如,如果它是一个变量,它应该反映存储在变量中的值;对于存储某人的净工资,netPay
是比x
更好的变量,尽管两者都有效。如果是一个函数(第七章),它应该给出这个函数应该做什么的一些指示;playGame
是比plg
更好的标识符。
使用大写字母和小写字母的组合来表示由标识符命名的项的种类是一个好主意。在本书中,我们使用以下约定:
- 变量通常用小写字母书写:例如,
sum
。如果我们需要一个由两个或更多单词组成的变量,我们用一个大写字母开始第二个和随后的单词:例如,voteCount
或sumOfSeries
。 - 符号(或命名)常量是一个标识符,可以用来代替常量,如
100
。假设100
代表我们希望在某个程序中处理的项目的最大数量。我们可能需要在程序的不同地方使用数字100
。但是假设我们改变主意,想要供应 500 件商品。我们必须把所有出现的100
都改成500
。然而,我们必须确保不改变除了最大项目数(在类似于principal*rate/100
的计算中)之外的其他用途的100
的出现。 - 为了便于改变我们的想法,我们可以将标识符
MaxItems
设置为100
,并在需要引用最大项数时使用MaxItems
。如果我们改变主意,我们只需要将MaxItems
设置为新值。我们将以大写字母开始一个符号常量。如果它由一个以上的单词组成,我们将以大写字母开始每个单词,如MaxThrowsPerTurn
。 - 我们将在 4.6 节看到如何使用符号常量。
2.4 基本数据类型
在 1.4 节中,我们简要地谈到了数据类型的概念。对于本书的大部分内容,我们将使用以下数据类型:
int, double, and char
其中,这些被称为原始数据类型。
每种数据类型都定义了该类型的常量。当我们声明一个变量为特定类型时,我们实际上是在说什么样的常量(值)可以存储在那个变量中。例如,如果我们将变量num
声明为int
,我们就是说num
的值在任何时候都可以是一个整数常量,如25
、-369
或1024
。
2.5 整数- int
int 变量用于存储一个整数值。整数值是 0、1、2、3、4 等中的一个。然而,在计算机上,可以存储的最大和最小整数是由用于存储整数的位数决定的。附录 C 展示了如何在计算机上表示整数。
通常,int
变量占用 16 位(2 字节),可用于存储-32,768 到+32,767 范围内的整数。但是,请注意,在某些机器上,int
可以占用 32 位,在这种情况下,它可以存储从-2,147,483,648 到+2,147,483,647 的整数。一般来说,如果用 n 位来存储一个int
,可以存储的数的范围是-2 n-1 到+2 n-1 - 1。
作为练习,找出计算机上最大和最小的int
值。
声明变量
在 C # 中,通过指定类型名后跟变量来声明变量。例如,
int h;
将h
声明为int
类型的变量。该声明为h
分配了空间,但没有将其初始化为任何值。除非明确地为变量赋值,否则不能假定变量包含任何值。
您可以在一个语句中声明几个相同类型的变量,如下所示:
int a, b, c; // declares 3 variables of type int
变量之间用逗号分隔,最后一个变量后用分号隔开。
您可以在一条语句中声明一个变量并赋予它一个初始值,如:
int h = 14;
这将h
声明为int
,并给它一个值14
。
整数表达式
整数常量的写法我们都很熟悉:比如354
、639, -1
、30705
、-4812
。请注意,您只能使用一个可能的符号,后跟从0
到9
的数字。特别是,您不能像分隔千位那样使用逗号;因此32,732
是一个无效的整数常量——您必须将其写成32732
。
可以使用以下算术运算符来编写整数表达式:
| `+` | 增加 | | − | 减去 | | `*` | 多样地 | | `/` | 划分 | | `%` | 查找余数 |例如,假设我们有以下声明:
int a, b, c;
那么以下都是有效的表达式:
a + 39
a + b - c * 2
b % 10 //the remainder when b is divided by 10
c + (a * 2 + b * 2) / 2
操作符+
、-
和*
都给出了预期的结果。但是,/
执行整数除法;如果有剩余,就扔掉。我们说整数除法截断。因此19/5
给出值3
;剩余部分4
被丢弃。
但是-19/5
的值是多少呢?这里的答案是–3
。规则是,在 C 中,整数除法向零截断。因为–19 ÷ 5
的精确值是–3.8
,向零截断得到–3
。(在下一节中,我们将展示如何获得一个整数除以另一个整数的精确值。)
当一个整数被另一个整数除时,%
运算符给出余数。举个例子,
19 % 5 evaluates to 4;
h % 7 gives the remainder when h is divided by 7;
例如,你可以用它来测试一个数字h
是偶数还是奇数。如果h % 2
是0
,那么h
是偶数;如果h % 2
是1
,那么h
就是奇数。
2.5.3 运算符优先级
c # 基于运算符的通常优先级对表达式求值:乘法和除法在加法和减法之前完成。我们说乘除法的优先级高于加减法。例如,表达式
5 + 3 * 4
首先将3
乘以4
(给出12
,然后将5
加到12
,给出17
作为表达式的值。
像往常一样,我们可以使用括号来强制表达式按照我们想要的顺序求值。例如,
(5 + 3) * 4
先将5
和3
相加(给出8
,再将8
乘以4
,给出32
。
当两个具有相同优先级的运算符出现在一个表达式中时,它们从左到右进行计算,除非用括号另行指定。例如,
24 / 4 * 2
被评估为
(24 / 4) * 2
(给予12
)和
12 - 7 + 3
被评估为
(12 - 7) + 3
给予8
。然而,
24 / (4 * 2)
按照预期进行评估,给出3
,并且
12 - (7 + 3)
如预期被评估,给出2
。
在 C 语言中,余数运算符%
与乘法(*
)和除法(/
)具有相同的优先级。
Exercise: What is printed by the following program? Verify your answer by typing and running the program
#include <stdio.h>
int main() {
int a = 15;
int b = 24;
printf("%d %d\n", b - a + 7, b - (a + 7));
printf("%d %d\n", b - a - 4, b - (a - 4));
printf("%d %d\n", b % a / 2, b % (a / 2));
printf("%d %d\n", b * a / 2, b * (a / 2));
printf("%d %d\n", b / 2 * a, b / (2 * a));
}
2.5.4 使用“字段宽度”打印整数
我们已经看到,我们可以通过在printf
语句中指定值(通过变量或表达式)来打印整数值。当我们这样做时,C 使用所需的“打印列”打印值。例如,如果值是 782,则使用 3 列打印,因为 782 有 3 个数字。如果该值为-2345,则使用 5 个打印列(一个用于负号)进行打印。
虽然这对于大多数目的来说已经足够了,但是有时候能够告诉 C 要使用多少个打印列是很有用的。例如,如果我们想在5
打印列中打印n
的值,我们可以通过指定字段宽度5
来实现,如下所示:
printf("%5d", n);
我们现在用%5d
代替规格%d
。字段宽度位于%
和d
之间。n
的值被打印在“字段宽度5
”中。
假设n
是279
;有 3 个数字要打印,因此需要 3 个打印列。由于字段宽度为5
,所以数字279
前会打印 2 个空格,因此:◎◎279
(表示空格)。我们也说“打印时有两个前导空格”和“打印时在左边填充两个空格”
更专业的说法是“n
在字段宽度5
中右对齐打印。”“右对齐”是指将数字尽可能地放在字段的右侧,并在其前面添加空格以构成字段宽度。如果数字放在尽可能靠左的位置,并在其后添加空格以构成字段宽度,则数字是左对齐的。例如,279
在字段宽度5
中左对齐。
减号可用于指定左对齐;%-wd
将在w
的字段宽度中打印一个左对齐的值。例如,要打印一个在5
的字段宽度中左对齐的整数值,我们使用%-5d
。
再比如,假设n
是-7
,字段宽度是5
。打印n
需要两个打印列(一个用于-
,一个用于7
);由于字段宽度为5
,打印时有 3 个前导空格,因此:◎◎-7
。
你可能会问,场宽太小会怎么样?假设要打印的值是23456
,字段宽度是3
。打印该值需要 5 列,这大于字段宽度 3。在这种情况下,C 忽略字段宽度,并根据需要使用尽可能多的列(本例中为 5 列)来打印值。
一般情况下,假设用规格%wd
打印整数值v
,其中w
为整数,假设需要n
列打印v
。有两种情况需要考虑:
If n
is less than w
(the field width is bigger), the value is padded on the left with (w - n
) spaces. For example, if w
is 7
and v
is -345
so that n
is 4
, the number is padded on the left with (7-4
) = 3
spaces and printed as ◊◊◊-345
. If n
is greater than or equal to w
(field width is the same or smaller), the value is printed using n
print columns. In this case, the field width is ignored.
当我们想要一个接一个地排列数字时,字段宽度很有用。假设我们有三个int
变量a
、b
和c
,分别具有值9876
、-3
和501
。声明
printf("%d\n", a);
printf("%d\n", b);
printf("%d\n", c);
将打印
9876
-3
501
每个数字仅使用所需的列数打印。由于这一数字与下一个数字不同,所以它们不会排成一行。例如,如果我们愿意,我们可以使用字段宽度5
来排列数字。声明
printf("%5d\n", a);
printf("%5d\n", b);
printf("%5d\n", c);
将打印(◊表示空格)
◊9876
◊◊◊-3
◊◊501
那会是这样的(不带◎):
9876
-3
501
一切都井然有序。
有趣的是,我们并不真的需要三个printf
语句。我们可以将最后三个printf
语句替换为
printf("%5d\n%5d\n%5d\n", a, b, c);
每个\n
强制下面的输出到新的一行。
2.6 浮点数—float
和double
浮点数是可能有小数部分的数字。浮点常量可以用两种方法之一来编写:
- 正常方式,带有一个可选符号,包括一个小数点;比如-3.75,0.537,47.0。
- 使用科学记数法,带有可选符号,包括小数点和“指数”部分;比如-0.375E1,意思是“-0.375 乘以 10 的 1 次方”,也就是-3.75。同样,0.537 可以写成 5.37e-1,即 5.37 x 10-1。指数可以用 e 或 e 来指定。
- 注意,同一个数有几种写法。例如,以下都表示同一个数字 27.96:
27.96E00 2.796E1 2.796E+1 2.796E+01 0.2796E+02 279.6E-1
在 C 中,我们可以使用float
或double
来声明浮点变量。一个float
值通常存储为一个 32 位浮点数,给出大约 6 或 7 个有效数字。一个double
值被存储为一个 64 位浮点数,给出大约 15 个有效数字。
浮点常量的类型是double
,除非它后面跟有f
或F
,在这种情况下,它的类型是float
。因此3.75
属于double
类型,但3.75f
或3.75F
属于float
类型。大多数计算都是使用double
精度完成的。如果您需要存储大量浮点数,并且希望使用尽可能少的存储空间(并且不介意只有 6 或 7 位精度),那么类型float
非常有用。
在本书中,我们将主要使用double
来处理浮点数。
2.6.1 打印double
和float
变量
我们已经在一个printf
语句中使用了格式规范%d
来打印一个整型变量的值。如果我们希望打印一个double
或float
变量的值,我们可以使用%f
。例如,考虑以下情况:
double d = 987.654321;
printf("%f \n", d);
d
的值将被打印到预定义的小数位数(通常是 6 位,但可能因编译器而异)。在这种情况下,打印的值将是987.654321
。然而,如果d
被赋值为987.6543215
,打印出的值将是987.654322
(四舍五入到小数点后六位)。
类似地,如果x
的类型是float
,那么它的值可以使用以下方式打印:
printf("%f \n", x);
我们刚刚看到规范%f
将数字打印到预定义的小数位数。然而,大多数时候,我们想说要打印多少个小数位,有时,要使用多少列。例如,如果我们想在宽度为 6 的字段中打印上面的d
,精确到 2 位小数,我们可以使用:
printf("%6.2f \n", d);
在%
和f
之间,我们写6.2
,也就是字段宽度,后面是一个.
(点),后面是小数位数。该值四舍五入到指定的小数位数,然后打印出来。这里,打印的值将是987.65
,正好占据 6 个打印列。如果字段宽度更大,数字将在左边用空格填充。如果字段宽度较小,则会被忽略,并根据需要使用尽可能多的列来打印数字。
作为另一个例子,考虑
b = 245.75;
printf("%6.1f \n", b);
在规范%6.1f
中,1
表示将数字四舍五入到小数点后 1 位;这给出了245.8
,它需要 5 列来打印。
6
表示打印第 6 列245.8
;由于打印数字只需要 5 列,所以在开头加了一个空格组成 6 列,所以数字打印为◎245.8
(¢表示空格)。
同样的,
printf("%6.0f \n", b);
将b
打印为◎◎246
(四舍五入到0
小数位,打印在字段宽度6
)。
如果规格是%3.1f
并且要打印的值是245.8
,它将使用5
打印列来打印,即使字段宽度是3
。同样,当指定的字段宽度小于所需的打印列数时,C 会忽略字段宽度,并根据需要使用尽可能多的列来打印值。
我们有时可以利用这一点。如果我们不知道一个值可能有多大,我们可以故意使用一个小的字段宽度,以确保使用打印该值所需的精确的打印列数来打印它。
一般来说,假设float
或double
值v
要用规格%w.df
打印,其中w
和d
是整数。首先,将值v
四舍五入到d
位小数。假设打印v
需要的打印列数,包括一个可能的点(d = 0 就没有点;该值将被四舍五入为整数)和一个可能的符号,是n
。有两种情况需要考虑:
If n
is less than w
(the field width is bigger), the value is padded on the left with (w - n
) spaces. For example, suppose w
is 7
and the value to be printed is -3.45
so that n
is 5
. The number is padded on the left with (7-5
) = 2
spaces and printed as ◊◊-3.45
. If n
is greater than or equal to w
(field width is the same or smaller), the value is printed using n
print columns. In this case, the field width is ignored.
与整数一样,当我们想要一个接一个地排列数字时,字段宽度很有用。假设我们有三个double
变量a
、b,
和c
,分别具有值419.563
、-8.7,
和3.25
。假设我们要将值打印到小数点后两位,按小数点对齐,如下所示:
419.56
-8.70
3.25
因为最大的数字需要 6 个打印列,我们可以使用至少 6 的字段宽度来排列它们。下面的语句会将它们按上述方式排列起来:
printf("%6.2f \n", a);
printf("%6.2f \n", b);
printf("%6.2f \n", c);
如果我们使用大于 6 的字段宽度,数字仍然会排列,但是会有前导空格。
例如,如果我们使用字段宽度8
,我们将得到(◊表示一个空格)
◊◊419.56
◊◊◊-8.70
◊◊◊◊3.25
同样,我们可以使用一个printf
而不是三个来达到相同的效果:
printf("%6.2f \n%6.2f \n%6.2f \n", a, b, c);
每个\n
强制下面的输出到新的一行。
2 . 6 . 2double
和float
之间的分配
正如所料,您可以在一个float
变量中存储一个float
值,在一个double
变量中存储一个double
值。由于float
比double
小,C 允许你在double
变量中存储一个float
值,没有任何问题。但是,如果将一个double
赋值给一个float
,可能会损失一些精度。请考虑以下几点:
double d = 987.654321;
float x = d;
printf("%f \n", x);
由于一个float
变量只允许大约 7 位数的精度,我们应该预料到d
的值可能不会精确地分配给x
。实际上,当使用一个编译器运行时,会为x
打印出值987.654297
。当d
改为987654321.12345
时,打印的数值为987654336.000000
。在这两种情况下,保留了大约 6 或 7 位数的精度。
作为一个练习,看看用你的编译器打印出了什么值。
浮点表达式
浮点表达式可以使用以下运算符编写:
| `+` | 添加 | | − | 减法 | | `*` | 增加 | | `/` | 分开 |这些按预期运行;特别地,除法以通常的方式执行,例如,19.0/5.0
给出值3.8
。
如果op1
和op2
是一个运算符的两个操作数,下面显示了所执行的计算类型:
因此,只有当两个操作数都是float
时,才会执行float
;否则执行double
。
2.6.4 具有整数和浮点值的表达式
使用包含整数值和浮点值的表达式是很常见的,例如,
a / 3 where a is float
n * 0.25 where n is int
在 C 语言中,这种表达式的规则是这样的:
If any operand of arithmetic operator is floating-point, the calculation is completed in floating-point arithmetic. Unless at least one operand is double, the calculation is of floating-point type, in which case the calculation is of double type.
在上面的第一个例子中,整数3
被转换为float
,计算在float
中完成。在第二个例子中,n
被转换为double
(因为0.25
是double
),计算在double
中完成。
比方说,我们如何得到整数除法19/5
的精确值?我们可以通过将一个或两个常量写成double
来强制进行双精度计算,因此:19/5.0
、19.0/5,
或19.0/5.0
。我们也可以使用石膏,比如
(double) 19 / 5
强制转换由括号中的类型名组成,并允许我们强制将一种类型转换为另一种类型。这里,19
被转换为double
,迫使5
转换为double
,并执行双精度除法。
然而,我们必须小心像这样的构造
(double) (19 / 5)
这可能不是我们想的那样。这不做浮点除法。由于两个常量都是整数,括号内的表达式计算为整数除法,给出3
;这个值转换成double
,给出3.0
。
2.6.5 将double/float
分配给int
考虑一下:
double d = 987.654321;
int n = d;
printf("%d \n", n);
打印数值987
。当我们将一个浮点值赋给一个int
时,小数部分(如果有的话)将被丢弃(不舍入)并且最终的整数值被赋值。我们有责任确保获得的整数足够小,能够适合一个int
。否则,结果值是不可预测的。
在一个编译器上,一个int
的最大值是32767
,当d
被改为987654.321
时,输出的值是4614
,与预期相差甚远,看起来不可预测。(不完全不可预测;赋值为987654 % 32768
,也就是4614
。一般来说,如果big
代表一个太大而无法存储的值,那么对于 16 位存储的整数,实际存储的值就是big % 32768
。)这是因为d
的截断值是987654
,它太大了,放不进一个int
变量。作为一个练习,看看你的编译器会输出什么样的值。
如果我们想要将d
的舍入值存储在n
中,我们可以用
n = d + 0.5;
如果d
中小数点后的第一个数字是5
或更多,添加0.5
会将1
添加到整个数字部分。如果点后的第一个数字小于5
,添加0.5
不会改变整个数字部分。
比如d
是245.75
,加0.5
会给246.25
,246
会赋给n
。但如果d
是245.49
,加0.5
会给245.99
,245
会赋给n
。
2.7 弦
到目前为止,我们已经看到了几个printf
语句中字符串常量的例子。
字符串常量是任何用双引号括起来的字符序列。例如:
"Once upon a time"
"645-2001"
"Are you OK?"
"c:\\data\\castle.in"
左引号和右引号必须出现在同一行。换句话说,C 不允许字符串常量延续到另一行。然而,一个长的字符串可以被分成几个部分,每个部分占一行。当程序被编译时,C 将把这些片段连接起来,形成一个字符串。例如,
printf("Place part of a long string on one line and "
"place the next part on the next line. The parts are "
"separated by whitespace, not comma or ; \n");
字符串常量的值是没有开始和结束引号的字符序列。因此,"Are you OK?"
的值就是Are you OK?
。
如果您希望双引号成为字符串的一部分,您必须使用转义序列\"
来编写它,如
"\"Don’t move!\", he commanded"
该字符串的值为
"Don’t move!", he commanded
每个\"
都被替换为"
,并且开始和结束的引号都被删除。
C 语言没有预定义的string
类型。这给初学编程的人带来了困难,因为他不能像处理数值变量那样处理字符串变量。
在 C 语言中,字符串存储在“字符数组”中既然我们在第六章的中讨论了字符,在第八章的中讨论了数组,我们可以耐心等待,直到理解数组是什么,字符串是如何存储的,以及我们如何用它们来存储一个名字。或者,我们可以凭着信心接受一些东西,并以一种有限的方式,比我们通常更快地收获能够与弦一起工作的好处。我们会不耐烦,选择后者。
假设我们希望在某个变量name
中存储一个人的名字。我们可以声明name
如下:
char name[50];
这声明了name
是一个大小为50
的“字符数组”。正如我们将在第八章中解释的,这允许我们在name
中存储最多49
个角色。如果您发现这对于您的目的来说太多(或太少),您可以使用不同的数字。
如果我们愿意,我们可以在声明中给name
赋一个字符串常量,这样:
char name[50] = "Alice Wonder";
这在name
中存储了从A
到r
的字符,包括空格。报价不会被存储。一旦完成,我们可以使用printf
中的规格%s
打印name
的值,因此:
printf("Hello, %s\n", name);
这会打印出来
Hello, Alice Wonder
name
的值替换%s
。
不幸的是,除了在name
的声明中,我们不能给name
赋值一个字符串常量。c 不允许我们写一个赋值语句,比如
name = "Alice in Wonderland"; // this is not valid
给name
赋值。我们可以使用标准函数strcpy
(用于字符串复制),如下所示:
strcpy(name, "Alice in Wonderland"); // this is valid
但是为了使用strcpy
(和其他字符串函数),我们必须在程序前加上指令:
#include <string.h>
我们在程序 P2.1 中总结了所有这些。
Program P2.1
#include <stdio.h> // needed for printf
#include <string.h> // needed for strcpy
int main() {
char name[50];
strcpy(name, "Alice in Wonderland");
printf("Hello, %s\n", name);
}
运行时,该程序将打印
Hello, Alice in Wonderland
在 3.4 和 5.9 节中,我们将看到如何将一个字符串值读入一个变量。
连接两个字符串是我们有时想要执行的操作。我们说我们想要连接两个字符串。我们可以用标准的字符串函数strcat
(字符串连接)来实现。例如,假设我们有:
char name[30] = "Alice";
char last[15] = "Wonderland";
声明
strcat(name, last);
会将last
中的字符串添加到name
中的字符串。我们有责任确保name
足够大,能够容纳连接的字符串。结果是name
现在会按住AliceWonderland
;last
中的值不变。以下语句将 name 设置为Alice in Wonderland
。
strcat(name, " in "); //one space before and after "in"
strcat(name, last);
2.8 转让声明
在 1.9 节中,我们介绍了赋值语句。回想一下,赋值语句包括一个变量,后面跟一个等号(=),后面跟要赋给变量的值,再后面跟一个分号。我们可以这样写:
<variable> = <value>;
<value>
必须与<variable>
兼容,否则会出错。例如,如果<variable>
是int
,我们必须能够从<value>
中导出一个整数。如果<variable>
是double
,我们必须能够从<value>
中导出一个浮点值。例如,如果n
是int
而x
是double
,我们就不能写
n = "Hi there"; //cannot assign string to int
x = "Be nice"; //cannot assign string to double
将赋值语句想象成如下执行是很有用的:计算=
右边的值。获得的值存储在左侧的变量中。变量的旧值(如果有)会丢失。例如,如果 s core
的值为25
,那么在语句之后
score = 84;
score
的值将是84
;旧的价值25
丢失了。我们可以把这想象成:
一个变量可以取几个值中的任何一个,但一次只能取一个。作为另一个例子,考虑这个陈述:
score = score + 5;
假设score
在这个语句执行之前有值84
。执行后的价值是什么?
首先,使用score
、84
的当前值评估右侧score + 5
。计算得出89
—该值存储在左侧的变量中;恰好是score
。最终结果是score
的值增加了5
到89
。旧值84
丢失。
即使赋值语句是有效的,它也可能在程序运行时产生错误。考虑以下情况(a
、b
、c
、d
、e
为int
):
a = 12;
b = 5;
c = (a – b) * 2;
d = c + e;
其中每一个都是格式正确的赋值语句。但是,当执行这些语句时,将会导致错误。你能看出是怎么回事吗?
第一条语句将12
赋值给a
;第二个将5
分配给b
;第三个将14
分配给c
;目前没有问题。然而,当计算机试图执行第四条语句时,它遇到了问题。e
没有值,所以表达式c + e
不能求值。我们说e
是未定义的——它没有值。
在我们可以在表达式中使用任何变量之前,它必须已经被某个先前的语句赋值。如果没有,我们将得到一个“未定义变量”的错误,我们的程序将暂停。
这个故事的寓意是:有效的程序不一定是正确的程序。
Exercise: What is printed by the following?
a = 13;
b = a + 12;
printf("%d %d\n", a, b);
c = a + b;
a = a + 11;
printf("a = %d b = %d c = %d\n", a, b, c);
2.9 printf
我们已经看到了几个printf
语句的例子。我们用它来打印字符串常量、整数值和浮点值。我们打印了有和没有字段宽度的值。我们还看到了如何使用转义序列\n
来强制输出到新的一行。
值得强调的是,格式字符串中的字符完全按照它们出现时的样子打印,只是格式规范被其相应的值所替换。例如,如果a
是25
,b
是847
,考虑以下语句
printf("%d%d\n", a, b);
这会打印出来
25847
这些数字粘在一起,我们分不清什么是a
什么是b
!这是因为规范%d%d
说要打印相邻的数字。如果我们想用一个空格将它们分开,比如说,我们必须在%d
和%d
之间放一个空格,就像这样:
printf("%d %d\n", a, b);
这会打印出来
25 847
为了在数字之间获得更多的空格,我们只需在%d
和%d
之间输入我们想要的数字。
Exercise: What is printed by the following?
printf("%d\n %d\n", a, b);
下面是一些关于格式规范的有用信息。
假设num
为int
,其值为75
:
- 规格
%d
将使用2
打印列75
打印75
- 规格
%5d
将打印带有 3 个前导空格的75
:◊◊◊75
- 规格
%-5d
将打印带有 3 个尾随空格的75
:75◊◊◊
- 规格
%05d
将打印带有 3 个前导零的75
:00075
对于前导0
可能有用的示例,考虑以下语句
printf("Pay this amount: $%04d\n", num);
这会打印出来
Pay this amount: $0075
这比印刷好
Pay this amount: $ 75
因为有人可以在$
和7
之间插入数字。
通常,减号指定左对齐,字段宽度前面的0
指定0
(零,而不是空格)作为填充字符。
Exercises 2In the ASCII character set, what is the range of codes for (a) the digits (b) the uppercase letters and (c) the lowercase letters? What is a token? Give examples. Spaces are normally not significant in a program. Give an example showing where spaces are significant. What is a reserved word? Give examples. Give the rules for making up an identifier. What is a symbolic constant and why is it useful? Give examples of integer constants, floating-point constants, and string constants. Name five operators that can be used for writing integer expressions and give their precedence in relation to each other. Give the value of (a) 39 % 7 (b) 88 % 4 (c) 100 % 11 (d) -25 % 9 Give the value of (a) 39 / 7 (b) 88 / 4 (c) 100 / 11 (d) -25 / 9 Write a statement that prints the value of the int
variable sum
, right justified in a field width of 6
. You are required to print the values of the int
variables b
, h
, and n
. Write a statement that prints b
with its rightmost digit in column 10, h
with its rightmost digit in column 20, and n
with its rightmost digit in column 30. Write statements that print the values of b
, h
, and n
lined up one below the other with their rightmost digits in column 8. Using scientific notation, write the number 345.72 in four different ways. Write a statement that prints the value of the double
variable total
to 3 decimal places, right justified in a field width of 9. You need to print the values of the float
variables a
, b
, and c
to 1 decimal place. Write a statement that prints a
with its rightmost digit in column 12, b
with its rightmost digit in column 20, and c
with its rightmost digit in column 32. What kind of variable would you use to store a telephone number? Explain. Write statements to print the values of 3 double variables a
, b
, and c
, to 2 decimal places, The values must be printed one below the other, with their rightmost digits in column 12. How can you print the value of a double
variable, rounded to the nearest whole number? What happens if you try to print a number (int
, float
, or double
) with a field width and the field width is too small? What if the field width is too big? Name some operators that can be used for writing floating-point expressions. Describe what happens when we attempt to assign an int
value to a float
variable. Describe what happens when we attempt to assign a float
value to an int
variable. Write a statement to print the following: Use \n to end a line of output
. Write a statement to increase the value of the int
variable quantity by 10
. Write a statement to decrease the value of the int variable quantity
by 5
. Write a statement to double the value of the int
variable quantity
. Write a statement to set a
to 2
times b
plus 3
times c
. The double
variable price
holds the price of an item. Write a statement to increase the price by (a) $12.50 (b) 25%. What will happen when the computer attempts to execute the following: p = 7;
q = 3 + p;
p = p + r;
printf("%d\n", p);
Suppose rate = 15
. What is printed by each of the following? printf("Maria earns rate dollars an hour\n");
printf("Maria earns %d dollars an hour\n", rate);
If m
is 3770
and n
is 123
, what is printed by each of the following? (a) printf("%d%d\n", n, m);
(b) printf("%d\n%d\n", n, m);
三、具有顺序逻辑的程序
在本章中,我们将解释以下内容:
- 读取用户提供的数据的想法
scanf
语句如何工作- 如何使用
scanf
读取数字数据 - 如何使用
gets
读取字符串数据 - 用几个例子说明程序编写的重要原则
3.1 导言
在上一章中,我们介绍了一些 C 语言的基本数据类型——int
、double
和float
——并用简单的语句来说明它们的用法。我们现在更进一步,通过使用这些类型编写程序来介绍几个编程概念。
本章中的程序将基于顺序逻辑——简单地说就是程序中的语句一个接一个地执行,从第一个到最后一个。这是最简单的一种逻辑,也叫直线逻辑。在下一章中,我们将编写使用选择逻辑的程序——程序测试某些条件并根据条件的真假采取不同行动的能力。
3.2 读取用户提供的数据
再次考虑程序 P1.3。
Program P1.3
// This program prints the sum of 14 and 25\. It shows how
// to declare variables in C and assign values to them.
#include <stdio.h>
int main() {
int a, b, sum;
a = 14;
b = 25;
sum = a + b;
printf("%d + %d = %d\n", a, b, sum);
}
c 允许我们在一条语句中声明一个变量并给它一个初始值,这样我们就可以更简洁地编写程序(没有注释),如程序 P3.1:
Program P3.1
#include <stdio.h>
int main() {
int a = 14;
int b = 25;
int sum = a + b;
printf("%d + %d = %d\n", a, b, sum);
}
因为,如前所述,我们并不真正需要变量sum
,这个程序可以写成程序 P3.2。
Program P3.2
#include <stdio.h>
int main() {
int a = 14;
int b = 25;
printf("%d + %d = %d\n", a, b, a + b);
}
这个项目非常严格。如果我们希望添加另外两个数字,我们必须将程序中的数字14
和25
更改为所需的数字。然后我们必须重新编译程序。每次我们想增加两个不同的数字,我们就必须改变程序。这可能会变得非常乏味。
如果我们能够以这样一种方式编写程序,当我们运行程序时,我们将有机会告诉程序我们希望添加哪些数字,那就太好了。这样,数字就不会与程序捆绑在一起,程序也会更加灵活。当我们“告诉”程序这些数字时,我们说我们在向程序提供数据。但是我们如何让程序向我们“询问”这些数字,我们如何“告诉”程序这些数字是什么?
我们可以让程序通过打印如下消息来提示我们输入一个数字:
Enter first number:
使用printf
语句。然后,程序必须等待我们键入数字,当数字被键入时,就读取它。这可以通过scanf
语句来完成。(严格来说,printf
和scanf
都是函数,但区别对我们来说并不太重要。)在我们看这个语句之前,让我们用这些新的想法重写这个算法:
prompt for the first number
read the number
prompt for the second number
read the number
find the sum
print the sum
我们可以用 C 语言将这个算法实现为程序 P3.3。
Program P3.3
//prompt for two numbers and find their sum
#include <stdio.h>
int main() {
int a, b;
printf("Enter first number: ");
scanf("%d", &a);
printf("Enter second number: ");
scanf("%d", &b);
printf("%d + %d = %d\n", a, b, a + b);
}
运行时,第一个printf
语句将打印出来:
Enter first number:
简单解释一下,scanf
语句会让计算机等待用户输入一个数字。
假设她输入23
;屏幕将如下所示:
Enter first number: 23
当她按下键盘上的“Enter”或“Return”键时,scanf
读取数字并将其存储在变量a
中。
然后,下一个printf
语句提示:
Enter second number:
再次,scanf
使计算机等待用户输入一个数字。假设她进入18
;scanf
读取数字,并存储在变量b
中。在这个阶段,数字23
存储在a
中,而18
存储在b
中。我们可以这样描述:
然后,程序执行最后一条printf
语句,并打印以下内容:
23 + 18 = 41
最后,屏幕将如下所示。下划线部分由用户输入,其他部分由计算机打印:
Enter first number:
23
Enter second number:
18
23 + 18 = 41
由于用户可以自由输入任何数字,只要数字足够小,可以存储在一个int
变量中,程序就可以处理任何输入的数字。否则,将会打印出奇怪的结果。
3.3 scanf
在程序 P3.3 中,语句
scanf("%d", &a);
使计算机等待用户键入数字。由于a
是一个整数变量,scanf
期望数据中的下一项是一个整数或一个值(比如说3.8
),它可以转换成整数,但去掉小数部分。如果不是(例如,如果是一个字母或特殊字符),程序将给出一个错误信息,如“无效的数字格式”并停止。我们说程序会崩溃。如果数据有效,该数字将存储在变量a
中。声明
scanf("%d", &b);
以类似的方式工作。
该声明包括:
scanf
这个词- 左右括号
- 括号内的两项(称为参数),用逗号分隔
和printf
一样,第一项是一个叫做格式字符串的字符串。在这个例子中,字符串只包含格式规范%d
。它指定了要读取的数据类型。这里,%d
用于表示要读取的整数值。
第二个参数指定存储读取值的位置。尽管我们希望值存储在a
,scanf
要求我们通过写&a
来指定。简单的解释是,我们必须告诉scanf
存储值的内存位置的地址;&a
代表“?? 的地址”你需要相信,为了使用scanf
将一个值读入一个变量,变量前面必须有&
,如&a
和&b
。请注意,这仅适用于scanf
语句。除此之外,变量以其正常形式使用(无&
),如下所示:
printf("%d + %d = %d\n", a, b, a + b);
我们可以使用scanf
一次读取多个值。例如,假设我们想读取变量a
、b
和c
的三个整数值。为此,我们需要在格式规范中写三次%d
,因此:
scanf("%d %d %d", &a, &b, &c);
当这个语句被执行时,它寻找三个整数。第一个存储在a
中,第二个存储在b
中,第三个存储在c
中。由用户来确保数据中接下来的三项是整数。如果不是这样,将会显示“无效数字格式”信息,程序将会崩溃。
输入数据时,数字必须用一个或多个空格分隔,如下所示:
42 -7 18
使用scanf
时,可以灵活的方式提供数据。唯一的要求是以正确的顺序提供数据。在本例中,三个数字可以如上或如下提供:
42
-7
18
或者这个:
42 -7
18
或者甚至用一个空行,就像这样:
42
-7 18
空格、制表符和空行(所谓的空白)无关紧要;scanf
将简单地继续读取数据,忽略空格、制表符和空白行,直到它找到这三个整数。但是,我们强调,如果在读取数据时遇到任何无效字符,程序将会崩溃。例如,如果用户键入
42 -7 v8
或者
42 = 18 24
程序会崩溃。第一种情况,v8
不是有效整数;并且,在第二种情况下,=
不是整数的有效字符。
3.3.1 将数据读入float
变量
如果我们希望将一个浮点数读入一个浮点变量 x,我们可以使用
scanf("%f", &x);
规范%f
用于将一个值读入float
(但不是double
,见下一节)变量。执行时,scanf
期望在数据中找到一个有效的浮点常量。例如,以下任何一项都是可以接受的:
4.265
-707.96
2.345E+1
在最后一种情况下,例如在5
和E
之间或者在E
和+
之间或者在+
和1
之间不能有空格。以下都将对读取数字23.45
无效:
2.345 E+1
2.345E +1
2.345E+ 1
3.3.2 将数据读入double
变量
如果我们希望将一个浮点数读入一个double
变量y
,我们可以使用
scanf("%lf", &y);
规格%lf
(percent ell f)用于将一个值读入double
变量。除了规格之外,对于float
和double
变量,数据以相同的方式输入。小心——你不能使用%f
将数据读入double
变量。如果你这样做,你的变量将包含无意义的内容,因为读取的值将存储在 32 位而不是 64 位,即double
的大小(见 2.6 节)。然而,如您所见,您可以使用%f
来打印double
变量的值。
为float/double
变量输入数据时,整数是可以接受的。如果你输入42
,比方说,它会被解释为42.0
。但是,如上所述,如果您为一个int
变量输入一个浮点常量(例如,2.35
,它将被截断(在本例中,截断到2
)。
如果需要,可以使用一条scanf
语句将值读入多个变量。如果x
和y
是double
变量,可以用
scanf("%lf %lf", &x, &y);
将数值读入x
和y
。执行时,scanf
期望在数据中找到两个有效的浮点(或整数)常量。第一个存储在x
中,第二个存储在y
中。数字之前、之间或之后可以有任意数量的空格或空白行。
您也可以在同一个scanf
语句中读取int
、double
或float
变量的值。您只需要确保对每个变量使用正确的规范。假设item
和quantity
是int
,而price
是double
。声明
scanf("%d %lf %d", &item, &price, &quantity);
期望在数据中找到三个数字。
- 第一个必须是存储在
item
中的int
常量。 - 第二个必须是存储在
price
中的double
(或int
)常量。 - 第三个必须是存储在
quantity
中的int
常量。
以下是该scanf
语句的所有有效数据:
4000 7.99 8.7 // 8.7 is truncated to 8
3575 10 44 // price will be interpreted as 10.00
5600 25.0 1
通常,任何数量的空格都可以用来分隔数字。
以下是该scanf
语句的所有无效数据:
4000 7.99 x.8 // x.8 is not an integer constant
25cm 10 44 // 25cm is not an integer constant
560 25 amt = 7 // a is not a valid numeric character
当scanf
获取一个数字时,它保持在该数字之后;随后的scanf
将继续从该点读取数据。举例来说,假设某些数据的类型为
4000 7.99 8
考虑一下这些陈述
scanf("%d", &item);
scanf("%lf", &price);
scanf("%d", &quantity);
第一个scanf
将把4000
存储在item
中。完成后,它保持在4000
后的空间。下一个scanf
将从该点继续读取,并将7.99
存储在price
中。此scanf
将在7.99
后的空格处停止。第三个scanf
将从该点继续读取,并将8
存储在quantity
中。此scanf
将在8
后的字符处停止;这可能是一个空格或行尾字符。任何后续的scanf
将从该点继续读取。
当读取数据项时,想象一个“数据指针”在数据中移动是很有用的。在任何时候,它都标记数据中的位置,下一个scanf
将从该位置开始寻找下一项数据。
3.4 读取字符串
在 2.6 节中,我们看到了如何声明一个变量来保存一个字符串值。例如,《宣言》
char item[50];
让我们在item
中存储一个字符串值(最大长度为 49)。我们还看到了如何使用标准的字符串函数strcpy
给item
赋值。
现在我们向您展示如何从输入中读取一个值到item
。在 c 语言中有几种方法可以做到这一点。我们将使用gets
(通常读作 get s not gets)语句(更准确地说,是一个函数),如下所示:
gets(item);
这将从数据指针的当前位置开始读取字符并将它们存储在item
中,直到到达行尾。不存储行尾字符。数据指针位于下一行的开头。
例如,如果数据线是
Right front headlamp
然后将字符串Right front headlamp
存储在item
中。效果和我们写的一样
strcpy(item, "Right front headlamp");
机警的读者会注意到,我们没有在item
前加一个&
,就像我们一直用scanf
读数字一样。现在,只需注意item
是一个“字符数组”, C 语言中的规则是,在向数组中读取数据时,不能在数组名前放入&
。在我们讨论了第八章中的数组之后,你可能会更好地理解这一点。简单的解释是,数组名表示“数组第一个元素的地址”,所以不需要&
获取地址。现在,就把它当成你需要遵守的规则。
考虑以下语句(假设声明char name[50]
):
printf("Hi, what’s your name? ");
gets(name);
printf("Delighted to meet you, %s\n", name);
当执行时,
printf
语句将询问您的姓名。- 会等你输入你的名字。输入后,名称将存储在变量
name
中。 printf
然后会用您的名字打印问候语。
您的计算机屏幕将如下所示(假设键入Birdie
作为名称):
Hi, what’s your name?
Birdie
Delighted to meet you, Birdie
3.5 示例
我们现在编写程序来解决一些问题。在寻找解决方案之前,你应该先试着解决问题。在示例运行中,带下划线的项目由用户键入;其他的都是电脑打印的。
问题 1 -平均
写一个程序请求三个整数并打印它们的平均值到一个小数位。该程序应该如下工作:
Enter 3 integers:
23 7 10
Their average is 13.3
解决方案如程序 P3.4 所示。
Program P3.4
//request 3 integers; print their average
#include <stdio.h>
int main() {
int a, b, c;
double average;
printf("Enter 3 integers: ");
scanf("%d %d %d", &a, &b, &c);
average = (a + b + c) / 3.0;
printf("\nTheir average is %3.1f\n", average);
}
关于程序 P3.4 的注意事项:
- 变量 average 被声明为
double
而不是int
,因为平均值可能不是整数。 - 如果数据中没有输入整数,程序将会崩溃,或者至多给出不正确的结果。
- 我们使用
3.0
而不是3
来计算平均值。这将强制执行浮点除法。如果我们使用了3
,就会执行整数除法,给出13.0
作为样本数据的答案,如上。 - 在最后一个
printf
中,第一个\n
用于打印输出中的空行。 - 我们可以在一个语句中声明
average
并赋值给它,就像这样:
double average = (a + b + c) / 3.0;
- 变量
average
在这个程序中并不是真正必要的。我们可以计算并打印出printf
语句中的平均值
printf("\nTheir average is %3.1f\n", (a + b + c) / 3.0);
3.5.2 问题 2 -正方形
写一个程序来请求一个整数并打印这个数和它的平方。该程序应该如下工作:
Enter a whole number:
6
Square of 6 is 36
解决方案如程序 P3.5 所示。
Program P3.5
//request a whole number; print its square
#include <stdio.h>
int main() {
int num, numSq;
printf("Enter a whole number: ");
scanf("%d", &num);
numSq = num * num;
printf("\nSquare of %d is %d\n", num, numSq);
}
关于程序 P3.5 的注意事项:
-
为了使输出可读,请注意
f
后面的空格和is
周围的空格。如果省略这些空格,示例输出将是Square of6is36
-
变量
numSq
并不是真正必要的。它可以完全省略,相同的输出打印为printf("\nSquare of %d is %d\n", num, num * num);
-
程序假定将输入一个整数;如果输入的不是整数,程序会崩溃或给出不正确的结果。为了迎合带点的数字,声明
num
(和numSq
,如果使用的话)为double
。
问题 3 -银行业务
给定银行中客户的以下数据:姓名、账号、平均余额和当月交易次数。需要计算所得利息和服务费。
利息计算如下:
interest = 6% of average balance
服务费是这样计算的:
service charge = 50 cents per transaction
编写一个程序,为客户读取数据,计算利息和服务费,并打印客户的姓名、平均余额、利息和服务费。
以下是该程序的运行示例:
Name? Alice Wonder
Account number? 4901119250056048
Average balance? 2500
Number of transactions? 13
Name: Alice Wonder
Average balance: $2500.00
Interest: $150.00
Service charge: $6.50
解决方案如程序 P3.6 所示。
Program P3.6
//calculate interest and service charge for bank customer
#include <stdio.h>
int main() {
char customer[30], acctNum[30];
double avgBalance, interest, service;
int numTrans;
printf("Name? ");
gets(customer);
printf("Account number? ");
gets(acctNum);
printf("Average balance? ");
scanf("%lf", &avgBalance);
printf("Number of transactions? ");
scanf("%d", &numTrans);
interest = avgBalance * 0.06;
service = numTrans * 0.50;
printf("\nName: %s\n", customer);
printf("Average balance: $%3.2f\n", avgBalance);
printf("Interest: $%3.2f\n", interest);
printf("Service charge: $%3.2f\n", service);
}
这个问题比我们到目前为止看到的那些问题更复杂。它涉及更多的数据和更多的处理。但是,如果我们采取小步骤解决问题,我们可以简化它的解决方案。
首先,让我们概述一下解决这个问题的算法。这可以是:
prompt for and read each item of data
calculate interest earned
calculate service charge
print required output
这里的逻辑相当简单,稍加思考就能让我们相信这些是解决问题所需的步骤。
接下来,我们必须为需要存储的数据项选择变量。
- 对于客户的名字,我们需要一个字符串变量—我们称之为
customer
。 - 我们可能会尝试使用整数变量作为账号,但这不是一个好主意,原因有二:账号可能包含字母(如
CD55887700
);或者它可能是一个很长的整数,太大而不适合一个int
变量。出于这些原因,我们使用一个叫做acctNum
的字符串变量。 - 平均余额可能包含一个小数点,必须存储在一个
double
变量中;我们称之为avgBalance
。 - 交易的数量是一个整数,所以我们使用一个变量
int
,numTrans
。
接下来,我们需要变量来存储利息和服务费。由于这些可能包含小数点,我们必须使用double
变量——我们称它们为interest
和service
。
鉴于我们到目前为止所介绍的内容,提示和读取数据相当简单。我们只需要强调,当输入数字数据时,它必须是一个数字常量。例如,我们不能将平均余额输入为$2500
或2,500
。我们必须以2500
或2500.0
或2500.00
的名字报名。
利息和服务费的计算是最大的挑战。我们必须以计算机能够理解和执行的形式来指定计算。
例如,我们不能书写
interest = 6% of avgBalance;
以至
interest = 6% * avgBalance;
或者
service = 50 cents per transaction;
我们必须使用适当的常量、变量和运算符,将每个右边表示为适当的算术表达式。因此,
“平均余额的 6%”必须表示为
avgBalance*0.06
或者
0.06*avgBalance
“每笔交易 50 美分”必须表示为
0.50*numTrans
或者
numTrans*0.5
或者类似的东西
numTrans/2.0
打印输出相当简单。例如,即使我们在输入平均余额的数据时不能使用$
,我们也可以在打印它的值时在它前面打印一个美元符号。我们需要做的就是将$
作为字符串的一部分打印出来。如何做到这一点显示在程序中。类似地,我们打印标有美元符号的利息和服务费。
我们使用规格%3.2f
来打印avgBalance
。我们有意使用一个小的字段宽度3
,这样avgBalance
只使用打印其值所需的精确的打印列数进行打印。这确保了它的值就印在美元符号的旁边。类似的话也适用于interest
和service
。
3.5.4 问题 4-门票
在足球比赛中,门票分为三类出售:保留票、看台票和场地票。对于这些类别中的每一个,您都会得到票价和售出的门票数量。编写一个程序来提示这些值,并打印从每一类门票中收取的金额。同时打印售出的门票总数和收取的总金额。
我们将编写运行时如下操作的程序:
Reserved price and tickets sold?
100 500
Stands price and tickets sold?
75 4000
Grounds price and tickets sold?
40 8000
Reserved sales: $50000.00
Stands sales: $300000.00
Grounds sales: $320000.00
12500 tickets were sold
Total money collected: $670000.00
如图所示,我们提示并一次读取两个值,价格和售出的门票数量。
对于每个类别,销售额的计算方法是将票价乘以售出的门票数量。
售出的门票总数是通过将每个类别售出的门票数相加计算出来的。
通过将每个类别的销售额相加来计算收集的总金额。
用于解决该问题的算法的概要如下:
prompt for and read reserved price and tickets sold
calculate reserved sales
prompt for and read stands price and tickets sold
calculate stands sales
prompt for and read grounds price and tickets sold
calculate grounds sales
calculate total tickets
calculate total sales
print required output
一个解决方案如程序 P3.7 所示,价格可以输入整数或double
常量;票的数量必须以整数常量的形式输入。
Program P3.7
//calculate ticket sales for football match
#include <stdio.h>
int main() {
double rPrice, sPrice, gPrice;
double rSales, sSales, gSales, tSales;
int rTickets, sTickets, gTickets, tTickets;
printf("Reserved price and tickets sold? ");
scanf("%lf %d", &rPrice, &rTickets);
rSales = rPrice * rTickets;
printf("Stands price and tickets sold? ");
scanf("%lf %d", &sPrice, &sTickets);
sSales = sPrice * sTickets;
printf("Grounds price and tickets sold? ");
scanf("%lf %d", &gPrice, &gTickets);
gSales = gPrice * gTickets;
tTickets = rTickets + sTickets + gTickets;
tSales = rSales + sSales + gSales;
printf("\nReserved sales: $%3.2f\n", rSales);
printf("Stands sales: $%3.2f\n", sSales);
printf("Grounds sales: $%3.2f\n", gSales);
printf("\n%d tickets were sold\n", tTickets);
printf("Total money collected: $%3.2f\n", tSales);
}
Exercises 3For each of the following, give examples of data that will be read correctly and examples of data that will cause the program to crash. Assume the declaration int i, j; double x, y;);
(a) scanf("%d %d", &i, &j);
(b) scanf("%lf %lf", &x, &y);
(c) scanf("%d %lf %d", &i, &x, &j);
For 1(c), state what will be stored in i
, x
, and j
for each of the following sets of data: (a) 14 11 52
(b) -7 2.3 52
(c) 0 6.1 7.0
(d) 1.0 8 -1
Write a program that requests a user to enter a weight in kilograms, and converts it to pounds. (1 kilogram = 2.2 pounds.) Write a program that requests a length in centimeters and converts it to inches. (1 inch = 2.54 cm.) Assuming that 12
and 5
are entered as data, identify the logic error in the following statements (a
, b
, c
, d
, and e
are int
): scanf("%d %d", &a, &b);
c = (a - b) * 2;
d = e + a;
e = a / (b + 1);
printf("%d %d %d\n", c, d, e);
When the error is corrected, what is printed? What is printed by the following (a
, b
, and c
are int
)? a = 13;
b = a + 12;
printf("%d %d\n", a, b);
c = a + b;
a = a + 11;
printf("%d %d %d\n", a, b, c);
Write a program that requests a price and a discount percent. The program prints the original price, the discount amount, and the amount the customer must pay. Same as 7, but assume that 15% tax must be added to the amount the customer must pay. Write a program to calculate electricity charges for a customer. The program requests a name, previous meter reading, and current meter reading. The difference in the two readings gives the number of units of electricity used. The customer pays a fixed charge of $25 plus 20 cents for each unit used. Print all the data, the number of units used, and the amount the customer must pay, appropriately labeled. Modify 9 so that the program requests the fixed charge and the rate per unit. Write a program to request a student’s name and marks in four subjects. The program must print the name, total marks, and average mark, appropriately labeled. Write a program that requests a person’s gross salary, deductions allowed and rate of tax (e.g., 25, meaning 25%), and calculates his net pay as follows: Tax is calculated by applying the rate of tax to the gross salary minus the deductions. Net pay is calculated by gross salary minus tax. Print the gross salary, tax deducted, and net pay, appropriately labeled. Also print the percentage of the gross salary that was paid in tax. Make up appropriate sets of data for testing the program. Write a program that, when run, works as follows (underlined items are typed by the user): Hi, what’s your name?
Alice
Welcome to our show, Alice
How old are you?
27
Hmm, you don’t look a day over 22
Tell me, Alice, where do you live?
Princes Town
Oh, I’ve heard Princes Town is a lovely place
A ball is thrown vertically upwards with an initial speed of U
meters per second. Its height H
after time T
seconds is given by H = UT - 4.9T
2
Write a program that requests U
and T
and prints the height of the ball after T
seconds. Write a program to calculate the cost of carpeting a rectangular room in a house. The program must do the following:
- 请求房间的长度和宽度(假设以米为单位)。
- 询问每平方米地毯的价格。
- 计算房间的面积。
- 计算房间地毯的价格。
- 打印面积和成本,并适当标记。
Write a program which, given a length in inches, converts it to yards, feet, and inches. (1 yard = 3 feet, 1 foot = 12 inches). For example, if the length is 100
inches, the program should print 2 yd 2 ft 4 in
.
四、具有选择逻辑的程序
在本章中,我们将解释以下内容:
- 什么是布尔表达式
- C 如何表示布尔值
- 如何用
if
写程序 - 如何用
if...else
写程序 - 哪里需要分号,哪里是可选的,哪里不能放分号
- 如何测试一个程序
- 为什么符号常量有用以及如何在 C 程序中使用它们
4.1 导言
在上一章中,我们展示了如何使用顺序逻辑编写程序——程序的语句是从第一个到最后一个“按顺序”执行的。
在这一章中,程序将使用选择逻辑——它们将测试一些条件,并根据条件是真还是假采取不同的行动。在 C 语言中,选择逻辑是使用if
和if...else
语句实现的。
4.2 布尔表达式
布尔表达式(以著名的英国数学家乔治·布尔命名)要么为真,要么为假。最简单的布尔表达式是比较两个值的表达式。一些例子是:
k is equal to 999
a is greater than 100
a2 + b2 is equal to c2
b2 is greater than or equal to 4ac
s is not equal to 0
这些都可能是真的或假的。这些是一种叫做关系表达式的特殊布尔表达式的例子。这种表达式只是检查一个值是否等于、不等于、大于、大于或等于、小于以及小于或等于另一个值。我们使用关系运算符来编写它们。
C 关系运算符(带示例)有:
| `==` | `equal to` | `k == 999, a*a + b*b == c*c` | | `!=` | `not equal to` | `s != 0, a != b + c` | | `>` | `greater than` | `a > 100` | | `>=` | `greater than or equal to` | `b*b >= 4.0*a*c` | | `<` | `less than` | `n < 0` | | `<=` | `less than or equal to` | `score <= 65` |布尔表达式通常用于控制程序执行的流程。例如,我们可能有一个变量(h
,比方说)以值0
开始。我们一直以1
递增它,我们想知道它的值何时达到100
。我们说我们希望知道条件h == 100
何时为真。条件是布尔表达式的通用名称。
编程的真正力量在于程序测试一个条件并决定它是真还是假的能力。如果为真,程序可以执行一组动作;如果它是假的,它可以执行另一套或干脆什么都不做。
例如,假设变量score
保存学生在一次测试中获得的分数,如果她的分数为 50 或更高,则该学生通过,如果低于 50,则该学生失败。可以编写一个程序来测试这个条件
score >= 50
如果是真的,学生通过;如果它是假的,学生失败。在 C 语言中,这可以写成:
if (score >= 50) printf("Pass\n");
else printf("Fail\n");
当计算机到达该语句时,它将比较当前值score
和50
。如果值大于或等于50
,我们说条件score >= 50
为真。在这种情况下,程序打印Pass
。如果score
的值小于50
,我们说条件score >= 50
为假。在这种情况下,程序打印Fail
。
在这一章中,我们将看到布尔表达式如何在if
和if...else
语句中使用,在下一章中,我们将看到它们如何在while
语句中使用。
4.2.1 和,&&
使用关系运算符,我们可以创建简单的条件。但是有时候,我们需要问一件事是不是真的,另一件事是不是真的。我们可能还需要知道两件事情中是否有一件是真的。对于这些情况,我们需要复合条件。为了创建复合条件,我们使用逻辑运算符 AND、OR 和 NOT。
例如,假设我们想知道h
的值是否在 1 和 99 之间,包括 1 和 99。我们想知道h
是否大于或等于1
,以及h
是否小于或等于99
。在 C 语言中,我们将此表示为:
(h >= 1) && (h <= 99)
在 C 语言中,和的符号是&&
。
请注意以下几点:
-
变量
h
在两种情况下都必须重复。写h >= 1 && <= 99 //this is wrong
很诱人,但却是错误的 -
h >= 1
和h <= 99
周围的括号是不需要的,但是放上去也没有错。这是因为&&
(和||
,见下一页)的优先级低于关系运算符。如果没有括号,h >= 1 && h <= 99
会被 C 这样解释:(h >= 1) && (h <= 99)
-
这与括号的情况相同。
4.2.2 或者,||
如果 n 是一个表示一年中某个月的整数,我们可以通过测试 n 是小于 1 还是大于 12 来检查 n 是否无效。在 C 语言中,我们将此表示为:
(n < 1) || (n > 12)
在 C # 中,或的符号是||。如上所述,括号不是必需的,我们可以将表达式写成
n < 1 || n > 12
这将测试 n 是否无效。当然,我们可以通过测试 if 来测试 n 是否有效
n >= 1 && n <= 12
我们使用哪种测试取决于我们希望如何表达我们的逻辑。有时使用有效测试很方便,有时使用无效测试。
4.2.3 不,!
如果p
是某个布尔表达式,那么NOT p
反转 p 的真值,换句话说,如果p
为真那么NOT p
为假;如果p
为假,那么NOT p
为真。在 C 中,表示不的符号是感叹号,!
。使用上面的例子,因为
n >= 1 && n <= 12
测试有效n
,条件NOT (n >=1 && n <= 12)
测试无效n
。这是用 C 写的
!(n >= 1 && n <= 12)
这相当于n < 1 || n > 12
。熟悉德摩根定律的人会知道
not (a and b) = (not a) or (not b)
和
not(a or b) = (not a) and (not b)
一般来说,如果p
和q
是布尔表达式,我们有如下表达式:
- 当
p
和q
都是true
和false
时p && q
是true
,否则; - 只有当
p
和q
都是false
时,当p
或q
之一是true
和false
时p || q
才是true
; - 当
p
为false
时!p
为真,当p
为true
时false
为真。
下表对此进行了总结(用T
表示true
,用F
表示false
):
本书中的大多数程序将使用简单的条件。少数会使用复合条件。
4.2.3.1 C99 中的数据类型bool
最初的 C 标准和后来的 ANSI C 标准没有定义布尔数据类型。传统上,C 使用表达式值的概念来表示真/假。数值表达式可用于任何需要真/假值的上下文中。如果表达式的值非零,则认为该表达式为真,如果值为零,则认为该表达式为假。
最新的 C99 标准定义了类型bool
。然而,在本书中,我们将使用传统的方法,主要是因为许多流行的 C 编译器还不支持 C99 标准。同样,正如你将看到的,没有bool
我们也能轻松生活。我们的绝大多数布尔表达式都是在if
和while
语句中使用的关系表达式。如果我们需要一个“布尔”变量,我们可以使用一个int
变量,其中1
代表true
,而0
代表false
。
4.3if
构造
让我们为下面的问题写一个程序:
一家电脑维修店每小时收取 100 美元的人工费,外加维修中使用的任何零件的费用。然而,任何工作的最低收费是 150 美元。提示输入工作小时数和零件成本(可能是$0 ),并打印工作费用。
我们将编写程序,使其工作如下:
Hours worked?
2.5
Cost of parts?
20
Charge for the job: $270.00
或者
Hours worked?
1
Cost of parts?
25
Charge for the job: $150.00
以下算法描述了解决问题所需的步骤:
prompt for and read the hours worked
prompt for and read the cost of parts
calculate charge = hours worked * 100 + cost of parts
if charge is less than 150 then set charge to 150
print charge
这是用伪代码编写的算法的另一个例子——一种指定编程逻辑的非正式方式。
该算法引入了一个新语句——if
语句。表情
charge is less than 150
是条件的一个例子。如果条件为true
,则执行then
之后的语句(称为 then 部分);如果是false
,则不执行then
之后的语句。
程序 P4.1 展示了如何用 C 程序来表达这个算法。
程序 P4.1
//print job charge based on hours worked and cost of parts
#include <stdio.h>
int main() {
double hours, parts, jobCharge;
printf("Hours worked? ");
scanf("%lf", &hours);
printf("Cost of parts? ");
scanf("%lf", &parts);
jobCharge = hours * 100 + parts;
if (jobCharge < 150) jobCharge = 150;
printf("\nCharge for the job: $%3.2f\n", jobCharge);
}
对于这个程序,我们选择使用三个变量— hours
、parts
和jobCharge
,都是double
类型,因为我们可能需要输入工作时间和零件成本的浮点值。
非常重要的是,你要额外努力去理解if
语句,因为它是编程中最重要的语句之一。是if
语句可以让程序看起来像是在思考。
条件
charge is less than 150
伪代码算法的表达式在我们的程序中表示为
jobCharge < 150
执行程序时,以正常方式(hours * 100 + parts
)计算作业费用。然后,if
语句测试这个值jobCharge
是否小于150
;如果是,那么jobCharge
被设置为150
。如果不小于150
,jobCharge
保持原样。声明
if (jobCharge < 150) jobCharge = 150;
是if
结构的一个简单例子。注意到在 C 语言中没有使用单词then
。一般来说,C 语言中的构造采用以下形式:
if (<condition>) <statement>
c 需要单词if
和围绕<condition>
的括号。您必须提供<condition>
和<statement>
,其中<condition>
是一个Boolean
表达式,而<statement>
可以是一行语句或一个块——由{
和}
包围的一个或多个语句。如果<condition>
为true
,则执行<statement>
;如果<condition>
为false
,则不执行<statement>
。在任何一种情况下,程序在<statement>
之后继续执行语句(如果有的话)。
在程序中,<condition>
是
jobCharge < 150
and <statement> is
jobCharge = 150;
举个例子,其中<statement>
是一个块,假设我们想要交换两个变量a
和b
的值,但前提是a
大于b
。这可以通过以下方式来实现,例如,假设a
= 15
、b
= 8
和c
是临时变量:
if (a > b)
{
c = a; //store a in c; c becomes 15
a = b; //store b in a; a becomes 8
b = c; //store old value of a, 15,in b
}
这里,<statement>
是从{
到}
的部分,一个包含三个赋值语句的块。如果a
大于b
,则执行该块(并交换值);如果a
不大于b
,则不执行该块(并且值保持不变)。顺便提一下,要知道交换两个变量的值需要三个赋值语句;两个人是做不到的。如果你不相信,试试看。
一般来说,如果一个条件为真,我们想做几件事;我们必须将它们包含在{
和}
中以创建一个块。这将确保我们满足 C 的规则,即<statement>
是单个语句或块。
在块中缩进语句是很好的编程习惯。这使得一眼就能看出块中有哪些语句。如果我们按如下方式编写上面的代码,代码块的结构就不那么容易看出来了:
if (a > b)
{
c = a; //store a in c; c becomes 15
a = b; //store b in a; a becomes 8
b = c; //store old value of a, 15,in b
}
当我们编写伪代码时,我们通常使用以下格式:
if <condition> then
<statement1>
<statement2>
etc.
endif
构造以endif
结束,这是许多程序员使用的约定。再次注意,如果<condition>
为真,我们缩进要执行的语句。我们强调endif
不是一个 C 语言单词,而仅仅是程序员在编写伪代码时使用的一个方便的单词。
该示例说明了在if
语句中编写块的一种风格。这种风格与{
和}
的搭配如下:
if (<condition>)
{
<statement1>;
<statement2>;
etc.
}
这里,{
和}
与if
排成一行,语句缩进。这样就很容易辨认出体内的东西。对于一个小程序来说,这可能无关紧要,但是随着程序大小的增加,代码的布局反映其结构将变得更加重要。在本书中,我们将使用下面的风格(正如你现在所知道的,编译器不关心使用哪种风格):
if (<condition>) {
<statement1>;
<statement2>;
etc.
}
我们将{
放在右括号后的第一行,让}
与if
匹配;块中的语句是缩进的。我们相信这和第一种风格一样清晰,并且在程序中少了一行!你用哪种风格是个人喜好问题;选择一个,坚持使用。
4.3.1 求两个长度的和
假设长度以米和厘米为单位,例如 3m 75cm。给你两对代表两个长度的整数。编写一个程序,提示输入两个长度并打印它们的和,使厘米值小于 100。
比如3m 25cm
和2m 15cm
之和是5m 40cm
,但是3m 75cm
和5m 50cm
之和是9m 25cm
。
假设程序如下工作:
Enter values for m and cm: 3 75
Enter values for m and cm: 5 50
Sum is 9m 25cm
请注意,数据必须仅由数字输入。例如,如果我们输入3m 75cm
,我们将得到一个错误,因为3m
不是一个有效的整数常量。我们的程序将假设输入的第一个数字是米值,第二个数字是厘米值。
我们将两个米值相加,再将两个厘米值相加,得到总和。如果厘米值小于 100,就没什么可做的了。但如果不是,我们必须从中减去 100,并在米值上加 1。这个逻辑表达如下:
m = sum of meter values
cm = sum of centimeter values
if cm >= 100 then
subtract 100 from cm
add 1 to m
endif
作为边界情况,我们必须检查我们的程序是否工作,如果cm
正好是100
。作为一个练习,验证它确实如此。
程序 P4.2 解决了上述问题。
程序 P4.2
//find the sum of two lengths given in meters and cm
#include <stdio.h>
int main() {
int m1, cm1, m2, cm2, mSum, cmSum;
printf("Enter values for m and cm: ");
scanf("%d %d", &m1, &cm1);
printf("Enter values for m and cm: ");
scanf("%d %d", &m2, &cm2);
mSum = m1 + m2; //add the meters
cmSum = cm1 + cm2; //add the centimeters
if (cmSum >= 100) {
cmSum = cmSum - 100;
mSum = mSum + 1;
}
printf("\nSum is %dm %dcm\n", mSum, cmSum);
}
我们使用变量m1
和cm1
表示第一长度,m2
和cm2
表示第二长度,mSum
和cmSum
表示两个长度的总和。
该程序假设给定长度的厘米部分小于 100,如果是这样,它就能正确工作。但是如果长度是3m 150cm
和2m 200cm
呢?
程序将打印6m 250cm
。(作为练习,按照程序的逻辑来看为什么。)虽然这是正确的,但格式不正确,因为我们要求厘米值小于 100。我们可以通过使用整数除法和%
(余数运算符)来修改我们的程序,使其在这些情况下也能工作。
以下伪代码显示了如何操作:
m = sum of meter values
cm = sum of centimeter values
if cm >= 100 then
add cm / 100 to m
set cm to cm % 100
endif
使用上面的例子,m
被设置为5
,cm
被设置为350
。由于cm
大于100
,我们用整数除法算出350 / 100
(这样算出cm
里有多少个100
s)就是3
;这个加到m
,给出8
。下一行将cm
设置为350 % 100
,也就是50
。所以我们得到的答案是8m 50cm
,是正确的,格式也是正确的。
请注意,“then 部分”中的语句必须按所示顺序书写。我们必须使用cm
的(原始)值计算出cm / 100
,然后在下一条语句中将它改为cm % 100
。作为练习,请计算出如果将这些陈述颠倒过来,总和将计算出什么值。(答案会是5m 50cm
,不对。你能看出为什么吗?)
这些变化反映在程序 P4.3 中。
程序 P4.3
//find the sum of two lengths given in meters and cm
#include <stdio.h>
int main() {
int m1, cm1, m2, cm2, mSum, cmSum;
printf("Enter values for m and cm: ");
scanf("%d %d", &m1, &cm1);
printf("Enter values for m and cm: ");
scanf("%d %d", &m2, &cm2);
mSum = m1 + m2; //add the meters
cmSum = cm1 + cm2; //add the centimeters
if (cmSum >= 100) {
mSum = mSum + cmSum / 100;
cmSum = cmSum % 100;
}
printf("\nSum is %dm %dcm\n", mSum, cmSum);
}
以下是该程序的运行示例:
Enter values for m and cm:
3 150
Enter values for m and cm:
2 200
Sum is 8m 50cm
敏锐的读者可能会意识到我们甚至不需要if
语句。
考虑一下这个:
mSum = m1 + m2; //add the meters
cmSum = cm1 + cm2; //add the centimeters
mSum = mSum + cmSum / 100;
cmSum = cmSum % 100;
其中最后两个语句来自于if
语句。
因此,我们知道,如果cmSum
大于或等于100
,这将起作用,因为在这种情况下,这四个语句将被执行。
如果cmSum
小于100
怎么办?最初,最后两条语句不会被执行,因为if
条件为假。现在他们被处决了。让我们看看会发生什么。以3m 25cm
和2m 15cm
为例,我们得到mSum
为5
和cmSum
为40
。
在下一个语句中40 / 100
是0
,所以mSum
不变,在最后一个语句中40 % 100
是40
,所以cmSum
不变。因此,答案将被正确地打印为
Sum is 5m 40cm
现在你应该开始意识到,通常有不止一种方式来表达程序的逻辑。有了经验和学习,你会知道哪些方式更好,为什么。
4.4if...else
构造
让我们为下面的问题写一个程序:
一个学生要参加 3 次考试,每次满分为 100 分。如果学生的平均分大于或等于 50 分,他就通过了;如果他的平均分小于 50 分,他就不及格。提示 3 个分数,如果学生通过,则打印Pass
,如果学生未通过,则打印Fail
。
我们将假设程序如下工作来编写程序:
Enter 3 marks: 60 40 56
Average is 52.0 Pass
或者
Enter 3 marks: 40 60 36
Average is 45.3
Fail
以下算法描述了解决问题所需的步骤:
prompt for the 3 marks
calculate the average
if average is greater than or equal to 50 then
print "Pass"
else
print "Fail"
endif
从if
到endif
的部分是if...else
结构的一个例子。
条件
average is greater than or equal to 50
是关系表达式的另一个例子。如果条件为true
,则执行then
之后的语句(then 部分);如果是false
,则执行else
(else 部分)之后的语句。
整个构造以endif
结束。
当你写伪代码的时候,重要的是你要表达的逻辑非常清楚。再次注意缩进是如何帮助识别then
部分和else
部分的。
不过,最终你必须用某种编程语言来表达代码,这样它才能在计算机上运行。程序 P4.4 显示了如何为上述算法做到这一点。
程序 P4.4
//request 3 marks; print their average and Pass/Fail
#include <stdio.h>
int main() {
int mark1, mark2, mark3;
double average ;
printf("Enter 3 marks: ");
scanf("%d %d %d", &mark1, &mark2, &mark3);
average = (mark1 + mark2 + mark3) / 3.0;
printf("\nAverage is %3.1f", average);
if (average >= 50) printf(" Pass\n");
else printf(" Fail\n");
}
仔细研究程序中的if...else
结构。它反映了上一页表达的逻辑。再次注意,c 中省略了单词then
。
一般来说,C 中的if...else
构造采用如下所示的形式。
if (<condition>) <statement1> else <statement2>
单词if
和else
以及括号是 c 需要的,你必须提供<condition>
、<statement1>
和<statement2>
。<statement1>
和<statement2>
中的每一个都可以是一行语句或一个块。如果<condition>
为true
,则执行<statement1>
,跳过<statement2>
;如果<condition>
为false
,则跳过<statement1>
,执行<statement2>
。当执行if
构造时,要么执行<statement1>
要么执行<statement2>
,但不能同时执行两者。
如果<statement1>
和<statement2>
是单行语句,可以使用以下布局:
if (<condition>) <statement1>
else <statement2>
如果<statement1>
和<statement2>
是块,可以使用以下布局:
if (<condition>) {
...
}
else {
...
}
在描述 C 中的各种结构时,我们通常使用短语“其中<statement>
可以是一行语句或一个块。”
请记住,在 C 中,对于单行语句,分号被视为语句的一部分。例如:
a = 5;
printf("Pass\n");
scanf("%d", &n);
因此,在使用单行语句的情况下,分号作为语句的一部分,必须存在。在程序 P4.4 中,在if...else
语句中,
<statement1> is
printf("Pass\n");
而<statement2>
是
printf("Fail\n");
但是,对于块或复合语句,右大括号}
结束块。因此,在使用块的情况下,不需要额外的分号来结束块。
有时候,记住整个if...else
结构(从if
到<statement2>
)被 C 认为是一个语句,可以在任何需要一个语句的地方使用,这是很有用的。
4.4.1 计算工资
对于需要块的示例,假设我们有工作小时数和工资率(每小时支付的金额)的值,并希望根据以下内容计算一个人的正常工资、加班工资和总工资:
如果工作时间少于或等于 40 小时,正常工资的计算方法是工作时间乘以工资率,加班工资为 0。如果工作时间超过 40 小时,正常工资的计算方法是 40 乘以工资率,加班工资的计算方法是超过 40 小时乘以工资率 1.5。总工资是通过将正常工资和加班工资相加计算出来的。
例如,如果小时数为36
,费率为每小时20
美元,则正常工资为$720
( 36
乘以20
),加班工资为$0
。总工资是$720
。
而如果小时数为50
,费率为每小时12
美元,则正常工资为$480
( 40
乘以12
),加班工资为$180
(加班时间10
乘以12
乘以1.5
)。毛工资是$660
( 480
+ 180
)。
上述描述可以用伪代码表示如下:
if hours is less than or equal to 40 then
set regular pay to hours x rate
set overtime pay to 0
else
set regular pay to 40 x rate
set overtime pay to (hours – 40) x rate x 1.5
endif
set gross pay to regular pay + overtime pay
我们使用缩进来突出显示条件“小时数小于或等于 40”为真时要执行的语句,以及条件为假时要执行的语句。整个构造以endif
结束。
下一步是将伪代码转换成 C。当我们这样做时,我们必须确保我们坚持 C 编写if...else
语句的规则。在这个例子中,我们必须确保then
和else
部分都被写成块,因为它们都包含不止一个语句。
使用变量hours
(工作时间)rate
(工资率)regPay
(正常工资)ovtPay
(加班工资)和grossPay
(总工资),我们编写 C 代码,因此:
if (hours <= 40) {
regPay = hours * rate;
ovtPay = 0;
} //no semicolon here; } ends the block
else {
regPay = 40 * rate;
ovtPay = (hours - 40) * rate * 1.5;
} //no semicolon here; } ends the block
grossPay = regPay + ovtPay;
请注意这两条注释。在第一个}
后面放一个分号是错误的,因为if
语句继续有一个else
部分。如果我们放一个,它有效地结束了if
语句,C 假设没有else
部分。当它找到单词else
时,将没有与之匹配的if
,程序将给出一个“放错地方”的错误。
在最后一个}
后面不需要分号,但是加一个也没什么坏处。
问题:写一个程序来提示工作时间和工资率。然后,该程序根据上述说明计算并打印正常工资、加班工资和总工资。
以下算法概述了解决方案的整体逻辑:
prompt for hours worked and rate of pay
if hours is less than or equal to 40 then
set regular pay to hours x rate
set overtime pay to 0
else
set regular pay to 40 x rate
set overtime pay to (hours – 40) x rate x 1.5
endif
set gross pay to regular pay + overtime pay
print regular pay, overtime pay and gross pay
该算法在程序 P4.5 中实现。所有变量都声明为double
,以便输入工作时间和工资率的小数值。
程序 P4.5
#include <stdio.h>
int main() {
double hours, rate, regPay, ovtPay, grossPay;
printf("Hours worked? ");
scanf("%lf", &hours);
printf("Rate of pay? ");
scanf("%lf", &rate);
if (hours <= 40) {
regPay = hours * rate;
ovtPay = 0;
}
else {
regPay = 40 * rate;
ovtPay = (hours - 40) * rate * 1.5;
}
grossPay = regPay + ovtPay;
printf("\nRegular pay: $%3.2f\n", regPay);
printf("Overtime pay: $%3.2f\n", ovtPay);
printf("gross pay: $%3.2f\n", grossPay);
}
该程序的运行示例如下所示:
Hours worked?
50
Rate of pay?
12
Regular pay: $480.00
Overtime pay: $180.00
Gross pay: $660.00
你应该核实结果确实是正确的。
注意,尽管hours
和rate
是double
,但是它们的数据可以以任何有效的数字格式提供——这里我们使用整数50
和12
。这些值在存储到变量中之前会被转换成double
格式。例如,如果我们愿意,我们可以键入50.0
和12.00
。
4.5 关于程序测试
当我们写一个程序时,我们应该彻底地测试它,以确保它正确地工作。至少,我们应该测试程序中的所有路径。这意味着我们必须选择测试数据,使得程序中的每条语句至少执行一次。
对于程序 P4.5,样本仅在工作时间大于 40 小时时运行测试。仅仅基于这个测试,我们不能确定如果工作时间少于或等于 40 小时,我们的程序将正确工作。可以肯定的是,我们必须运行另一个测试,其中工作时间少于或等于 40。下面是这样一个运行示例:
Hours worked?
36
Rate of pay?
20
Regular pay: $720.00
Overtime pay: $0.00
Gross pay: $720.00
这些结果是正确的,这使我们更加确信我们的程序是正确的。我们还应该在时间正好是40
时进行测试;我们必须总是在程序的“边界”测试它对于这个程序,40
是一个界限——它是开始支付加班工资的值。
如果结果不正确怎么办?比如假设加班费是错的。我们说程序包含一个 bug(一个错误),我们必须调试程序(从程序中移除错误)。在这种情况下,我们可以查看计算加班工资的报表,看看我们是否正确地指定了计算方法。如果这不能发现错误,我们必须使用产生错误的测试数据,费力地手工“执行”程序。如果操作得当,这通常会揭示错误的原因。
4.6 符号常量
在程序 4.1 中,我们使用了两个常量——100 和 150——分别表示每小时的人工费用和最低工作成本。如果这些值在程序写完之后改变了呢?我们必须找到它们在程序中的所有出现,并将它们更改为新值。
这个程序相当短,所以这不会太难做到。但是想象一下,如果程序包含数百甚至数千行代码,任务会是什么样子。做出所有必要的改变将是困难的、耗时的并且容易出错的。
我们可以通过使用符号常量(也称为显式常量或命名常量)来使生活变得稍微容易一点,符号常量是我们在一个地方设置为所需常量的标识符。如果我们需要改变一个常量的值,只需要在一个地方进行改变。例如,在程序 P4.1 中,我们可以使用符号常量ChargePerHour
和MinJobCost
。我们会将ChargePerHour
设为100
,将MinJobCost
设为150
。
在 C 语言中,我们使用#define
指令来定义符号常量,以及其他用途。我们通过将程序 P4.1 重写为程序 P4.6 来演示如何操作。
程序 P4.6
//This program illustrates the use of symbolic constants
//Print job charge based on hours worked and cost of parts
#include <stdio.h>
#define ChargePerHour 100
#define MinJobCost 150
int main() {
double hours, parts, jobCharge;
printf("Hours worked? ");
scanf("%lf", &hours);
printf("Cost of parts? ");
scanf("%lf", &parts);
jobCharge = hours * ChargePerHour + parts;
if (jobCharge < MinJobCost) jobCharge = MinJobCost;
printf("\nCharge for the job: $%3.2f\n", jobCharge);
}
4 . 6 . 1#define
指令
C 语言中的指令通常出现在程序的顶部。出于我们的目的,#define 指令采用以下形式:
#define identifier followed by the "replacement text"
在节目中,我们使用了
#define ChargePerHour 100
注意,这不是一个普通的 C 语句,不需要分号来结束它。这里,标识符是ChargePerHour
,替换文本是常量100
。在程序体中,我们使用标识符而不是常量。
当程序被编译时,C 执行所谓的“预处理”步骤。它用替换文本替换标识符的所有出现。在程序 P4.6 中,将所有出现的ChargePerHour
替换为100
,将所有出现的MinJobCost
替换为150
。完成这些后,程序就编译好了。由程序员负责确保当标识符被替换时,结果语句是有意义的。
实际上,指令说标识符ChargePerHour
等价于常量100
,标识符MinJobCost
等价于150
。
例如,预处理步骤改变
if (jobCharge < MinJobCost) jobCharge = MinJobCost;
到
if (jobCharge < 150) jobCharge = 150;
例如,假设最低工作成本从150
变为180
。我们只需要改变#define
指令中的值,因此:
#define MinJobCost 180
不需要其他的改变。
在本书中,我们将使用以大写字母开始符号常量标识符的惯例。但是请注意,C 允许您使用任何有效的标识符。
4.6.2 示例–符号常量
举一个稍微大一点的例子,考虑程序 P4.5。在这里,我们使用了两个常量——40 和 1.5——分别表示最大正常工作时间和加班工资系数。我们使用符号常量MaxRegularHours
(设置为 40)和OvertimeFactor
(设置为 1.5)将程序 P4.5 重写为程序 P4.7。
程序 P4.7
#include <stdio.h>
#define MaxRegularHours 40
#define OvertimeFactor 1.5
int main() {
double hours, rate, regPay, ovtPay, grossPay;
printf("Hours worked? ");
scanf("%lf", &hours);
printf("Rate of pay? ");
scanf("%lf", &rate);
if (hours <= MaxRegularHours) {
regPay = hours * rate;
ovtPay = 0;
}
else {
regPay = MaxRegularHours * rate;
ovtPay = (hours - MaxRegularHours) * rate * OvertimeFactor;
}
grossPay = regPay + ovtPay;
printf("\nRegular pay: $%3.2f\n", regPay);
printf("Overtime pay: $%3.2f\n", ovtPay);
printf("Gross pay: $%3.2f\n", grossPay);
}
例如,假设最大正常工作时间从40
变为35
。程序 P4.7 比程序 P4.5 更容易修改,因为我们只需要修改#define
指令中的值,就像这样:
#define MaxRegularHours 35
不需要其他的改变。
程序 P4.5 中使用的数字40
和1.5
被称为幻数——它们出现在程序中没有明显的原因,就像变魔术一样。幻数是一个很好的迹象,表明一个程序可能是限制性的,依赖于这些数字。尽可能地,我们必须编写没有幻数的程序。使用符号常量有助于使我们的程序更加灵活和易于维护。
4.7 更多示例
我们现在编写程序来解决另外两个问题。他们的解决方案将说明如何使用 if...else 语句来确定从几个选项中选择哪一个。在示例运行中,带下划线的项目由用户键入;其他的都是电脑打印的。
4.7.1 打印字母等级
编写一个程序,请求在测试中获得分数,并根据以下内容打印一个字母等级:
| `score < 50` | `F` | | `50 <= score < 75` | `B` | | `score >= 75` | `A` |该程序应该如下工作:
Enter a score: 70
Grade B
解决方案如程序 P4.8 所示。
程序 P4.8
//request a score; print letter grade
#include <stdio.h>
int main() {
int score;
printf("Enter a score: ");
scanf("%d", &score);
printf("\nGrade ");
if (score < 50) printf("F\n");
else if (score < 75) printf("B\n");
else printf("A\n");
}
第二个printf
打印一个空行,后跟单词Grade
,后跟一个空格,但不结束该行。当字母等级确定后,它将打印在同一行上。
我们看到,if...else
语句采用以下形式
if (<condition>) <statement1> else <statement2>
其中<statement1>
和<statement2>
可以是任意语句。特别是,其中任何一个(或两个)都可以是一个if...else
语句。这允许我们编写所谓的嵌套 if 语句。当我们有几个相关的条件要测试时,这特别有用,就像这个例子一样。在节目中,我们可以想到这一部分:
if (score < 50) printf("F\n");
else if (score < 75) printf("B\n");
else printf("A\n");
如同
if (score < 50) printf("F\n");
else <statement>
这里的<statement>
是这个if...else
语句:
if (score < 75) printf("B\n");
else printf("A\n");
如果score
小于50
,程序打印F
并结束。如果不是,那么score
必须大于或等于50
。
知道了这一点,第一个else
部分检查score
是否小于75
。如果是,程序打印B
并结束。如果不是,那么score
必须大于或等于75
。
了解到这一点,第二个else
部分(else printf("A\n");
与第二个if
匹配)打印A
并结束。
为了确保程序是正确的,你应该用至少 3 个不同的分数(例如70
、45
、83
)来运行它,以验证 3 个等级中的每一个都被正确打印。你也应该在“边界”数字、50
和75
上测试它。
注意书写else if
的首选风格。如果我们遵循我们正常的缩进风格,我们将会书写
if (score < 50) printf("F\n");
else
if (score < 75) printf("B\n");
else printf("A\n");
当然,这仍然是正确的。然而,如果我们有更多的案例,缩进会太深,看起来会很笨拙。此外,由于不同的分数范围实际上是可选的(而不是一个在另一个之内),最好将它们保持在相同的缩进级别。
这里的语句都是单行的 printf 语句,所以我们选择将它们写在 if 和 else 的同一行。但是,如果它们是块,最好这样写:
if (score < 50) {
...
}
else if (score < 75) {
...
}
else {
...
}
作为练习,修改程序以根据以下内容打印正确的分数:
| `score < 50` | `F` | | `50 <= score < 65` | `C` | | `50 <= score < 80` | `B` | | `score >= 80` | `A` |对三角形进行分类
给定代表三角形边的三个整数值,打印:
Not a triangle
如果值不能是任何三角形的边。如果任何一个值是负数或零,或者任何一条边的长度大于或等于另外两条边的长度之和,就会出现这种情况;Scalene
如果三角形是不规则的(各边不同);Isosceles
如果三角形是等腰的(两边相等);Equilateral
如果三角形是等边的(三条边相等)。
该程序应该如下工作:
Enter 3 sides of a triangle: 7 4 7
Isosceles
解决方案如程序 P4.9 所示。
程序 P4.9
//request 3 sides; determine type of triangle
#include <stdio.h>
int main() {
int a, b, c;
printf("Enter 3 sides of a triangle: ");
scanf("%d %d %d", &a, &b, &c);
if (a <= 0 || b <= 0 || c <= 0) printf("\nNot a triangle\n");
else if (a >= b + c || b >= c + a || c >= a + b)
printf("\nNot a triangle\n");
else if (a == b && b == c) printf("\nEquilateral\n");
else if (a == b || b == c || c == a) printf("\nIsosceles\n");
else printf("\nScalene\n");
}
第一个任务是确定我们实际上有一个有效的三角形。第一个if
检查任一侧是否为负或零。如果是,则打印Not a triangle
。如果它们都是肯定的,我们转到else
部分,它本身由一个if...else
语句组成。
这里,if
检查任何一边是否大于或等于其他两边的总和。如果是,则打印Not a triangle
。如果不是,那么我们有一个有效的三角形,必须通过执行else
部分开始来确定它的类型
if (a == b ...
最简单的方法是首先检查它是否是等边的。如果两对不同的边相等,那么三条边都相等,我们就有一个等边三角形。
如果它不是等边的,那么我们检查它是否是等腰的。如果任意两条边相等,我们就有一个等腰三角形。
如果它既不是等边的,也不是等腰的,那么它一定是不等边的。
作为练习,修改程序以确定三角形是否成直角。如果两条边的平方和等于第三条边的平方,那么这条线就是直角。
Exercises 4An auto repair shop charges as follows. Inspecting the vehicle costs $75. If no work needs to be done, there is no further charge. Otherwise, the charge is $75 per hour for labor plus the cost of parts, with a minimum charge of $120. If any work is done, there is no charge for inspecting the vehicle. Write a program to read values for hours worked and cost of parts (either of which could be 0) and print the charge for the job. Write a program that requests two weights in kilograms and grams and prints the sum of the weights. For example, if the weights are 3kg 500g
and 4kg 700g
, your program should print 8kg 200g
. Write a program that requests two lengths in feet and inches and prints the sum of the lengths. For example, if the lengths are 5 ft. 4 in.
and 8 ft. 11 in.
, your program should print 14 ft. 3 in.
(1 ft. = 12 in.) A variety store gives a 15% discount on sales totaling $300 or more. Write a program to request the cost of three items and print the amount the customer must pay. Write a program to read two pairs of integers. Each pair represents a fraction. For example, the pair 3 5
represents the fraction 3/5
. Your program should print the sum of the given fractions. For example, give the pairs 3 5
and 2 3
, your program should print 19/15
, since 3/5 + 2/3 = 19/15
. Modify the program so that it prints the sum with the fraction reduced to a proper fraction; for this example, your program should print 1 4/15
. Write a program to read a person’s name, hours worked, hourly rate of pay, and tax rate (a number representing a percentage, e.g., 25 meaning 25%). The program must print the name, gross pay, tax deducted, and gross pay. Gross pay is calculated as described in Section 4.4.1. The tax deducted is calculated by applying the tax rate to 80% of gross pay. And the net pay is calculated by subtracting the tax deducted from the gross pay. For example, if the person works 50 hours at $20/hour and the tax rate is 25%, his gross pay would be (40 x 20) + (10 20 1.5) = $1100. He pays 25% tax on 80% of $1100, that is, 25% of $880 = $220. His net pay is 1100 - 220 = $880. Write a program to read integer values for month
and year
and print the number of days in the month. For example, 4 2005
(April 2005) should print 30
, 2 2004
(February 2004) should print 29
and 2 1900
(February 1900) should print 28
. A leap year, n, is divisible by 4; however, if n is divisible by 100 then it is a leap year only if it is also divisible by 400. So 1900 is not a leap year but 2000 is. In an English class, a student is given three term tests (marked out of 25) and an end-of-term test (marked out of 100). The end-of-term test counts the same as the three term tests in determining the final mark (out of 100). Write a program to read marks for the three term tests followed by the mark for the end-of-term test. The program then prints the final mark and an indication of whether the student passes or fails. To pass, the final mark must be 50 or more. For example, given the data 20 10 15 56
, the final mark is calculated by (20+10+15)/75*50 + 56/100*50 = 58
Write a program to request two times given in 24-hour clock format and find the time (in hours and minutes) that has elapsed between the first time and the second time. You may assume that the second time is later than the first time. Each time is represented by two numbers: e.g., 16 45
means the time 16:45, that is, 4:45 p.m. For example, if the two given times are 16 45
and 23 25
your answer should be 6
hours 40
minutes. Modify the program so that it works as follows: if the second time is sooner than the first time, take it to mean a time for the next day. For example, given the times 20:30
and 6:15
, take this to mean 8.30 p.m. to 6.15 a.m. of the next day. Your answer should be 9
hours 45
minutes. A bank pays interest based on the amount of money deposited. If the amount is less than $5,000, the interest is 4% per annum. If the amount is $5,000 or more but less than $10,000, the interest is 5% per annum. If the amount is $10,000 or more but less than $20,000, the interest is 6% per annum. If the amount is $20,000 or more, the interest is 7% per annum. Write a program to request the amount deposited and print the interest earned for one year. For any year
between 1900 and 2099, inclusive, the month
and day
on which Easter Sunday falls can be determined by the following algorithm: set a to year minus 1900
set b to the remainder when a is divided by 19
set c to the integer quotient when 7b + 1 is divided by 19
set d to the remainder when 11b + 4 - c is divided by 29
set e to the integer quotient when a is divided by 4
set f to the remainder when a + e + 31 - d is divided by 7
set g to 25 minus the sum of d and f
if g is less than or equal to 0 then
set month to 'March'
set day to 31 + g
else
set month to 'April'
set day to g
endif
Write a program that requests a year between 1900 and 2099, inclusive, and checks if the year is valid. If it is, print the day on which Easter Sunday falls in that year. For example, if the year is 1999
, your program should print April 4
. Write a program to prompt for the name of an item, its previous price, and its current price. Print the percentage increase or decrease in the price. For example, if the previous price is $80 and the current price is $100, you should print increase of 25%
; if the previous price is $100 and the current price is $80, you should print decrease of 20%
. A country charges income tax as follows based on one’s gross salary. No tax is charged on the first 20% of salary. The remaining 80% is called taxable income. Tax is paid as follows:
- 应纳税收入的前 15,000 美元的 10%;
- 接下来的 20,000 美元应税收入的 20%;
- 超过 35,000 美元的所有应税收入的 25%。
Write a program to read a value for a person’s salary and print the amount of tax to be paid. Also print the average tax rate, that is, the percentage of salary that is paid in tax. For example, on a salary of $20,000, a person pays $1700 in tax. The average tax rate is 1700/20000*100 = 8.5%
.
五、具有重复逻辑的程序
在本章中,我们将解释以下内容:
- 如何使用
while
构造在程序中执行“循环” - 如何求任意一组数的和与平均值
- 如何让一个程序“算数”
- 如何找出任意一组数字中的最大值和最小值
- 如何从文件中读取数据
- 如何将输出写入文件
- 如何使用
for
构造在程序中执行“循环” - 如何使用
for
生成表格
5.1 导言
在第三章中,我们向你展示了如何使用顺序逻辑编写程序——程序的语句从第一个到最后一个“按顺序”执行。
在第四章中,我们向你展示了如何为需要选择逻辑的问题编写程序。这些程序使用了if
和if...else
语句。
在这一章中,我们讨论需要重复逻辑的问题。这个想法是写一次语句,让计算机重复执行它们,只要某些条件为真。我们将看到如何使用while
和for
语句来表达重复逻辑。
5.2 while 结构
考虑这样一个问题:编写一个程序来寻找用户一次输入一个数字的和。程序将提示用户输入如下数字:
Enter a number:
13
Enter a number:
8
Enter a number:
16
等等。我们想让用户输入任意多的数字。因为我们不知道会有多少,而且每次运行程序时数量会有所不同,所以我们必须让用户“告诉”我们何时他想停止输入数字。
他如何“告诉”我们?嗯,用户唯一一次与程序“交谈”是在他键入一个数字来回应提示的时候。如果他希望停止输入数字,他可以输入一些“商定的”值;当程序读取这个值时,它将知道用户希望停止。
在这个例子中,我们可以使用0
作为告诉程序用户希望停止的值。当一个值以这种方式使用时,它被称为标记值或数据结束值。它有时被称为流氓值-该值不被视为实际数据值之一。
我们可以用什么作为哨兵值?任何不能与实际数据值混淆的值都是可以的。例如,如果数据值都是正数,我们可以使用0
或1
作为标记值。当我们提示用户时,提醒他使用什么值作为哨兵值是个好主意。
假设我们希望程序如下运行:
Enter a number (0 to end):
24
Enter a number (0 to end):
13
Enter a number (0 to end):
55
Enter a number (0 to end):
32
Enter a number (0 to end):
19
Enter a number (0 to end):
0
The sum is 143
我们如何让程序像那样运行?我们希望能够以计算机能够理解的形式表达下面的逻辑:
As long as the user does not enter 0, he will continue to be prompted to enter another number and add it to the total.
似乎很明显,我们必须,至少,提示他第一个数字。如果这个数字是0
,我们必须打印出总和(当然,此时应该是0
)。如果数字不是0
,我们必须把它加到总数中,并提示输入另一个数字。如果这个数字是0
,我们必须打印出总和。如果这个数字不是0
,我们必须把它加到总数中,并提示输入另一个数字。如果这个数字是0
...,等等。
当用户输入0
时,该过程将结束。
这个逻辑用一个while
结构(也称为while
语句或while
循环)表达得非常简洁:
//Algorithm for finding sum
set sum to 0
get a number, num
while num is not 0 do
add num to sum
get another number, num
endwhile
print sum
特别注意,我们在进入while
循环之前得到一个数字。这是为了确保while
条件第一次有意义。(如果num
没有价值,那就没有意义了。)
要计算总和,我们需要:
- 选择一个变量来保存总和;我们将使用
sum
。 - 将
sum
初始化为0
(在while
循环之前)。 - 给
sum
加一个数(在while
循环内)。每次循环时都会增加一个数字。
退出循环时,sum
包含所有输入数字的总和。
只要某个条件为true
,这个while
构造就可以让我们重复执行一个或多个语句。这里,两个声明
add num to sum
get another number, num
只要条件num is not 0
为真,就重复执行。
在伪代码中,while
构造通常如下所示:
while <condition> do
statements to be executed repeatedly
endwhile
要重复执行的语句被称为while
结构的主体(或者简单地说,循环的主体)。该构造按如下方式执行:
<condition>
is tested. If true
, the body is executed and we go back to step 1; if false
, we continue with the statement, if any, after endwhile
.
我们现在展示如何使用上面输入的样本数据来执行该算法。为便于参考,数据按以下顺序输入:
24 13 55 32 19 0
最初,num
未定义,sum
为0
。我们显示如下:
24
被输入并存储在num
中;
num
不是0
所以我们进入while
循环;
num
( 24
)加到sum
( 0
,给出:
13
被输入并存储在num
中;
num
不是0
所以我们进入while
循环;
num
( 13
)加到sum
( 24
,给出:
55
被输入并存储在num
中;
num
不是0
所以我们进入while
循环;
num
( 55
)加到sum
( 37
,给出:
32
被输入并存储在num
中;
num
不是0
所以我们进入while
循环;
num
( 32
)加到sum
( 92
,给出:
19
被输入并存储在num
中;
num
不是0
所以我们进入while
循环;
num
( 19
)加到sum
( 124
,给出:
0
被输入并存储在num
中;
num
是0
,因此我们退出while
循环,并使用
sum
现在是143
,所以算法打印出143
。
当一个 while 结构被执行时,我们说程序正在循环或者 while 循环正在被执行。
如何在 c 语言中表达这个算法还有待展示。程序 P5.1 展示了如何表达。
Program P5.1
//print the sum of several numbers entered by a user
#include <stdio.h>
int main() {
int num, sum = 0;
printf("Enter a number (0 to end): ");
scanf("%d", &num);
while (num != 0) {
sum = sum + num;
printf("Enter a number (0 to end): ");
scanf("%d", &num);
}
printf("\nThe sum is %d\n", sum);
}
特别有趣的是 while 语句。伪代码
while num is not 0 do
add num to sum
get another number, num
endwhile
在 C 语言中表示为
while (num != 0) {
sum = sum + num;
printf("Enter a number (0 to end): ");
scanf("%d", &num);
}
当程序运行时,如果输入的第一个数字是0
,会发生什么?因为num
是0
,所以while
条件立即是false
,所以我们退出while
循环,继续执行printf
语句。程序将打印出正确的答案:
The sum is 0
一般来说,如果第一次测试时while
条件是false
,那么主体根本不执行。
形式上,C 语言中的while
构造定义如下:
而(
单词while
和括号是必需的。您必须提供<condition>
和<statement>. <statement>
必须是一条语句或一个块——由{
和}
括起来的一条或多条语句。第一,<condition>
是测试;如果为真,则执行<statement>
并再次测试<condition>
。重复此过程,直到<condition>
变为false
;当这种情况发生时,在<statement>
之后继续执行语句(如果有的话)。如果第一次<condition>
为false
,则<statement>
不执行,继续执行以下语句(如有)。
在程序 P5.1 中,<condition>
是num != 0
,<statement>
是块
{
sum = sum + num;
printf("Enter a number (0 to end): ");
scanf("%d", &num);
}
如果<condition>
为真,每当我们想执行几个语句时,我们必须用{
和}
将语句括起来。实际上,这使它们成为一个语句,一个复合语句,满足 C 的语法规则,即要求一个语句作为主体。
最高公因数
让我们写一个程序,求两个数的最高公因数 HCF(也叫最大公约数,GCD)。该程序将按如下方式运行:
Enter two numbers:
42 24
Their HCF is 6
我们将使用欧几里德算法来寻找两个整数的 HCF,m
和n
。算法如下:
1\. if n is 0, the HCF is m and stop
2\. set r to the remainder when m is divided by n
3\. set m to n
4\. set n to
r
5\. go to step 1
使用m
作为42
,使用n
作为24
,逐步执行算法,并验证它是否给出正确答案6
。
只要n
不是0
,就执行步骤 2、3 和 4。因此,该算法可以用一个while
循环表示如下:
while n is not 0 do
set r to m % n
set m to n
set n to r
endwhile
HCF is m
我们现在可以编写程序 P5.2,它查找输入的两个数字的 HCF。
Program P5.2
//find the HCF of two numbers entered by a user
#include <stdio.h>
int main() {
int m, n, r;
printf("Enter two numbers: ");
scanf("%d %d", &m, &n);
while (n != 0) {
r = m % n;
m = n;
n = r;
}
printf("\nTheir HCF is %d\n", m);
}
注意,while
条件是n != 0
并且while
主体是块
{
r = m % n;
m = n;
n = r;
}
无论m
是否大于n
,算法和程序都会工作。使用上面的例子,如果m
是24
而n
是42
,当第一次执行循环时,它会将m
设置为42
并将n
设置为24
。一般来说,如果m
小于n
,算法做的第一件事就是交换它们的值。
5.3 保持计数
程序 P5.1 计算输入的一组数字的和。假设我们想计算输入了多少个数字,而不是计算数据结束符0
。我们可以使用一个整数变量n
来保存计数。为了让程序保持计数,我们需要执行以下操作:
- 选择一个变量来保存计数;我们选择
n
。 - 将
n
初始化为0
。 - 在适当的位置添加
1
到n
。这里,我们需要在用户每次输入一个非零数字时将1
加到n
上。 - 打印计数。
程序 P5.3 是用于计数的修改程序。
Program P5.3
//print the sum and count of several numbers entered by a user
#include <stdio.h>
int main() {
int num, sum = 0, n = 0;
printf("Enter a number (0 to end): ");
scanf("%d", &num);
while (num != 0) {
n = n + 1;
sum = sum + num;
printf("Enter a number (0 to end): ");
scanf("%d", &num);
}
printf("\n%d numbers were entered\n", n);
printf("The sum is %d\n", sum);
}
以下是该程序的运行示例:
Enter a number (0 to end):
24
Enter a number (0 to end):
13
Enter a number (0 to end):
55
Enter a number (0 to end):
32
Enter a number (0 to end):
19
Enter a number (0 to end):
0
5 numbers were entered
The sum is 143
对程序 P5.3 的评论
- 我们在
while
循环之前声明并初始化n
和sum
到0
。 - 语句
n = n + 1;
将1
加到n
上。我们说n
增加了1
。假设n
具有值3
。 - 当计算右侧时,得到的值是
3 + 1 = 4
。该值存储在左侧的变量中,即n
。最终结果是4
被存储在n
中。 - 该语句被放在循环内部,以便每次执行循环体时
n
都递增。由于循环体是在num
不是0
时执行的,所以n
的值总是到目前为止输入的数字的数量。 - 当我们退出
while
循环时,n
中的值将是输入的数字数量,不包括0
。然后打印该值。 - 注意,如果输入的第一个数字是
0
,则while
条件将立即为假,控制将直接转到循环后的第一个printf
语句,其中n
和sum
都具有值0
。程序将正确打印:0 numbers were entered
The sum is 0
- 如果输入一个数字,程序会打印出
"1 numbers were entered"
——不是很好的英文。使用一个if
语句来解决这个问题。
5.3.1 求平均值
程序 P5.3 可以很容易地修改,以找到输入数字的平均值。正如我们在上面看到的,在退出while
循环时,我们知道总数(sum
)和输入了多少个数字(n
)。我们可以添加一个printf
语句,将平均值打印到小数点后两位,比如:
printf("The average is %3.2f\n", (double) sum/n);
对于示例运行中的数据,输出将是
5 numbers were entered
The sum is 143
The average is 28.60
如第 2.5.4 节所述,注意强制浮点计算的强制转换(double
)的使用。如果没有它,由于sum
和n
是int
,将执行整数除法,给出28
。
或者,我们可以将sum
声明为double
,并打印总和以及平均值,如下所示:
printf("The sum is %3.0f\n", sum);
printf("The average is %3.2f\n", sum/n);
但是,还有一个问题。如果用户输入0
作为第一个数字,执行将到达最后一个printf
语句,其中sum
和n
都具有值0
。程序将试图用0
除0
,给出错误“试图用0.
除”。这是一个运行时(或执行)错误的例子。
为了迎合这种情况,我们可以在while
循环后使用以下代码:
if (n == 0) printf("\nNo numbers entered\n");
else {
printf("\n%d numbers were entered\n", n);
printf("The sum is %d\n", sum);
printf("The average is %3.2f\n", (double) sum/n);
}
这个故事的寓意是,只要有可能,你应该试着预测你的程序可能失败的方式,并迎合它们。这就是所谓的防御性编程的一个例子。
5.4 递增和递减运算符
有许多运算符起源于 C,并赋予了 C 独特的风格。其中最著名的是增量运算符++
。在上一个节目中,我们使用了
n = n + 1;
将1
加到n
上。声明
n++;
做同样的事情。运算符++
将1
添加到其参数中,该参数必须是一个变量。可以写成前缀(++n
)或者后缀(n++
)。
即使++n
和n++
都将1
加到了n
上,但在某些情况下,++n
的副作用与n++
不同。这是因为++n
在使用其值之前递增n
,而n++
在使用其值之后递增n
。作为一个例子,假设n
具有值7
。声明
a = ++n;
首先增加n
,然后将值(8
)分配给a
。但是声明
a = n++;
首先将值7
分配给a
,然后将n
增加到8
。不过,在这两种情况下,最终结果都是n
被赋予了值8
。
作为练习,下面打印的是什么?
n = 5;
printf("Suffix: %d\n", n++);
printf("Prefix: %d\n", ++n);
减量运算符--
类似于++
,只是它从变量参数中减去了1
。例如,--n
和n--
都相当于
n = n - 1;
如上所述,--n
减去1
,然后使用n
的值;n--
使用n
的值,然后从中减去1
。用--
代替++
来做上面的练习会很有用。
5.5 赋值运算符
到目前为止,我们已经使用赋值操作符=
将表达式的值赋给变量,如下所示:
c = a + b
由变量=
和表达式组成的整个结构被称为赋值表达式。当表达式后面跟一个分号时,它就变成了一个赋值语句。赋值表达式的值就是赋给变量的值。例如,如果a
是15
,而b
是20
,那么赋值表达式
c = a + b
将值35
分配给c
。(整个)赋值表达式的值也是35
。
多重赋值是可能的,如
a = b = c = 13
操作符=
从右到左求值,所以上面等价于
a = (b = (c = 13))
最右边的赋值首先完成,然后是左边的,依此类推。
c 提供了其他赋值运算符,其中使用最广泛的是+=
。在上面的程序 P5.3 中,我们使用了语句
sum = sum + num;
将num
的值加到sum
上。用+=
可以写得更清楚,比如:
sum += num; //add num to sum
要将3
加到n
上,我们可以这样写
n += 3
这和
n = n + 3
其他赋值运算符包括-=
、*=
、/=、%=
。如果op
代表+
、-
、*
、/
或%
中的任意一个,则
variable op= expression
相当于
variable = variable op expression
我们指出,我们可以不用递增、递减或特殊的赋值操作符来编写所有的程序。然而,有时,它们允许我们更简洁、更方便、可能更清楚地表达某些操作。
5.6 找到最大的
假设我们想写一个程序,工作方式如下:用户输入一些数字,程序会找到输入的最大数字。以下是该程序的运行示例(带下划线的项目由用户键入):
Enter a number (0 to end):
36
Enter a number (0 to end):
17
Enter a number (0 to end):
43
Enter a number (0 to end):
52
Enter a number (0 to end):
50
Enter a number (0 to end):
0
The largest is 52
用户将被提示输入数字,一次一个。我们将假设输入的数字都是正整数。我们将让用户输入她喜欢的数字。然而,在这种情况下,她需要告诉程序她什么时候想停止输入数字。为此,她将键入0
。
寻找最大数量包括以下步骤:
- 选择一个变量来保存最大的数字;我们选择
bigNum
。 - 将
bigNum
初始化为一个非常小的值。选择的值应该是这样的,无论输入什么数字,它的值都要大于这个初始值。因为我们假设输入的数字是正数,所以我们可以将bigNum
初始化为0
。 - 当每个数字(
num
,比方说)被输入时,它与bigNum
进行比较;如果num
大于bigNum
,那么我们有一个更大的数字,并且bigNum
被设置为这个新的数字。 - 当所有的数字都被输入和检查后,
bigNum
将包含最大的一个。
这些想法用下面的算法来表达:
set bigNum to 0
get a number, num
while num is not 0 do
if num is bigger than bigNum, set bigNum to num
get a number, num
endwhile
print bigNum
像以前一样,我们在进入while
循环之前得到第一个数字。这是为了确保while
条件第一次有意义(被定义)。如果num
没有价值,那就没有意义。如果不是0
,我们进入循环。在循环内部,我们处理数字(与bigNum
等进行比较)。)之后我们得到另一个数。然后这个数字被用于while
条件的下一次测试。当while
条件为false
( num
为0
)时,程序在循环后继续执行print
语句。
该算法如程序 P5.4 所示实现。
Program P5.4
//find the largest of a set of numbers entered
#include <stdio.h>
int main() {
int num, bigNum = 0;
printf("Enter a number (0 to end): ");
scanf("%d", &num);
while (num != 0) {
if (num > bigNum) bigNum = num; //is this number bigger?
printf("Enter a number (0 to end): ");
scanf("%d", &num);
}
printf("\nThe largest is %d\n", bigNum);
}
让我们使用本节开始时输入的示例数据来“逐步完成”这个程序。为便于参考,数据按以下顺序输入:
36 17 43 52 50 0
最初,num
未定义,bigNum
为0
。我们将此显示为:
36
被输入并存储在num
中;
num
不是0
所以我们进入while
循环;
num
( 36
)与bigNum
(0)相比较;
36
更大,因此bigNum
被设置为36
,给出:
17
被输入并存储在num
中;
num
不是0
所以我们进入while
循环;
num
( 17
)对比bigNum
(36
);
17
不大,所以bigNum
保持在36
,给出:
43
被输入并存储在num
中;
num
不是0
所以我们进入while
循环;
num
( 43
)对比bigNum
(36
);
43
更大,因此bigNum
被设置为43
,给出:
52
被输入并存储在num
中;
num
不是0
所以我们进入while
循环;
num
( 52
)对比bigNum
(43
);
52
更大,因此bigNum
被设置为52
,给出:
50
被输入并存储在num
中;
num
不是0
所以我们进入while
循环;
num
( 50
)对比bigNum
(52
);
50
不大,所以bigNum
保持在52
,给出:
0
被输入并存储在num
中;
num
是0
,因此我们退出while
循环,并使用
bigNum
现在是52
,并且打印出printf
报表
The largest is
52
5.7 找到最小的
除了寻找一组项目中最大的项目,我们有时还对寻找最小的项目感兴趣。我们将找到一组整数中最小的一个。为此,需要执行以下步骤:
- 选择一个变量来保存最小的数字;我们选择
smallNum
。 - 将
smallNum
初始化为一个非常大的值。选择的值应该是这样的,无论输入什么数字,它的值都小于这个初始值。如果我们知道我们将得到的数字,我们可以选择一个合适的值。 - 例如,如果我们知道数字最多包含 4 个数字,我们可以使用初始值,比如
10000
。如果我们不知道这一点,我们可以将smallNum
设置为编译器定义的最大整数值(32767
表示 16 位整数)。同样,当我们寻找最大值时,我们可以将bigNum
(比如说)初始化为一个非常小的数,比如-32767
。 - 另一种可能是读取第一个数字,并将
smallNum
(或bigNum
)设置为该数字,前提是它不是0
。为了多样化,我们将举例说明这种方法。 - 当每个数字(
num
,比方说)被输入时,它与smallNum
进行比较;如果 num 比smallNum
小,那么我们有一个更小的数字,并且smallNum
被设置为这个新的数字。 - 当所有的数字都被输入和检查后,
smallNum
将包含最小的一个。
这些想法用下面的算法来表达:
get a number, num
if num is 0 then stop //do nothing and halt the program
set smallNum to num
while num is not 0 do
if num is smaller than smallNum, set smallNum to num
get a number, num
endwhile
print smallNum
第一个数字被读出。如果是0
,则什么都不做,程序暂停。如果它不是0
,我们将smallNum
设置为它。在这个阶段,我们可以在执行while
语句之前获得另一个数字。然而,为了简洁起见,我们没有。这样做的代价是,即使我们知道num
不是0
,也不小于smallNum
(是一样的),我们仍然在得到下一个数之前做这些测试。
该算法如程序 P5.5 所示实现。
Program P5.5
//find the smallest of a set of numbers entered
#include <stdio.h>
int main() {
int num;
printf("Enter a number (0 to end): ");
scanf("%d", &num);
if (num == 0) return; //halt the program
int smallNum = num;
while (num != 0) {
if (num < smallNum) smallNum = num;
printf("Enter a number (0 to end): ");
scanf("%d", &num);
}
printf("\nThe smallest is %d\n", smallNum);
}
在 C 语言中,关键字return
可以在main
中使用,通过“返回”到操作系统来暂停程序。我们将在第七章的中更详细地讨论返回。
运行时,如果按以下顺序输入数字:
36 17 43 52 50 0
该程序将打印
The smallest is 17
如果输入的数字是
36 -17 43 -52 50 0
该程序将打印
The smallest is -
52
5.8 从文件中读取数据
到目前为止,我们编写的程序都假设要提供的数据是在键盘上输入的。我们使用scanf
读取数字,使用gets
读取字符串来获取数据。通常,程序会提示用户输入数据,并等待用户键入数据。当数据被键入时,程序读取它,将它存储在一个(或多个)变量中,并继续执行。这种提供数据的模式被称为交互模式,因为用户是在与程序进行交互。
我们说我们一直在从“标准输入”中读取数据 c 使用预定义的标识符stdin
来引用标准输入。当你的程序启动时,C 假设stdin
指的是键盘。类似地,预定义的标识符stdout
指的是标准输出,屏幕。到目前为止,我们的程序已经把输出写到屏幕上了。
我们也可以通过将数据存储在文件中来为程序提供数据。当程序需要数据时,它直接从文件中获取数据,无需用户干预。当然,我们必须确保适当的数据以正确的顺序和格式存储在文件中。这种提供数据的模式通常被称为批处理模式。(术语“批处理”是历史性的,来源于数据在提交处理之前必须“批处理”的旧时代。)
例如,假设我们需要为几个商品提供一个商品编号(int
)和一个价格(double
)。如果编写程序时假设数据文件包含几对数字(一个int
常量后跟一个double
常量),那么我们必须确保文件中的数据符合这一点。
假设我们创建一个名为input.txt
的文件,并在其中键入数据。该文件是字符文件或文本文件。根据 C 编译器提供的编程环境,有可能将stdin
分配给input.txt
——我们说将标准输入重定向到input.txt
。一旦完成,你的程序将从文件中而不是键盘上读取数据。类似地,也可以将标准输出重定向到一个文件,比如说output.txt
。如果完成,你的printf
将把输出写到文件中,而不是屏幕上。
我们将采用一种稍微不同的方法,这种方法更加通用,因为它适用于任何 C 程序,并且不依赖于您正在使用的特定编译器或操作系统。
假设我们希望能够从文件input.txt
中读取数据。我们需要做的第一件事是声明一个称为“文件指针”的标识符这可以通过语句来完成。
FILE * in; // read as "file pointer in"
单词FILE
必须按图示拼写,全部大写字母。*
前后的空格可以省略。所以你可以写FILE* in
、FILE *in
甚至FILE*in
。我们使用了标识符in
;其他什么都行,比如inf
、infile
、inputFile
、payData
。
我们必须做的第二件事是将文件指针in
与文件input.txt
相关联,并告诉 C 我们将从文件中读取数据。这是使用功能fopen
完成的,如下所示:
in = fopen("input.txt", "r");
这告诉 C“打开文件input.txt
进行读取”:"r"
表示读取。(如果我们希望打开文件进行“写入”,即接收输出,我们将使用"w"
。)如果我们愿意,我们可以用一个语句完成这两件事,因此:
FILE * in = fopen("input.txt", "r");
一旦完成,这个“数据指针”将被定位在文件的开头。我们现在可以编写从文件中读取数据的语句。我们很快就会看到。
我们有责任确保文件存在并包含适当的数据。否则,我们将得到一条错误消息,如“文件未找到”如果需要,我们可以指定文件的路径。
假设文件位于C:\testdata\input.txt
。
我们可以告诉 C,我们将从这个文件中读取数据:
FILE * in = fopen("C:\\testdata\\input.txt", "r");
回想一下,转义序列\\
用于表示字符串中的\
。如果文件在带有指定字母E
的闪存盘上,我们可以使用:
FILE * in = fopen("E:\\input.txt", "r");
5.8.1 fscanf
我们使用语句(更准确地说是函数)fscanf
从文件中读取数据。除了第一个参数是文件指针in
之外,它的用法与scanf
完全相同。例如,如果num
是int
,则语句
fscanf(in, "%d", &num);
将从文件input.txt
(与in
关联的文件)中读取一个整数,并存储在num
中。注意,第一个参数是文件指针,而不是文件名。
当我们从文件中读取完数据后,我们应该关闭它。这是通过fclose
完成的,如下所示:
fclose(in);
有一个参数,文件指针(不是文件名)。该语句中断了文件指针in
与文件input.txt
的关联。如果需要的话,我们现在可以使用下面的代码将标识符in
与另一个文件(paydata.txt
)链接起来:
in = fopen("paydata.txt", "r");
注意,我们不重复声明的FILE *
部分,因为它已经被声明为FILE *
。后续的fscanf(in, ...)
语句将从文件paydata.txt
中读取数据。
5.8.2 查找文件中数字的平均值
为了说明fscanf
的用法,让我们重写程序 P5.3,从一个文件中读取几个数字,并求出它们的平均值。之前,我们讨论了如何求平均值。我们只需要做些修改来从文件中读取数字。假设文件名为input.txt
,包含几个正整数,0
表示结束,例如
24 13 55 32 19 0
程序 P5.6 显示了如何将文件定义为读取数据的地方,以及如何找到平均值。
Program P5.6
//read numbers from a file and find their average; 0 ends the data
#include <stdio.h>
int main() {
FILE * in = fopen("input.txt", "r");
int num, sum = 0, n = 0;
fscanf(in, "%d", &num);
while (num != 0) {
n = n + 1;
sum = sum + num;
fscanf(in, "%d", &num);
}
if (n == 0) printf("\nNo numbers supplied\n");
else {
if (n == 1) printf("\n1 number supplied\n");
else printf("\n%d numbers supplied\n", n);
printf("The sum is %d\n", sum);
printf("The average is %3.2f\n", (double) sum/n);
}
fclose(in);
}
对程序 P5.6 的评论
- 使用
FILE *
和fopen
以便fscanf
语句从文件input.txt
中获取数据。 - 因为数据是直接从文件中读取的,所以不会出现提示输入数据的问题。不再需要提示输入数据的
printf
语句。 - 该程序在试图寻找平均值之前确保
n
不是0
。 - 运行时,程序从文件中读取数据并打印结果,无需任何用户干预。
- 如果数据文件包含
24 13 55 32 19 0
,输出将是5 numbers were supplied
The sum is 143
The average is 28.60
- 文件中的数字可以以“自由格式”提供——任何数量都可以放在一行中。例如,样本数据可以在一行中键入,如上所示或如下:
24 13
55 32
19 0
或如下:24 13
55
32 19
0
- 或者像这样:
24
13
55
32
19
0
- 作为练习,将语句添加到程序中,以便它也打印文件中最大和最小的数字。
File cannot be found
当您尝试运行此程序时,它可能无法正常运行,因为它找不到文件input.txt
。这可能是因为编译器在错误的位置查找文件。一些编译器希望在与程序文件相同的文件夹/目录中找到该文件。其他人希望在与编译器相同的文件夹/目录中找到它。试着依次将input.txt
放在这些文件夹中,然后运行程序。如果这不起作用,那么您需要在fopen
语句中指定文件的完整路径。例如,如果文件位于文件夹 CS10E 中的文件夹 data 中,该文件夹位于 C:驱动器上,则需要使用以下语句:
FILE * in = fopen("C:\\CS10E\\data\\input.txt", "r");
5.9 将输出发送到文件
到目前为止,我们的程序已经从标准输入(键盘)读取数据,并将输出发送到标准输出(屏幕)。我们刚刚看到了如何从文件中读取数据。我们现在向您展示如何将输出发送到文件。
这很重要,因为当我们将输出发送到屏幕上时,当我们退出程序或关闭计算机时,输出就会丢失。如果我们需要保存我们的输出,我们必须把它写到一个文件中。那么只要我们希望保存文件,输出就可用。
这个过程类似于从文件中读取。我们必须声明一个“文件指针”(我们使用out
),并使用fopen
将它与实际文件关联起来。这可以通过
FILE * out = fopen("output.txt", "w");
这告诉 C“打开文件output.txt
进行写入”;"w"
表示书写。当这个语句被执行时,如果文件output.txt
不存在,它将被创建。如果它存在,它的内容就会被销毁。换句话说,无论您向文件中写入什么,都将替换其原始内容。请注意,不要打开要写入内容的文件。
5.9.1 fprintf
我们使用语句(更准确地说,是函数)fprintf
将输出发送到文件。除了第一个参数是文件指针out
之外,它的用法与printf
完全相同。例如,如果 sum 是值为143
的int
,则语句
fprintf(out, "The sum is %d\n", sum);
会写
The sum is 143
到文件output.txt
。
注意,第一个参数是文件指针,而不是文件名。
当我们将输出写入文件后,我们必须close
它。这对于输出文件尤其重要,因为按照某些编译器的操作方式,这是确保所有输出都发送到文件的唯一方法。(例如,它们将输出发送到内存中的临时缓冲区;只有当缓冲区已满时,它才会被发送到文件。如果不关闭文件,一些输出可能会留在缓冲区中,永远不会发送到文件中。)我们用fclose
关闭文件,如下:
fclose(out);
有一个参数,文件指针(不是文件名)。该语句中断了文件指针与文件output.txt
的关联。如果需要的话,我们现在可以使用以下方法将标识符out
与另一个文件(payroll.txt
)链接起来:
out = fopen("payroll.txt", "w");
注意,我们不重复声明的FILE *
部分,因为 out 已经被声明为FILE *
。后续的fprintf(out, ...)
语句会将输出发送到文件payroll.txt
。
例如,我们通过添加fopen
和fprintf
语句将程序 P5.6 重写为程序 P5.7。唯一的区别是 P5.6 将其输出发送到屏幕,而 P5.7 将其输出发送到文件output.txt
。
Program P5.7
//read numbers from a file and find their average; 0 ends the data
#include <stdio.h>
int main() {
FILE * in = fopen("input.txt", "r");
FILE * out = fopen("output.txt", "w");
int num, sum = 0, n = 0;
fscanf(in, "%d", &num);
while (num != 0) {
n = n + 1;
sum = sum + num;
fscanf(in, "%d", &num);
}
if (n == 0) fprintf(out, "No numbers entered\n");
else {
fprintf(out, "%d numbers were entered\n", n);
fprintf(out, "The sum is %d\n", sum);
fprintf(out, "The average is %3.2f\n", (double) sum/n);
}
fclose(in);
fclose(out);
}
如 5.8 节所述,如果你愿意,可以在fopen
语句中指定文件的完整路径。例如,如果您想将输出发送到闪存驱动器上的文件夹Results
(带有指定的字母F
,您可以使用
FILE * out = fopen("F:\\Results\\output.txt", "w");
当你运行程序 P5.7 时,看起来好像什么都没发生。但是,如果您使用您指定的文件路径检查您的文件系统,您将会找到文件output.txt
。打开它查看您的结果。
5.10 工资单
到目前为止,我们的程序已经从标准输入(键盘)读取数据,并将输出发送到标准输出(屏幕)。我们刚刚看到了如何从文件中读取数据。我们现在向您展示如何将输出发送到文件。
每个雇员的数据由名、姓、工作时数和工资率组成。数据将存储在文件paydata.txt
中,输出将发送到文件payroll.txt
。
为了向您展示读取字符串的另一种方式,我们将假设数据存储在文件中,如下所示:
Maggie May 50 12.00
Akira Kanda 40 15.00
Richard Singh 48 20.00
Jamie Barath 30 18.00
END
我们使用“名字”END
作为数据结束标记。
常规工资、加班工资和净工资将按照第 4.4.1 节所述进行计算。员工姓名、工作时间、工资率、正常工资、加班工资和净工资都打印在适当的标题下。此外,我们将编写程序来做以下事情:
- 统计处理了多少员工。
- 计算工资总额(所有员工的净工资总额)。
- 确定哪个员工的工资最高,有多少。我们将忽略平局的可能性。
对于示例数据,输出应该如下所示:
Name Hours Rate Regular Overtime Net
Maggie May 50.0 12.00 480.00 180.00 660.00
Akira Kanda 40.0 15.00 600.00 0.00 600.00
Richard Singh 48.0 20.00 800.00 240.00 1040.00
Jamie Barath 30.0 18.00 540.00 0.00 540.00
Number of employees: 4
Total wage bill: $2840.00
Richard Singh earned the most pay of $1040.00
用于读取数据的算法的概要如下:
read firstName
while firstName is not "END" do
read lastName, hours, rate
do the calculations
print results for this employee
read firstName
endwhile
我们将使用fscanf
中的规范%s
来读取名称。假设我们已经声明firstName
为
char firstName[20];
我们可以用语句将一个字符串读入firstName
fscanf(in, "%s", firstName);
规范%s
必须匹配一个字符数组,比如firstName
。如 3.4 节所述,当一个数组名是scanf
(或fscanf
)的参数时,我们不能在它前面写&
。
%s
用于读取不包含任何空白字符的字符串。从下一个非空白字符开始,字符存储在firstName
中,直到遇到下一个空白字符。由我们来确保数组足够大以容纳字符串。
因为空白字符结束了字符串的读取,所以%s
不能用于读取包含空白的字符串。出于这个原因,我们将对名字(firstName
)和姓氏(lastName
)使用单独的变量。
例如,假设下一段数据包含(◊表示空格):
◇◆T0◆T1◆T1
声明
fscanf(in, "%s", firstName);
将跳过空格,直到到达第一个非空白字符R
。从R
开始,它将字符存储在firstName
中,直到到达下一个空格,即n
之后的空格。读取停止,Robin
存储在firstName
中。数据指针位于n
后的空格处。如果我们现在执行
fscanf(in, "%s", lastName);
fscanf
将跳过空格,直到到达H
。从H
开始,它将字符存储在lastName
中,直到到达d
后的空格。读数停止,Hood
被存储在lastName
中。如果d
是该行的最后一个字符,行尾字符(空白)将会停止阅读。
由于%s
的工作方式,我们需要分别读出名字和姓氏。然而,为了让输出整齐地排列起来,如前一页所示,将整个名称存储在一个变量中会更方便一些(name
)。假设Robin
存储在firstName
中,Hood
存储在lastName
中。我们将把firstName
复制到name
,用
strcpy(name, firstName);
然后,我们将添加一个空格
strcat(name, " ");
strcat
是一个预定义的字符串函数,允许我们连接两个字符串。它代表“字符串连接”。如果s1
和s2
是字符串,strcat(s1, s2)
会在s1
的末尾加上s2
。它假设s1
足够大,可以容纳连接的字符串。
然后我们将在lastName
后面加上
strcat(name, lastName);
以我们的例子为例,在这一切结束时,name
将包含Robin Hood
。
在我们的程序中,我们将使用规范%-15s
来打印name
。这将在字段宽度15
中左对齐打印name
。换句话说,所有的名字都将使用15
打印列进行打印。这对于输出整齐排列是必要的。为了适应较长的名称,您可以增加字段宽度。
要使用字符串函数,我们必须编写指令
#include <string.h>
如果我们想使用 c 语言提供的字符串函数。
我们的程序需要检查firstName
中的值是否是字符串"END"
。理想情况下,我们会这样说
while (firstName != "END") { //cannot write this in C
但是我们不能这样做,因为 C 不允许我们使用关系运算符来比较字符串。我们能做的就是使用预定义的字符串函数strcmp
(字符串比较)。
如果s1
和s2
是字符串,表达式strcmp(s1, s2)
返回以下值:
0
如果s1
与s2
相同- 如果
s1
小于s2
,则< 0(按字母顺序) -
0
如果s1
大于s2
(按字母顺序)
例如,
strcmp("hello", "hi") is < 0
strcmp("hi","hello") is > 0
strcmp("allo","allo") is 0
使用strcmp
,我们可以将while
条件写成
while (strcmp(firstName, "END") != 0)
如果strcmp(firstName, "END")
不是0
,说明firstName
不包含END
这个词所以我们还没有到达数据的末尾;进入while
循环来处理该雇员。
当面对一个需要做很多事情的程序时,最好从解决问题的一部分开始,把它做好,然后解决其他部分。对于这个问题,我们可以从让程序读取和处理数据开始,不需要计数,找到总数或者找到工资最高的员工。
程序 P5.8 基于程序 P4.7(第 4.6.2 节)。
Program P5.8
#include <stdio.h>
#include <string.h>
#define MaxRegularHours 40
#define OvertimeFactor 1.5
int main() {
FILE * in = fopen("paydata.txt", "r");
FILE * out = fopen("payroll.txt", "w");
char firstName[20], lastName[20], name[40];
double hours, rate, regPay, ovtPay, netPay;
fprintf(out,"Name Hours Rate Regular Overtime Net\n\n");
fscanf(in, "%s", firstName);
while (strcmp(firstName, "END") != 0) {
fscanf(in, "%s %lf %lf", lastName, &hours, &rate);
if (hours <= MaxRegularHours) {
regPay = hours * rate;
ovtPay = 0;
}
else {
regPay = MaxRegularHours * rate;
ovtPay = (hours - MaxRegularHours) * rate * OvertimeFactor;
}
netPay = regPay + ovtPay;
//make one name out of firstName and lastName
strcpy(name,firstName); strcat(name," "); strcat(name,lastName);
fprintf(out, "%-15s %5.1f %6.2f", name, hours, rate);
fprintf(out, "%9.2f %9.2f %7.2f\n", regPay, ovtPay, netPay);
fscanf(in, "%s", firstName);
}
fclose(in);
fclose(out);
}
对程序 P5.8 的评论
- 我们使用“文件指针”
in
和out
从paydata.txt
读取数据并将输出发送到payroll.txt
。 - 由于数据是从文件中读取的,因此不需要提示。
- 我们使用
fscanf
读取数据,使用fprintf
写入输出。 - 我们使用
fclose
来关闭文件。 - 我们打印一个带有以下语句的标题:
fprintf(out,"Name Hours Rate Regular Overtime Net\n\n");
- 为了让输出很好地对齐,您需要调整打印结果的语句中单词之间的空格和字段宽度。例如,
e
和H
之间有 12 个空格,s
和R
之间有 3 个空格,e
和R
之间有 2 个空格,r
和O
之间有 2 个空格,e
和N
之间有 5 个空格。 - 您应该试验一下
fprintf
语句(写一行输出)中的字段宽度,看看它对您的输出有什么影响。 - 我们使用一个
while
循环来处理几个雇员。当“名字”END
被读取时,程序知道它已经到达数据的末尾。它关闭文件并停止。
既然我们已经得到了基本的处理,我们可以添加语句来执行其他任务。程序 P5.9 是一个完整的程序,它计算雇员人数,计算工资总额,并确定工资最高的雇员。
计算雇员人数和工资总额相当简单。我们使用变量numEmp
和wageBill,
,它们在循环之前被初始化为0
。它们在循环中递增,并在循环后打印出它们的最终值。如果你理解代码有困难,你需要重读 5.1 和 5.2 节。我们用numEmp++
将1
加到numEmp
,用wageBill += netPay
将netPay
加到wageBill
。
变量mostPay
保存了所有员工获得的最多工资。它被初始化为0
。每次我们计算当前员工的netPay
时,我们都将其与mostPay
进行比较。如果更大,我们将mostPay
设置为新的金额,并将员工的姓名(name
)保存在bestPaid
中。
Program P5.9
#include <stdio.h>
#include <string.h>
#define MaxRegularHours 40
#define OvertimeFactor 1.5
int main() {
FILE * in = fopen("paydata.txt", "r");
FILE * out = fopen("payroll.txt", "w");
char firstName[20], lastName[20], name[40], bestPaid[40];
double hours, rate, regPay, ovtPay, netPay;
double wageBill = 0, mostPay = 0;
int numEmp = 0;
fprintf(out,"Name Hours Rate Regular Overtime Net\n\n");
fscanf(in, "%s", firstName);
while (strcmp(firstName, "END") != 0) {
numEmp++;
fscanf(in, "%s %lf %lf", lastName, &hours, &rate);
if (hours <= MaxRegularHours) {
regPay = hours * rate;
ovtPay = 0;
}
else {
regPay = MaxRegularHours * rate;
ovtPay = (hours - MaxRegularHours) * rate * OvertimeFactor;
}
netPay = regPay + ovtPay;
//make one name out of firstName and lastName
strcpy(name,firstName); strcat(name," "); strcat(name,lastName);
fprintf(out, "%-15s %5.1f %6.2f", name, hours, rate);
fprintf(out, "%9.2f %9.2f %7.2f\n", regPay, ovtPay, netPay);
if (netPay > mostPay) {
mostPay = netPay;
strcpy(bestPaid, name);
}
wageBill += netPay;
fscanf(in, "%s", firstName);
} //end while
fprintf(out, "\nNumber of employees: %d\n", numEmp);
fprintf(out, "Total wage bill: $%3.2f\n", wageBill);
fprintf(out,"%s earned the most pay of $%3.2f\n",bestPaid, mostPay);
fclose(in); fclose(out);
}
5.11 for 结构
在第三章、第四章和第五章中,我们向你展示了三种可用于编写程序的逻辑——顺序、选择和重复。信不信由你,有了这三个,你就有了表达任何程序逻辑所需的所有逻辑控制结构。事实证明,这三种结构是你制定逻辑来解决任何可以在计算机上解决的问题所需要的。
由此可见,你所需要的是if
和while
语句来编写任何程序的逻辑。然而,许多编程语言提供了额外的语句,因为它们允许您比使用if
和while
更方便地表达某些类型的逻辑。for
声明就是一个很好的例子。
while
让您在某些条件为真时重复语句,for
让您重复语句指定的次数(比如 25 次)。考虑下面的for
构造的伪代码示例(通常称为for
循环):
for h = 1 to 5 do
print "I must not sleep in class"
endfor
这表示执行print
语句5
次,其中h
假设值为 1、2、3、4 和 5,每个5
次一个值。效果是打印以下内容:
I must not sleep in class
I must not sleep in class
I must not sleep in class
I must not sleep in class
I must not sleep in class
该结构包括:
for
这个词- 循环变量(示例中的
h
) =
- 初始值(示例中的
1
) to
这个词- 最终值(示例中的
5
) do
这个词- 每次通过循环要执行的一条或多条语句;这些语句构成了循环的主体
- 单词
endfor
,表示构造的结束
我们强调endfor
不是一个 C 字,没有出现在任何 C 程序中。它只是程序员在编写伪代码时使用的一个方便的词,用来表示一个for
循环的结束。
为了突出循环的结构,使其更具可读性,我们将for
和endfor
排成一行,并在主体中缩进语句。
在for
和do
之间的部分被称为循环的控制部分。这是决定身体被执行多少次的因素。在这个例子中,控制部分是h = 1 to 5
。其工作原理如下:
h
被设置为1
并执行主体(print
h
被设置为2
并执行主体(print
h
被设置为3
并执行主体(print
h
被设置为4
并执行主体(print
h
被设置为5
并执行主体(print
最终结果是,在这种情况下,主体执行了5
次。
一般来说,如果控制部分是h = first to last
,则执行如下:
- 如果
first > last
,身体根本不执行;在endfor
之后,继续执行语句(如果有的话);否则 h
被设置为first
并执行主体1
加到h
;如果h
的值小于或等于last
,则再次执行主体1
加到h
;如果h
的值小于或等于last
,则再次执行主体- 等等
当h
的值达到last
时,最后一次执行主体,控制转到endfor
之后的语句(如果有的话)。
实际效果是,对于在first
和last
之间的h
的每个值,执行主体。
5 . 11 . 1 C 中的 for 语句
伪代码结构
for h = 1 to 5 do
print "I must not sleep in class"
endfor
在 C 中实现如下:
for (h = 1; h <= 5; h++)
printf("I must not sleep in class\n");
假设h
声明为int
。然而,更常见的是在for
语句本身中声明h
,就像这样:
for (int h = 1; h <= 5; h++)
printf("I must not sleep in class\n");
不过,在这种情况下,请注意h
的范围只扩展到了for
的主体(见下一节)。
Caution
在早期版本的 c 语言中,不允许在for
语句中声明循环变量。如果您使用的是较旧的编译器,将会出现错误。在这种情况下,只需在for
语句之前声明循环变量。
在 C 中,for
的主体必须是一条语句或一个块。在本例中,它是单个printf
语句。如果它是一个块,我们将把它写成如下形式:
for (int h = 1; h <= 5; h++) {
<statement1>
<statement2>
etc.
}
当我们在for
语句中声明循环变量(示例中的h
)时,h
仅在块内“已知”(可以使用)。如果我们试图在块后使用h
,我们将得到一个“未声明变量”的错误消息。
程序 P5.10 说明了如何使用for
语句打印以下五次:
I must not sleep in class
正如您可能会想到的,如果您想打印100
行,比方说,您所要做的就是将for
语句中的5
改为100
。
Program P5.10
#include
int main() {
int h;
for (h = 1; h <= 5; h++)
printf("I must not sleep in class\n");
}
C # 中 for 语句的一般形式是
for (<expr1>; <expr2>; <expr3>)
<statement>
单词for
、括号和分号是必需的。你必须提供<expr1>
、<expr2>
、<expr3>
和<statement>
。
具体来说,for
语句包括
for
这个词- 一个左括号,
(
<expr1>
,称为初始化步骤;这是执行 for 时执行的第一步。- 分号,;
<expr2>
,控制是否执行<statement>
的条件。- 分号,;
<expr3>
,称为重新初始化步骤- 一个右括号,
)
<statement>
,称为循环体。这可以是一个简单的语句,也可以是一个块。
当遇到一个for
语句时,它按如下方式执行:
<expr1>
is evaluated. <expr2>
is evaluated. If it is false
, execution continues with the statement, if any, after <statement>
. If it is true
, <statement>
is executed, followed by <expr3>
, and this step (2) is repeated.
这可以更简明地表达如下:
<expr1>;
while (<expr2>) {
<statement>;
<expr3>;
}
请考虑以下几点:
for (h = 1; h <= 5; h++)
printf("I must not sleep in class\n");
h = 1
是<expr1>
h <= 5
是<expr2>
h++
是<expr3>
<statement>
是printf(...);
该代码执行如下:
h
is set to 1
The test h <=
5 is performed. It is true, so the body of the loop is executed (one line is printed). The reinitialization step h++
is then performed, so h
is now 2
. The test h <= 5
is again performed. It is true, so the body of the loop is executed (a second line is printed); h++
is performed, so h
is now 3
. The test h <= 5
is again performed. It is true, so the body of the loop is executed (a third line is printed); h++
is performed, so h
is now 4
. The test h <= 5
is again performed. It is true, so the body of the loop is executed (a fourth line is printed); h++
is performed, so h
is now 5
. The test h <= 5
is again performed. It is true, so the body of the loop is executed (a fifth line is printed); h++
is performed, so h
is now 6
. The test h <= 5
is again performed. It is now false, so execution of the for
loop ends and the program continues with the statement, if any, after printf(...)
.
从for
循环退出时,h
(本例中为6
)的值可用,如果需要,可由程序员使用。但是,请注意,如果在for
语句中声明了h
,那么它在循环之外将不可用。
如果我们需要一个循环来倒计数(比如从5
到1
,我们可以写
for (int h = 5; h >= 1; h--)
执行循环体时,h
取值为5
、4
、3
、2
和1
。
除了 1,我们还可以向上(或向下)计数。例如,语句
for (int h = 10; h <= 20; h += 3)
将执行带有值10
、13
、16
和19
的h
的主体。和声明
for (int h = 100; h >= 50; h -= 10)
将执行带有值100
、90
、80
、70
、60
和50
的h
的身体。
一般来说,我们可以使用任何我们需要的表达方式来达到我们想要的效果。
在程序 P5.10 中,h
在循环内取值为1
、2
、3
、4
和5
。我们还没有在身体中使用h
,但是如果需要的话,它是可用的。我们展示了程序 P5.11 中的一个简单用法,其中我们通过打印h
的值来给行编号。
Program P5.11
#include <stdio.h>
int main() {
for (int h = 1; h <= 5; h++)
printf("%d. I must not sleep in class\n", h);
}
运行时,该程序将打印以下内容:
1\. I must not sleep in class
2\. I must not sleep in class
3\. I must not sleep in class
4\. I must not sleep in class
5\. I must not sleep in class
for
语句中的初始值和最终值不必是常量;它们可以是变量或表达式。例如,考虑以下情况:
for (h = 1; h <= n; h++) ...
这个循环的主体会被执行多少次?我们不能说,除非我们知道遇到这个语句时n
的值。如果n
的值为7
,那么主体将被执行7
次。
这意味着在计算机执行for
语句之前,n
必须已经被赋予了某个值,这个值决定了循环执行的次数。如果没有给n
赋值,那么for
语句就没有意义,程序就会崩溃(或者最多给出一些无意义的输出)。
举例来说,我们可以修改程序 P5.11 来询问用户她想要打印多少行。输入的数字用于控制循环执行的次数,从而控制打印的行数。
这些变化显示在程序 P5.12 中。
Program P5.12
#include <stdio.h>
int main() {
int n;
printf("How many lines to print? ");
scanf("%d", &n);
printf("\n"); //print a blank line
for (int h = 1; h <= n; h++)
printf("%d. I must not sleep in class\n", h);
}
下面显示了一个运行示例。我们将很快展示如何整理输出。
How many lines to print?
12
1\. I must not sleep in class
2\. I must not sleep in class
3\. I must not sleep in class
4\. I must not sleep in class
5\. I must not sleep in class
6\. I must not sleep in class
7\. I must not sleep in class
8\. I must not sleep in class
9\. I must not sleep in class
10\. I must not sleep in class
11\. I must not sleep in class
12\. I must not sleep in class
请注意,我们不(也不可能)预先知道用户将键入什么数字。然而,这不是问题。我们只是将数字存储在一个变量中(使用了n
),并使用n
作为for
语句中的“最终值”。因此,用户键入的数字将决定主体被执行的次数。
现在,用户只需根据提示输入所需的值,就可以更改打印的行数。程序中不需要任何改变。程序 P5.12 比 P5.11 灵活得多。
一点美学
在上面的运行中,虽然输出是正确的,但是数字没有很好地对齐,结果是I
没有正确对齐。在打印h
时,我们可以通过使用字段宽度来对齐。举这个例子,2
就可以了。但是,如果数量可能达到数百,我们必须至少使用3
,对于数千,至少使用4
,以此类推。
在程序 P5.12 中,如果我们将printf
语句改为:
printf("%2d. I must not sleep in class\n", h);
下面的输出看起来更整洁,将被打印出来:
How many lines to print?
12
1\. I must not sleep in class
2\. I must not sleep in class
3\. I must not sleep in class
4\. I must not sleep in class
5\. I must not sleep in class
6\. I must not sleep in class
7\. I must not sleep in class
8\. I must not sleep in class
9\. I must not sleep in class
10\. I must not sleep in class
11\. I must not sleep in4
class
12\. I must not sleep in class
5.12 乘法表
对于生成乘法表来说,for
语句非常方便。举例来说,让我们写一个程序来产生一个从 1 到 12 的“2 倍”表。程序应该打印以下内容:
1 x 2 = 2
2 x 2 = 4
3 x 2 = 6
4 x 2 = 8
5 x 2 = 10
6 x 2 = 12
7 x 2 = 14
8 x 2 = 16
9 x 2 = 18
10 x 2 = 20
11 x 2 = 22
12 x 2 = 24
查看输出可以发现,每一行都由三部分组成:
A number on the left that increases by 1
for each new line. A fixed part " x 2 = "
(note the spaces) that is the same for each line. A number on the right; this is derived by multiplying the number on the left by 2
.
我们可以使用以下语句生成左边的数字:
for (int m = 1; m <= 12; m++)
然后我们每次通过循环打印m
。我们可以通过将m
乘以2
得出右边的数字。
程序 P5.13 展示了如何编写它。运行时,它将生成上表。
Program P5.13
#include <stdio.h>
int main() {
for (int m = 1; m <= 12; m++)
printf("%2d x 2 = %2d\n", m, m * 2);
}
注意使用字段宽度2
(在%2d
中)打印m
和m * 2
。这是为了确保数字如输出所示排列。如果没有字段宽度,表格看起来会不整洁—试试看吧。
如果我们想打印一个“7 倍”的表呢?需要什么样的改变?我们只需要将printf
语句改为
printf("%2d x 7 = %2d\n", m, m * 7);
类似地,如果我们想要一个“9 倍”的表,我们必须将7
改为9
,并且我们必须为我们想要的每个表不断地改变程序。
更好的方法是让用户告诉计算机他想要哪张桌子。然后,程序将使用这些信息生成所需的表格。现在当程序运行时,它会提示:
Enter type of table:
如果用户想要一个“7 倍”的表,他会输入7
。该计划将继续进行,并产生一个“7 倍”表。程序 P5.14 显示了如何操作。
Program P5.14
#include <stdio.h>
int main() {
int factor;
printf("Type of table? ");
scanf("%d", &factor);
for (int m = 1; m <= 12; m++)
printf("%2d x %d = %2d\n", m, factor, m * factor);
}
因为我们事先不知道需要什么类型的表,所以我们不能在格式字符串中使用7
,因为用户可能想要一个“9 倍”的表。我们必须打印包含表格类型的可变因子。
以下是运行示例:
Type of table?
7
1 x 7 = 7
2 x 7 = 14
3 x 7 = 21
4 x 7 = 28
5 x 7 = 35
6 x 7 = 42
7 x 7 = 49
8 x 7 = 56
9 x 7 = 63
10 x 7 =
70
11 x 7 = 77
12 x 7 = 84
我们现在有一个程序可以生成从1
到12
的任何乘法表。但是从1
到12
的范围并没有什么神圣的(也许很特别,因为这是我们在学校学过的)。我们如何将程序一般化以产生任何范围内的任何表格?我们必须让用户告诉程序他想要什么类型的表和什么范围。在程序中,我们需要用变量代替数字1
和12
(比如说start
和finish
)。
所有这些变化都反映在计划 P5.15 中。
Program P5.15
#include <stdio.h>
int main() {
int factor, start, finish;
printf("Type of table? ");
scanf("%d", &factor);
printf("From? ");
scanf("%d", &start);
printf("To? ");
scanf("%d", &finish);
printf("\n");
for (int m = start; m <= finish; m++)
printf("%2d x %d = %2d\n", m, factor, m * factor);
}
下面的示例运行显示了如何从10
到16
生成一个“6 次”表。
Type of table?
6
From?
10
To?
16
10 x 6 = 60
11 x 6 = 66
12 x 6 = 72
13 x 6 = 78
14 x 6 = 84
15 x 6 = 90
16 x 6 = 96
为了迎合更大的数字,如果我们希望数字整齐排列,我们需要增加printf
语句中2
的字段宽度。
对程序 P5.15 的评论
程序假设start
小于或等于finish
。如果不是这样呢?例如,假设用户为start
输入20
,为finish
输入15
。for
语句变成
for (int m = 20; m <= 15; m++)
m
设置为20
;因为这个值立即大于最终值15
,所以主体根本不执行,程序结束时不打印任何东西。
为了迎合这种可能性,我们可以让程序验证start
和finish
的值,以确保“From”值小于或等于“To”值。一种方法是编写以下内容:
if (start > finish)
printf("Invalid data: From value is bigger than To value\n");
else {
printf("\n");
for (int m = start; m <= finish; m++)
printf("%2d x %d = %2d\n", m, factor, m * factor);
}
验证输入的数据是防御性编程的另一个例子。此外,最好打印一条消息通知用户这个错误,而不是让程序什么也不做。这使得程序更加用户友好。
这里的另一个选择是不将较大的start
值视为错误,而是简单地以相反的顺序打印表格,从最大到最小。还有一种可能性是交换start
和finish
的值,并以正常方式打印表格。这些变化是作为练习留下的。
5.13 温度转换表
有些国家用摄氏温标来测量温度,而有些国家用华氏温标。假设我们想打印一个摄氏温度到华氏温度的转换表。工作台以10
的步长从0
摄氏度运行到100
摄氏度,因此:
Celsius Fahrenheit
0 32
10 50
20 68
30 86
40 104
50 122
60 140
70 158
80 176
90 194
100 212
对于摄氏温度 C 来说,华氏温度相当于 32 + 9C/5。
如果我们用c
来保持摄氏温度,我们可以写一个for
语句让c
取值0
、10
、20
,...,直到100
,带有
for (c = 0; c <= 100; c += 10)
每次执行循环时,c
增加10
。利用这一点,我们编写程序 P5.16 来生成表格。
Program P5.16
#include <stdio.h>
int main() {
double c, f;
printf("Celsius Fahrenheit\n\n");
for (c = 0; c <= 100; c += 10) {
f = 32 + 9 * c / 5;
printf("%5.0f %9.0f\n", c, f);
}
}
程序中一个有趣的部分是printf
语句。为了让温度在标题下居中,我们需要做一些计算。考虑标题
Celsius Fahrenheit
第 1 列中的C
和第 2 列中的s
和F
之间的空格。
假设我们希望摄氏温度排列在i
下,华氏温度排列在n
下(见上面的输出)。
通过计数,我们发现i
在第 5 列,n
在第 15 列。
由此我们可以得出,c
的值必须打印在宽度为 5 的字段中(前 5 列),而f
的值必须打印在接下来的 10 列中。我们对f
使用字段宽度 9,因为在printf(...)
中的f
和%
之间已经有一个空格。
我们使用0
作为格式规范中的小数位数,打印出不带小数点的c
和f
。如果任何温度不是一个整数,0
规格会将其四舍五入为最接近的整数,如下表所示。
Celsius Fahrenheit
20 68
22 72
24 75
26 79
28 82
30 86
32 90
34 93
36 97
38 100
40 104
作为练习,重写程序 P5.16,使其请求三个值用于start
、finish
和incr
,并生成一个转换表,其中摄氏温度以incr
为步长从start
到finish
。按照上一节的思路制作乘法表。例如,如果start
是20
, finish
是40
并且incr
是2
,程序将生成下表(华氏温度四舍五入为最接近的整数):
作为另一个练习,编写一个程序,生成一个从华氏温度到摄氏温度的表格。对于华氏温度F
,摄氏当量是5(F - 32)/
9
。
5.14】的表现力
在 C 语言中,for
语句不仅仅可以用来计算循环执行的次数。这是可能的,因为< expr1
>、< expr2
>、< expr3
>可以是任何表达式;他们甚至不需要有任何关系。因此,例如,< expr1
>可以是h = 1
,< expr2
>可以测试a
是否等于b
,< expr3
>可以是k++
或程序员期望的任何其他表达式。以下是完全有效的:
for (h = 1; a == b; k++)
也可以省略<expr1
>、< expr2
>或< expr3
>中的任何一个。但是,分号必须包括在内。因此,要省略< expr3
>,可以这样写
for (<expr1>; <expr2>; ) <statement>
在这种情况下,
<expr1
> is evaluated; then <expr2
> is evaluated. If it is false, execution continues after <statement
>. If it is true, <statement
> is executed and this step (2) is repeated.
这相当于
<expr1>;
while (<expr2>) <statement>
此外,如果我们省略<expr1
>,我们将有
for ( ; expr2 ; ) <statement> // note the semicolons
现在,<expr2
>被求值。如果为假,则在< statement
>之后继续执行。如果为真,则执行< statement
>,接着执行另一个< expr2
>的求值,以此类推。最终效果是,只要< expr2
>为真,就会执行<statement
>——这与通过
while (<expr2>) <statement>
大多数时候,<expr1
>会初始化一些变量,< expr2
>会测试它,< expr3
>会改变它。但是更多是可能的。例如,以下是有效的:
for (lo = 1, hi = n; lo <= hi; lo++, hi--) <statement>
这里,<expr1
>由逗号分隔的两个赋值语句组成;< expr3
>由逗号分隔的两个表达式组成。当两个变量相关并且我们想要突出这种关系时,这是非常有用的。在这种情况下,这种关系被捕捉到一个地方,即for
语句。我们可以很容易地看到变量是如何初始化的,以及它们是如何被改变的。
这个特性在处理数组时非常方便。我们将在第八章中看到例子。现在,我们把打印所有整数对的例子留给你,这些整数加起来就是一个给定的整数,n
。
代码是:
int lo, hi;
//assume n has been assigned a value
for (lo = 1, hi = n - 1; lo <= hi; lo++, hi--)
printf("%2d %2d\n", lo, hi);
如果n
是10
,该代码将打印以下内容:
1 9
2 8
3 7
4 6
5 5
变量lo
和hi
被初始化为第一对。打印一对后,lo
增加1
,而hi
减少1
,以获得下一对。当lo
通过hi
时,所有对都已打印完毕。
5.15do...while
声明
我们已经看到,while
语句允许一个循环执行零次或多次。然而,在某些情况下,让循环至少执行一次是很方便的。例如,假设我们希望提示输入一个代表一周中某一天的数字,1 代表星期日,2 代表星期一,依此类推。我们还希望确保输入的数字是有效的,并且在 1 到 7 的范围内。在这种情况下,必须至少输入一个数字。如果它是有效的,我们继续前进;否则,我们必须再次提示输入另一个号码,只要输入的号码无效,我们就必须这样做。我们可以使用如下的while
语句来表达这个逻辑:
printf("Enter a day of the week (1-7): ");
scanf("%d", &day); //assume int day
while (day < 1 || day > 7) { //as long as day is invalid
printf("Enter a day of the week (1-7): ");
scanf("%d", &day);
}
虽然这是可行的,但是我们可以使用一个do...while
语句来更简洁地表达它。
C 语言中do...while
语句的一般形式是
do
<statement>
while (<expression>);
单词do
和while
、括号和分号是必需的。你必须供应< statement
>和< expression
>。
当遇到do...while
时,
<statement
> is executed <expression
> is then evaluated; if it is true (non-zero), repeat from step 1. If it is false (zero), execution continues with the statement, if any, after the semicolon.
只要<expression
>为真,statement
>就会被执行。值得注意的是,由于构造的编写方式,< statement
>总是至少执行一次。
使用do...while
,我们可以将上面的逻辑表达如下:
do {
printf("Enter a day of the week (1-7): ");
scanf("%d", &day);
} while (day < 1 || day > 7); //as long as day is invalid
注意这看起来有多整洁。这里,<statement
>是由{
和}
划定的区块,< expression
>是day < 1 || day > 7
。
我们用另外两个例子来说明do...while
的用法。
最高公因数
之前我们写过程序 P5.2,用欧几里德的算法求两个整数的 HCF。我们现在使用do...while
重写程序,以确保输入的两个数字确实是正整数。我们还使用do...while
代替while
对算法进行了重新编码。这显示为程序 P5.17。
Program P5.17
#include <stdio.h>
int main() {
int m, n, r;
do {
printf("Enter two positive integers: ");
scanf("%d %d", &m, &n);
} while (m <= 0 || n <= 0);
// At this point, both m and n are positive
printf("\nThe HCF of %d and %d is ", m, n);
do {
r = m % n;
m = n;
n = r;
} while (n > 0);
printf("%d\n", m);
}
程序 P5.17 要求两个正值。第一个do...while
一直问,直到输入两个正值。当这种情况发生时,程序继续计算 HCF。从第二个do...while
退出时,n
的值为0
,m
的值为 HCF。以下是程序 P5.17 的运行示例:
Enter two positive integers:
84 -7
Enter two positive integers:
46 0
Enter two positive integers:
200 16
The HCF of 200 and 16 is 8
银行利息
考虑以下问题:
一个人在银行存了 1000 美元,年利率是 10%。在每年年底,获得的利息被加到存款金额中,成为下一年的新存款。编写一个程序来确定累计金额首次超过 2000 美元的年份。对于每一年,打印年初的存款和当年的利息,直到达到目标。
程序 P5.18 给出了这个问题的解决方案。
Program P5.18
#include <stdio.h>
int main() {
int year;
double initialDeposit, interestRate, target, deposit, interest;
printf("Initial deposit? ");
scanf("%lf", &initialDeposit);
printf("Rate of interest? ");
scanf("%lf", &interestRate);
printf("Target deposit? ");
scanf("%lf", &target);
printf("\nYear Deposit Interest\n\n");
deposit = initialDeposit;
year = 0;
do {
year++;
interest = deposit * interestRate / 100;
printf("%3d %8.2f %8.2f\n", year, deposit, interest);
deposit += interest;
} while (deposit <= target);
printf("\nDeposit exceeds $%7.2f at the end of year %d\n", target, year);
}
该程序使用以下变量:
| `initialDeposit` | `1000, in the example` | | `interestRate` | `10, in the example` | | `target` | `2000, in the example` | | `deposit` | `the deposit at any given time` |只要年末存款没有超额完成目标,我们就再计算一年的利息。程序 P5.18 不考虑初始存款大于目标值的情况。如果需要,可以使用while
语句(练习!).以下是 P5.18 的运行示例:
Initial deposit? 1000
Rate of interest? 10
Target deposit? 2000
Year Deposit Interest
1 1000.00 100.00
2 1100.00 110.00
3 1210.00 121.00
4 1331.00 133.10
5 1464.10 146.41
6 1610.51 161.05
7 1771.56 177.16
8 1948.72 194.87
Deposit exceeds $2000.00 at the end of year 8
EXERCISES 5What is an end-of-data marker? Give the other names for it. Write a program to read data for several items from a file. For each item, the price and a discount percent is given. Choose an appropriate end-of-data marker. For each item, print the original price, the discount amount, and the amount the customer must pay. At the end, print the number of items and the total amount the customer must pay. An auto repair shop charges as follows. Inspecting the vehicle costs $75. If no work needs to be done, there is no further charge. Otherwise, the charge is $75 per hour for labor plus the cost of parts, with a minimum charge of $120. If any work is done, there is no charge for inspecting the vehicle. Write a program to read several sets of hours worked and cost of parts and, for each, print the charge for the job. Choose an appropriate end-of-data marker. (You cannot choose 0 since either hours or parts could be 0.) At the end, print the total charge for all jobs. Write a program to calculate electricity charges for several customers. The data for each customer consists of a name, the previous meter reading, and the current meter reading. The difference in the two readings gives the number of units of electricity used. The customer pays a fixed charge of $25 plus 20 cents for each unit used. The data is stored in a file. Assume that the fixed charge and the rate per unit are the same for all customers and are given on the first line. This is followed by the data for the customers. Each set of data consists of two lines: a name on the first line and the meter readings on the second line. The “name” xxxx
ends the data. Print the information for the customers under a suitable heading. Also,
- 计算处理了多少客户
- 打印应付给电力公司的总额
- 找到账单最高的客户
A file contains data for several persons. The data for each person consists of their gross salary, deductions allowed and rate of tax (e.g., 25, meaning 25%). Tax is calculated by applying the rate of tax to (gross salary minus deductions). Net pay is calculated by gross salary minus tax. Under an appropriate heading, print the gross salary, tax deducted, net pay, and the percentage of the gross salary that was paid in tax. For each person, the data consists of two lines: a name on the first line and gross salary, deductions allowed and rate of tax on the second line. The “name” xxxx
ends the data. Also,
- 计算处理了多少人
- 打印薪金总额、扣除的税款和净工资总额
- 找出净工资最高的人
Write a program that reads several lengths in inches and, for each, converts it to yards, feet and inches. (1 yard = 3 feet, 1 foot = 12 inches). For example, if a length is 100
, the program should print 2 yd 2 ft 4 in
. Choose an appropriate end-of-data marker. Each line of data in a file consists of two lengths. Each length is given as two numbers representing feet and inches. A line consisting of 0 0 only ends the data. For each pair of lengths, print their sum. For example, if the lengths are 5 ft. 4 in.
and 8 ft. 11 in.
, your program should print 14 ft. 3 in.
The line of data for this example would be given as 5 4 8 11
You are given a file containing an unknown amount of numbers. Each number is one of the numbers 1 to 9. A number can appear zero or more times and can appear anywhere in the file. The number 0
indicates the end of the data. Some sample data are: 5 3 7 7 7 4 3 3 2 2 2 6 7 4 7 7
2 2 9 6 6 6 6 6 8 5 5 3 7 9 9 9 0
Write a program to read the data once and print the number that appears the most in consecutive positions and the number of times it appears. Ignore the possibility of a tie. For the above data, output should be 6 5
. A contest was held for the promotion of SuperMarbles. Each contestant was required to guess the number of marbles in a jar. Write a program to determine the Grand Prize winner (ignoring the possibility of a tie) based on the following: The first line of data contains a single integer (answer
, say) representing the actual number of marbles in the jar. Each subsequent line contains a contestant’s ID number (an integer), and an integer representing that contestant’s guess. The data is terminated by a line containing 0 only. The Grand Prize winner is that contestant who guesses closest to answer
without exceeding it. There is no winner if all guesses are too big. Assume all data are valid. Print the number of contestants and the ID number of the winner, if any. The manager of a hotel wants to calculate the cost of carpeting the rooms in the hotel. All the rooms are rectangular in shape. He has a file, rooms.in, which contains data for the rooms. Each line of data consists of the room number, the length, and breadth of the room (in meters), and the cost per square meter of the carpet for that room. For example, the data line: 325 3.0 4.5 40.00
means that room 325
is 3.0
meters by 4.5
meters, and the cost of the carpet for that room is $40.00
per square meter. The last line of the file contains 0
only, indicating the end of the data. Write a program to do the following, sending output to the file rooms.out
:
- 打印一个合适的标题,并在标题下为每个房间打印房间号、房间面积和房间地毯的价格;
- 打印已处理的房间数量;
- 打印所有房间铺地毯的总费用;
- 打印地毯费用最高的房间号(忽略领带)。
The price of an item is p dollars. Due to inflation, the price of the item is expected to increase by r% each year. For example, the price might be $79.50 and inflation might be 7.5%. Write a program which reads values for p and r, and, starting with year 1, prints a table consisting of year and year-end price. The table ends when the year-end price is at least twice the original price. A fixed percentage of water is taken from a well each day. Request values for W and P where
- w 是第一天开始时井中的水量(以升为单位)
- p 是每天从井中取出的水的百分比
Write a program to print the number of the day, the amount taken for that day, and the amount remaining at the end of the day. The output should be terminated when 30 days have been printed or the amount remaining is less than 100 liters, whichever comes first. For example, if W is 1000 and P is 10, the output should start as follows: Day Amount Amount
Taken Remaining
1 100 900
2 90 810
3 81 729
Write a program to print the following 99 times: When you have nothing to say, it is a time to be silent
Write a program to print 8 copies of your favorite song. Write a program to print a table of squares from 1 to 10. Each line of the table consists of a number and the square of that number. Write a program to request a value for n
and print a table of squares from 1
to n
. Write a program to request values for first
and last
, and print a table of squares from first
to last
. Write a program to print 100 mailing labels for The Computer Store
57 First Avenue
San Fernando
Write a program to print a conversion table from miles to kilometers. The table ranges from 5 to 100 miles in steps of 5. (1 mile = 1.61 km). Write a program which requests a user to enter an amount of money. The program prints the interest payable per year for rates of interest from 5% to 12% in steps of 0.5%. Write a program to request a value for n
; the user is then asked to enter n
numbers, one at a time. The program calculates and prints the sum of the numbers. The following is a sample run: How many numbers? 3
Enter a number? 12
Enter a number? 25
Enter a number? 18
The sum of the 3 numbers is 55
Write a program to request an integer n
from 1
to 9
and print a line of output consisting of ascending digits from 1
to n
followed by descending digits from n-1
to 1
. For example, if n
= 5
, print the line 123454321
Solve problem 10, above, assuming that the first line of data contains the number of rooms (n
, say) to carpet. This is followed by n
lines of data, one line for each room. Solve problem 12, above, but this time print the table for exactly 30 days. If necessary, continue printing the table even if the amount of water falls below 100 liters.
六、字符
在本章中,我们将解释以下内容:
- 字符集的一些重要特性
- 如何使用字符常量和值
- 如何在 C 中声明字符变量
- 如何在算术表达式中使用字符
- 如何阅读、操作和打印字符
- 如何使用
\n
测试行尾 - 如何使用
EOF
测试文件结束 - 如何比较人物
- 如何从文件中读取字符
- 如何将数字从字符转换成整数
6.1 字符集
我们大多数人都熟悉计算机或打字机键盘(称为标准英语键盘)。在上面,我们可以键入字母表中的字母(大写和小写)、数字和其他“特殊”字符,如+、=、、&和%—这些就是所谓的可打印字符。
在计算机上,每个字符被赋予一个唯一的整数值,称为它的代码。根据所使用的字符集,不同计算机的代码可能会有所不同。例如,A
的代码在一台计算机上可能是33
,但在另一台计算机上可能是65
。
在计算机内部,这个整数代码被存储为一个比特序列;例如,33
的 6 位代码是100001
,而65
的 7 位代码是1000001
。
现在,大多数计算机使用 ASCII(美国信息交换标准码)字符集来表示字符。这是一个 7 位字符标准,包括标准键盘上的字母、数字和特殊字符。它还包括控制字符,如退格、制表符、换行符、换页符和回车符。
ASCII 码从0
到127
(可使用 7 位存储的数字范围)。ASCII 字符集如附录 b 所示。值得注意的有趣特性如下:
- 数字
0
到9
占用代码48
到57
。 - 大写字母
A
到Z
占用代码65
到90
。 - 小写字母
a
到z
占用代码97
到122
。
然而,请注意,尽管 ASCII 集合是使用 7 位代码定义的,但它在大多数计算机上是以 8 位字节存储的——在 7 位代码的前面添加了一个0
。比如A
的 7 位 ASCII 码是1000001
;在计算机上,存储为01000001
,占用一个字节。
在本书中,我们将尽可能在编写程序时不对底层字符集做任何假设。在不可避免的情况下,我们将假设使用 ASCII 字符集。例如,我们可能需要假设大写字母被分配了连续的代码,对于小写字母也是如此。对于另一个字符集来说,这不一定是真的。即便如此,我们也不会依赖于代码的具体值,只知道它们是连续的。
6.2 字符常量和值
字符常量是用单引号括起来的单个字符,如' A ','+'和' 5 '。有些字符不能这样表示,因为我们不能键入它们。其他的在 C 中起着特殊的作用(比如',)。对于这些,我们用单引号括起转义序列。下表显示了一些示例:
| 茶 | 描述 | 密码 | | --- | --- | --- | | `'\n'` | `new line` | `10` | | `'\f'` | `form feed` | `12` | | `'\t'` | `tab` | `9` | | `'\''` | `single quote` | `39` | | `'\\'` | `backslash` | `92` |字符常量'\0'
在 C 中比较特殊;就是代码为0
的字符,通常称为空字符。它的一个特殊用途是指示内存中一个字符串的结束(见第八章)。
字符常量的字符值是所表示的字符,没有单引号。因此,'T'
的角色值是T
,'\\'
的角色值是\
。
字符常量有一个与之关联的整数值,即所代表字符的数字代码。因此,'T'
的整数值是84
,因为T
的 ASCII 码是84
。因为\
的 ASCII 码是92
,所以'\\'
的整数值是92
。并且'\n'
的整数值是10
,因为换行符的 ASCII 码是10
。
我们可以使用printf
中的规范%c
打印字符值,使用%d
打印整数值。例如,语句
printf("Character: %c, Integer: %d\n", 'T', 'T');
将打印
Character: T, Integer: 84
6.3 类型char
在 C 中,我们使用关键字char
来声明一个变量,我们希望在其中存储一个字符。例如,语句
char ch;
将ch
声明为字符变量。例如,我们可以给ch
分配一个字符常量,如下所示:
ch = 'R'; //assign the letter R to ch
ch = '\n'; //assign the newline character, code 10, to ch
我们可以使用printf
中的%c
打印字符变量的字符值。我们可以使用%d
打印一个字符变量的整数值。例如,
ch = 'T';
printf("Mr. %c\n", ch);
printf("Mr. %d\n", ch);
将打印
Mr. T
Mr. 84
6.4 算术表达式中的字符
c 允许我们在算术表达式中直接使用char
类型的变量和常量。当我们这样做时,它使用字符的整数值。例如,语句
int n = 'A' + 3;
将68
分配给n
,因为'A'
的代码是65
。
类似地,我们可以将一个整数值赋给一个char
变量。例如,
char ch = 68;
在这种情况下,“代码为 68 的字符”被分配给ch
;这个人物就是'D'
。
对于更有用的示例,请考虑以下内容:
int d = '5' - '0';
由于'5'
的代码是53
而'0'
的代码是48
,所以整数5
被分配给d
。
请注意,字符形式的数字的代码与数字的值不同;例如,字符'5'
的代码是53
,但是数字5
的值是5
。有时我们知道一个字符变量包含一个数字,我们想得到这个数字的(整数)值。
上面的语句显示了我们如何获得该数字的值——我们只需从该数字的代码中减去'0'
的代码。数字的实际代码是什么并不重要;重要的是0
到9
的代码是连续的。(练习:假设数字有一组不同的代码值,自己检查一下。)
一般来说,如果ch
包含一个数字字符('0'
到'9'
,我们可以用语句获得该数字的整数值
d = ch - '0';
6.4.1 大写字母到小写字母的转换
假设ch
包含一个大写字母,我们想把它转换成等价的小写字母。例如,假设ch
包含'H'
,我们想把它改成'h'
。首先我们观察到'A'
到'Z'
的 ASCII 码范围从65
到90
,而'a'
到'z'
的码范围从97
到122
。我们进一步观察到,字母的两种大小写的代码之间的差异总是32
;例如,
'r' - 'R' = 114 – 82 = 32
因此,我们可以通过在大写代码中添加32
来将字母从大写转换成小写。这可以通过
ch = ch + 32;
如果ch
包含'H'
(代码72
),上面的语句在72
上加32
给出104
;将“代码为 104 的字符”赋给ch
,即'h'
。我们已经将ch
的值从'H'
更改为'h'
。相反,要将一个字母从小写转换成大写,我们从小写代码中减去32
。
顺便说一下,我们真的不需要知道字母的代码。我们所需要的是大写和小写代码之间的区别。我们可以让 C 用'a'
- 'A'
告诉我们有什么区别,像这样:
ch = ch + 'a' - 'A';
不管字母的实际代码是什么,这都是有效的。当然,它假设ch
包含一个大写字母,并且所有字母的大写和小写代码之间的差异是相同的。
6.5 阅读和打印字符
许多程序都围绕着一次读写一个字符的思想,开发编写这种程序的技能是编程的一个非常重要的方面。我们可以使用scanf
将标准输入(键盘)中的单个字符读入一个char
变量(比如说ch
)中:
scanf("%c", &ch);
数据中的下一个字符存储在ch
中。非常重要的是,要注意读一个数字和读一个字符之间的巨大差异。当读取一个数字时,scanf
将跳过任意数量的空格,直到找到该数字。当读取一个字符时,下一个字符(不管它是什么,即使它是一个空格)存储在变量中。
虽然我们可以使用scanf
,但是读取字符非常重要,所以 C 提供了一个特殊的函数getchar
来从标准输入中读取字符。(严格地说,getchar
就是所谓的宏,但这种区别对我们的目的来说并不重要。)大部分情况下,我们可以认为getchar
返回数据中的下一个字符。然而,它实际上返回下一个字符的数字代码。因此,它通常被赋给一个int
变量,如:
int c = getchar(); // the brackets are required
但它也可以赋给一个char
变量,如:
char ch = getchar(); // the brackets are required
准确地说,getchar
返回数据中的下一个字节——实际上,这是下一个字符。如果我们在没有更多的数据时调用getchar
,它将返回-1
。
更准确地说,它返回由在stdio.h
中定义的符号常量EOF
(全大写)指定的值。这个值通常是-1
,尽管并不总是如此。实际值取决于系统,但EOF
将始终表示运行程序的系统返回的值。当然,我们总是可以通过打印EOF
来找出返回值,因此:
printf("Value of EOF is %d \n", EOF);
例如,考虑以下语句:
char ch = getchar();
假设用户键入的数据如下:
Hello
当执行ch = getchar()
时,第一个字符H
被读取并存储在ch
中。然后我们可以以任何我们喜欢的方式使用ch
。假设我们只想打印读取的第一个字符。我们可以使用:
printf("%c \n", ch);
这会打印出来
H
单独在一条线上。当然,我们可以将输出标记为下面的语句:
printf("The first character is %c \n", ch);
这会打印出来
The first character is H
最后,我们甚至不需要ch
。如果我们只想打印数据中的第一个字符,我们可以使用:
printf("The first character is %c \n", getchar());
如果我们想打印第一个字符的数字代码,我们可以通过使用规范%d
而不是%c
来实现。这些想法都包含在程序 P6.1 中。
Program P6.1
//read the first character in the data, print it,
//its code and the value of EOF
#include <stdio.h>
int main() {
printf("Type some data and press 'Enter' \n");
char ch = getchar();
printf("\nThe first character is %c \n", ch);
printf("Its code is %d \n", ch);
printf("Value of EOF is %d \n", EOF);
}
以下是运行示例:
Type some data and press 'Enter'
Hello
The first character is H
Its code is 72
Value of EOF is -1
提醒一句:我们可能会想写以下内容:
printf("The first character is %c \n", getchar());
printf("Its code is %d \n", getchar()); // wrong
但是如果我们这样做了,并且假设将Hello
作为输入,那么这些语句将会打印出来:
The first character is H
Its code is 101
为什么呢?在第一个printf
,getchar
返回H
,它被打印出来。第二个printf
,getchar
返回下一个字符,是e
;打印的是e
的代码(101
)。
在程序 P6.1 中,我们可以使用一个int
变量(比如说n
)来代替ch
,程序将以相同的方式工作。如果使用%c
打印一个int
变量,变量的最后(最右边)8 位被解释为一个字符并打印该字符。例如,H
的代码是72
,也就是二进制的01001000
,使用 8 位。假设n
是 16 位int
,当H
被读取时,分配给n
的值将为
00000000 01001000
如果n
现在印有%c
,最后 8 位将被解释为一个字符,当然是H
。
类似地,如果将一个int
值n
赋给一个char
变量(ch
),那么n
的最后 8 位将被赋给ch
。
如上所述,getchar
返回所读取字符的整数值。当用户按下键盘上的“Enter”或“return”时,它返回什么?它返回换行符\n
,其代码是10
。使用程序 P6.1 可以看到这一点。当程序等待您键入数据时,如果您只按“Enter”或“Return”键,输出的第一行将如下所示(注意空行):
The first character is
Its code is 10
为什么是空行?由于ch
包含\n
,该语句
printf("\nThe first character is %c \n", ch);
实际上与下面的相同(用ch
的值代替%c
)
printf("\nThe first character is \n \n");
is
后的\n
结束第一行,最后的\n
结束第二行,有效地打印一个空行。然而,请注意\n
的代码打印正确。
在程序 P6.1 中,我们只读取第一个字符。如果我们想阅读和打印前三个字符,我们可以用程序 P6.2 来做。
Program P6.2
//read and print the first 3 characters in the data
#include <stdio.h>
int main() {
printf("Type some data and press 'Enter' \n");
for (int h = 1; h <= 3; h++) {
char ch = getchar();
printf("Character %d is %c \n", h, ch);
}
}
以下是该程序的运行示例:
Type some data and press 'Enter'
Hi, how are you?
Character 1 is H
Character 2 is i
Character 3 is,
如果我们想读取并打印前 20 个字符,我们所要做的就是将for
语句中的3
改为20
。
假设数据行的第一部分包含任意数量的空白,包括没有空白。我们如何找到并打印第一个非空白字符?因为我们不知道要读多少个空格,所以我们不能说“读 7 个空格,然后读下一个字符”
更有可能的是,我们需要说类似“只要读到的字符是空白,就继续读下去。”只要某些“条件”为真,我们就有做某事(阅读角色)的概念;这里的条件是字符是否为空。这可以更简明地表达如下:
read a character
while the character read is a blank
read the next character
程序 P6.3 显示了如何读取数据并打印第一个非空白字符。(这段代码将在本节的后面写得更简洁。)
Program P6.3
//read and print the first non-blank character in the data
#include <stdio.h>
int main() {
printf("Type some data and press 'Enter' \n");
char ch = getchar(); // get the first character
while (ch == ' ') // as long as ch is a blank
ch = getchar(); // get another character
printf("The first non-blank is %c \n", ch);
}
以下是该程序的运行示例(表示空白):
Type some data and press 'Enter'
你好
The first non-blank is H
程序将定位第一个非空白字符,不管它前面有多少个空格。
作为对while
语句如何工作的提醒,考虑程序 P6.3 中带有不同注释的以下代码部分:
char ch = getchar(); //executed once; gives ch a value
//to be tested in the while condition
while (ch == ' ')
ch = getchar(); //executed as long as ch is ' '
假设输入的数据是(◊表示空格):
◇♂Hello
代码将按如下方式执行:
The first character is read and stored in ch
; it is a blank. The while
condition is tested; it is true
. The while
body ch = getchar();
is executed and the second character is read and stored in ch
; it is a blank. The while
condition is tested; it is true
. The while
body ch = getchar();
is executed and the third character is read and stored in ch
; it is a blank. The while
condition is tested; it is true
. The while
body ch = getchar();
is executed and the fourth character is read and stored in ch
; it is H
. The while
condition is tested; it is false
. Control goes to the printf
, which prints. The first non-blank is
H
如果H
是数据中的第一个字符呢?代码将按如下方式执行:
The first character is read and stored in ch
; it is H
. The while
condition is tested; it is false
. Control goes to the printf
, which prints. The first non-blank is H
还能用!如果第一次测试时while
条件为false
,则主体根本不执行。
作为另一个例子,假设我们想打印所有字符,但不包括第一个空格。为此,我们可以使用程序 P6.4。
Program P6.4
//print all characters before the first blank in the data
#include <stdio.h>
int main() {
printf("Type some data and press 'Enter' \n");
char ch = getchar(); // get the first character
while (ch != ' ') { // as long as ch is NOT a blank
printf("%c \n", ch);// print it
ch = getchar(); // and get another character
}
}
以下是 P6.4 的运行示例:
Type some data and press 'Enter'
Way to go
W
a
y
while
的主体由两条语句组成。这些由{
和}
括起来,以满足 C 的规则,即while
主体必须是单个语句或块。在这里,只要读取的字符不是空白,就执行主体——我们使用!=
(不等于)来编写条件。
如果字符不是空白,则打印该字符并读取下一个字符。如果不是空白,则打印出来并读取下一个字符。如果不是空白,则打印出来并读取下一个字符。依此类推,直到一个空白字符被读取,使while
条件false
,导致从循环中退出。
如果我们不告诉你一些 c 语言的表达能力,那我们就大错特错了。例如,在 P6.3 程序中,我们可以读取字符并在while
条件下测试它。我们可以重写以下三行:
ch = getchar(); // get the first character
while (ch == ' ') // as long as ch is a blank
ch = getchar(); // get another character
作为一条线
while ((ch = getchar()) == ' '); // get a character and test it
ch = getchar()
为赋值表达式,其值为赋给ch
的字符,即读取的字符。然后测试这个值,看它是否为空。因为==
比=
具有更高的优先级,所以ch = getchar()
周围的括号是必需的。如果没有它们,条件将被解释为ch = (getchar() == ' ')
。这将把一个条件的值(在 C 语言中,是代表false
的0
或代表true
的1
)赋给变量ch
;这不是我们想要的。
既然我们已经把身体中的语句移入了条件,身体就是空的;这在 c 语言中是允许的。条件现在会被重复执行,直到它变成false
。
再举一个例子,在程序 6.4 中,考虑下面的代码:
char ch = getchar(); // get the first character
while (ch != ' ') { // as long as ch is NOT a blank
printf("%c \n", ch) // print it
ch = getchar(); // and get another character
}
这可以重新编码如下(假设ch
在循环之前声明):
while ((ch = getchar()) != ' ') // get a character
printf("%c \n", ch); // print it if non-blank; repeat
既然正文只包含一条语句,就不再需要大括号了。五行被减成了两行!
6.6 计算字符数
程序 P6.3 打印第一个非空白字符。假设我们想计算在第一个非空白之前有多少个空白。我们可以使用一个整数变量numBlanks
来保存计数。程序 P6.5 是计算前导空白的修改程序。
Program P6.5
//find and print the first non-blank character in the data;
// count the number of blanks before the first non-blank
#include <stdio.h>
int main() {
char ch;
int numBlanks = 0;
printf("Type some data and press 'Enter' \n");
while ((ch = getchar()) == ' ') // repeat as long as ch is blank
numBlanks++; // add 1 to numBlanks
printf("The number of leading blanks is %d \n", numBlanks);
printf("The first non-blank is %c \n", ch);
}
以下是该程序的运行示例(表示空格):
Type some data and press 'Enter'
你好
The number of leading blanks is 4
The first non-blank is H
对程序 P6.5 的意见:
numBlanks
在while
循环之前被初始化为0
。numBlanks
在循环内由1
递增,因此每次执行循环体时numBlanks
都会递增。由于循环体是在ch
包含空白时执行的,所以numBlanks
的值总是到目前为止读取的空白数。- 当我们退出
while
循环时,numBlanks
中的值将是读取的空白的数量。然后打印该值。 - 注意,如果数据中的第一个字符不为空,
while
条件将立即变为false
,控制将直接转到第一个printf
语句,其中numBlanks
的值为0
。该程序将正确打印:
The number of leading blanks is 0
6.6.1 计算一行中的字符数
假设我们想计算一行输入中的字符数。现在,我们必须阅读字符,直到行尾。我们的程序如何测试行尾?回想一下,当用户按下“Enter”或“Return”键时,换行符\n
由getchar
返回。下面的while
条件读取一个字符并测试\n
。
while ((ch = getchar()) != '\n')
程序 P6.6 读取一行输入并计算其中的字符数,不计算“行尾”字符。
Program P6.6
//count the number of characters in the input line
#include <stdio.h>
int main() {
char ch;
int numChars = 0;
printf("Type some data and press 'Enter' \n");
while ((ch = getchar()) != '\n') // repeat as long as ch is not \n
numChars++; // add 1 to numChars
printf("The number of characters is %d \n", numChars);
}
这个程序和程序 P6.5 的主要区别在于,这个程序读取字符直到行尾,而不是直到第一个非空白。运行示例如下:
Type some data and press 'Enter'
One moment in time
The number of characters is 18
6.7 计算一行数据中的空白
假设我们要计算一行数据中的所有空格。我们仍然必须读取字符,直到遇到行尾。但现在,每读一个字,都要检查是否是空格。如果是,则计数递增。我们需要两个计数器——一个计数一行中的字符数,另一个计数空格数。该逻辑可以表示为:
set number of characters and number of blanks to 0
while we are not at the end-of-line
read a character
add 1 to number of characters
if character is a blank then add 1 to number of blanks
endwhile
该逻辑的实现如程序 P6.7 所示。
Program P6.7
//count the number of characters and blanks in the input line
#include <stdio.h>
int main() {
char ch;
int numChars = 0;
int numBlanks = 0;
printf("Type some data and press 'Enter' \n");
while ((ch = getchar()) != '\n') { // repeat as long as ch is not \n
numChars++; // add 1 to numChars
if (ch == ' ') numBlanks++; // add 1 if ch is blank
}
printf("\nThe number of characters is %d \n", numChars);
printf("The number of blanks is %d \n", numBlanks);
}
下面是一个运行示例:
Type some data and press 'Enter'
One moment in time
The number of characters is 18
The number of blanks is
3
if
语句测试条件ch == ' '
;如果是true
(即ch
包含空白),numBlanks
递增1
。如果是false
,则numBlanks
不递增;控制通常会转到循环中的下一条语句,但是没有(if
是最后一条语句)。因此,控制返回到while
循环的顶部,在那里读取另一个字符并对\n
进行测试。
6.8 比较字符
可以使用关系运算符==,!=,和> =。我们已经使用ch == ' '
和ch != ' '
将char
变量ch
与空白变量进行了比较。
现在让我们写一个程序来读取一行数据并打印出“最大”的字符,即代码最高的字符。例如,如果一行是由英语单词组成的,那么字母表中最后出现的字母将被打印出来。(回想一下,小写字母比大写字母具有更高的代码,例如,'g'
大于'T'
。)
“寻找最大字符”包括以下步骤:
- 选择一个变量来保存最大值;我们选择
bigChar
。 - 将
bigChar
初始化为一个非常小的值。选择的值应该是这样的,无论读取什么字符,它的值都大于这个初始值。对于字符,我们通常使用'\0'
——空字符,即代码为0
的“字符”。 - 当每个字符(
ch
,比方说)被读取时,它与bigChar
进行比较;如果ch
大于bigChar
,那么我们有一个‘更大’的字符,并且bigChar
被设置为这个新字符。 - 当所有字符都被读取和检查后,
bigChar
将包含最大的一个。
这些想法在程序 P6.8 中表达。
Program P6.8
//read a line of data and find the 'largest' character
#include <stdio.h>
int main() {
char ch, bigChar = '\0';
printf("Type some data and press 'Enter' \n");
while ((ch = getchar()) != '\n')
if (ch > bigChar) bigChar = ch; //is this character bigger?
printf("\nThe largest character is %c \n", bigChar);
}
下面是一个运行示例;因为它的代码是所有输入字符中最高的,所以被打印出来。
Type some data and press 'Enter'
Where The Mind Is Without Fear
The largest character is u
6.9 从文件中读取字符
在我们到目前为止的例子中,我们已经阅读了在键盘上输入的字符。如果我们想从一个文件中读取字符(input.txt
),我们必须声明一个文件指针(in
),并使用
FILE * in = fopen("input.txt", "r");
一旦完成,我们可以用下面的语句将文件中的下一个字符读入一个字符变量(ch
:
fscanf(in, "%c", &ch);
但是,C 提供了更方便的函数getc
(获取字符)来从文件中读取字符。它的用法如下:
ch = getc(in);
getc
接受一个参数,文件指针(不是文件名)。它读取并返回文件中的下一个字符。如果没有更多的字符需要读取,getc 返回EOF
。因此,getc
的工作方式与getchar
完全一样,除了getchar
从键盘读取,而getc
从文件读取。
举例来说,让我们编写一个程序,从文件input.txt
中读取一行数据,并将其打印在屏幕上。这显示为程序 P6.9。
Program P6.9
#include <stdio.h>
int main() {
char ch;
FILE *in = fopen("input.txt", "r");
while ((ch = getc(in)) != '\n')
putchar(ch);
putchar('\n');
fclose(in);
}
这个程序使用标准函数putchar
将单个字符写入标准输出。(像getchar
,putchar
是一个宏,但是这种区别对于我们的目的并不重要。)它将一个字符值作为唯一的参数,并将该字符写入输出中的下一个位置。然而,如果该字符是控制字符,则产生该字符的效果。例如,
putchar('\n');
将结束当前输出行——与按下“Enter”或“Return”的效果相同。
程序从文件中一次读取一个字符,并使用putchar
将其打印在屏幕上。它一直这样做,直到\n
被读取,表示整行都已被读取。在退出while
循环时,它使用putchar('\n')
终止屏幕上的行。
不过,要小心。该程序假设数据行由一个行尾字符\n
(当您按下“Enter”或“Return”时生成)终止。但是,如果行没有被\n
终止,程序就会‘挂起’——它会陷入一个无法摆脱的循环(我们说它会陷入无限循环)。为什么呢?
因为由于没有要读取的\n
,所以while
条件((ch = getc(in)) != '\n')
永远不会变成false
(当ch
为'\n'
时会发生这种情况)。但是,如前所述,当我们到达文件末尾时,由getchar
返回的值,现在也由getc
返回,是在stdio.h
中定义的符号常量EOF d
。了解了这一点,我们可以通过在while
条件下测试\n
和EOF
来轻松解决我们的问题,因此:
while ((ch = getc(in)) != '\n' && ch != EOF)
即使\n
不存在,当到达文件末尾时getc(in)
将返回EOF
,并且条件ch != EOF
将为false
,导致从循环中退出。
6.10 将字符写入文件
假设我们想把字符写到一个文件中(output.txt
)。像往常一样,我们必须声明一个文件指针(out
),并使用
FILE * out = fopen("output.txt", "w");
如果ch
是一个 char 变量,我们可以用
fprintf(out, "%c", ch);
c 也提供了函数putc
(放置一个字符)来做同样的工作。要将ch
的值写入与out
相关的文件,我们必须写:
putc(ch, out);
注意文件指针是putc
的第二个参数。
6.10.1 回声输入,数字线
让我们扩展上一页的示例,从文件中读取数据,并将相同的数据写回(回显数据)到屏幕上,从 1 开始对行进行编号。
程序将从文件中读取数据,并将其写入屏幕,因此:
1\. First line of data
2\. Second line of data
etc.
这个问题比我们到目前为止遇到的那些问题更难一点。当面对这样的问题时,最好是一次解决一点,解决问题的简单版本,然后逐步解决整个问题。
对于这个问题,我们可以先写一个程序,简单的回显输入,不需要对行进行编号。当我们把这个做好了,我们就可以开始给这些线编号了。
这个第一版本的算法概要如下:
read a character, ch
while ch is not the end-of-file character
print ch
read a character, ch
endwhile
这将保持数据文件的行结构,例如,当从文件中读取\n
时,它会立即打印到屏幕上,强制当前行结束。
程序 P6.10 实现了上述算法,用于从文件中读取数据,并在屏幕上打印精确的副本。
Program P6.10
#include <stdio.h>
int main() {
char ch;
FILE *in = fopen("input.txt", "r");
while ((ch = getc(in)) != EOF)
putchar(ch);
fclose(in);
}
既然我们可以回显输入,我们只需要弄清楚如何打印行号。一种简单的方法基于以下大纲:
set lineNo to 1
print lineNo
read a character, ch
while ch is not the end-of-file character
print ch
if ch is \n
add 1 to lineNo
print lineNo
endif
read a character, ch
endwhile
我们只是在上面的算法中添加了处理行号的语句。我们可以很容易地将处理行号的代码添加到程序 P6.10 中,以得到程序 P6.11。注意,当我们打印行号时,我们不使用\n
终止行,因为数据必须与行号写在同一行。
Program P6.11
//This program prints the data from a file numbering the lines
#include <stdio.h>
int main() {
char ch;
FILE *in = fopen("input.txt", "r");
int lineNo = 1;
printf("%2d. ", lineNo);
while ((ch = getc(in)) != EOF) {
putchar(ch);
if (ch == '\n') {
lineNo++;
printf("%2d. ", lineNo);
}
}
fclose(in);
}
假设输入文件包含以下内容:
There was a little girl
Who had a little curl
Right in the middle of her forehead
程序 P6.11 将打印以下内容:
1\. There was a little girl
2. Who had a little curl
3\. Right in the middle of her forehead
4.
差不多,但不完全正确!小问题是我们在末尾打印了一个额外的行号。要了解原因,请查看if
语句。当第三条数据线的\n
被读取时,1
将被加到lineNo
上,成为4
,由下一条语句打印。如果输入文件是空的,这种额外行号的打印也成立,因为在这种情况下行号1
将被打印,但是没有这样的行。
为了解决这个问题,我们必须延迟打印行号,直到我们确定该行至少有一个字符。我们将使用一个int
变量writeLineNo
,初始设置为1
。如果我们要打印一个字符,而writeLineNo
是1
,则打印行号,并将writeLineNo
设置为0
。当writeLineNo
为0
时,所发生的只是将刚刚读出的字符打印出来。
当\n
打印结束一行输出时,writeLineNo
设置为1
。如果结果是下一行有一个字符要打印,由于writeLineNo
是1
,行号将首先被打印。如果没有更多的字符要打印,就不再打印;特别是,不打印行号。
程序 P6.12 包含所有细节。运行时,它将对行进行编号,而不打印额外的行号。
Program P6.12
//This program prints the data from a file numbering the lines
#include <stdio.h>
int main() {
char ch;
FILE *in = fopen("input.txt", "r");
int lineNo = 0, writeLineNo = 1;
while ((ch = getc(in)) != EOF) {
if (writeLineNo) {
printf("%2d. ", ++lineNo);
writeLineNo = 0;
}
putchar(ch);
if (ch == '\n') writeLineNo = 1;
}
fclose(in);
}
我们将if
条件写成如下:
if (writeLineNo)...
如果writeLineNo
为1
,则条件评估为1
,因此为true
;如果是0
,则条件为false
。我们也可以把条件写成
if (writeLineNo == 1)...
在声明中
printf("%d. ", ++lineNo);
表达式++lineNo
表示lineNo
在打印前先递增。相比之下,如果我们使用了lineNo++
,那么lineNo
将首先被打印,然后递增。
练习:修改程序 P6.12,将输出发送到文件linecopy.txt
。
练习:编写一个程序,将文件input.txt
的内容复制到文件copy.txt
中。提示:你只需要对程序 P6.10 做一些小的改动。
6.11 将数字字符转换为整数
让我们考虑如何将一系列数字转换成整数。当我们输入数字 385 时,我们实际上是在输入三个独立的字符——3,8,5。在计算机内部,整数 385 和三个字符“3”“8”“5”完全不同。因此,当我们键入 385 并试图将其读入一个int
变量时,计算机必须将这三个字符的序列转换为整数 385。
举例来说,字符“3”、“8”和“5”的 8 位 ASCII 码分别是 00110011、00111000 和 00110101。当输入到屏幕或文件中时,数字 385 表示为:
00110011 00111000 00110101
假设使用 16 位存储整数,则整数 385 由其等价的二进制表示
0000000110000001
请注意,字符表示与整数表示有很大不同。当我们要求scanf
(或fscanf
)读取我们输入的整数时,它必须将字符表示转换成整数表示。我们现在展示这是如何做到的。
基本步骤要求我们将数字字符转换成等价的整数值。例如,我们必须将字符“5”(用 00110101 表示)转换为整数 5(用 000000000000101 表示)。
假设数字0
到9
的代码是连续的(因为它们在 ASCII 和其他字符集中),这可以如下进行:
数字的整数值=数字字符的代码-字符'0
'的代码
例如,在 ASCII 中,“5”的代码是 53,而“0
”的代码是 48。从 53 中减去 48 得到字符“5”的整数值(5)。一旦我们可以转换单个数字,我们就可以使用以下算法从左到右读取数字,从而构建数字的值:
set num to
0
get a character, ch
while ch is a digit character
convert ch to the digit value, d = ch - '0'
set num to num*10 + d
get a character, ch
endwhile
num now contains the integer value
字符序列 385 被转换如下:
num =
0
get '3'; convert to 3
num = num*10 + 3 = 0*10 + 3; num is now 3
get '8'; convert to 8
num = num*10 + 8 = 3*10 + 8; num is now 38
get '5'; convert to 5
num = num*10 + 5 = 38*10 + 5; num is now 385
没有更多的数字,num
的最终值是 385。
让我们用这个想法写一个程序,一个字符一个字符地读取数据,直到找到一个整数。它构造并打印整数。
程序将不得不读取字符,直到它找到一个数字,整数的第一个。找到第一个数字后,只要它一直得到一个数字,就必须通过读取字符来构造整数。例如,假设数据如下:
Number of items: 385, all in good condition
该程序将读取字符,直到它找到第一个数字,3。它将使用 3 构造整数,然后读取 8 和 5。当它读取逗号时,它知道整数已经结束。
这个大纲可以用伪代码表示如下:
read a character, ch
while ch is not a digit do
read a character, ch
endwhile
//at this point, ch contains a digit
while ch is a digit do
use ch to build the integer
read a character, ch
endwhile
print the integer
我们如何测试ch
中的字符是否是数字?我们必须测试
ch >= '0' && ch <= '9'
如果这是真的,我们就知道这个角色在'0'
和'9'
之间,包含这两个值。反过来,为了测试ch
是否不是一个数字,我们可以测试
ch < '0' || ch > '9'
把所有这些想法放在一起,我们就有了 P6.13 程序。
Program P6.13
#include <stdio.h>
int main() {
printf("Type data including a number and press \"Enter\"\n");
char ch = getchar();
// as long as the character is not a digit, keep reading
while (ch < '0' || ch > '9') ch = getchar() ;
// at this point, ch contains the first digit of the number
int num = 0;
while (ch >= '0' && ch <= '9') { // as long as we get a digit
num = num * 10 + ch - '0'; // update num
ch = getchar();
}
printf("Number is %d\n", num);
}
下面显示了一个运行示例:
Type data including a number and press "Enter"
hide the number``&``(%%)4719``&``*(``&
Number is 4719
这个程序会找到这个数字,不管它隐藏在这行的什么地方。
EXERCISES 6Give the range of ASCII codes for (a) the digits (b) the uppercase letters (c) the lowercase letters. How is the single quote represented as a character constant? What is the character value of a character constant? What is the numeric value of a character constant? How is the expression 5 + 'T'
evaluated? What is its value? What value is assigned to n
by n = 7 + 't'
? What character is stored in ch
by ch = 4 + 'n'
? If ch = '8'
, what value is assigned to d
by d = ch - '0'
? If ch
contains any uppercase letter, explain how to change ch
to the equivalent lowercase letter. If ch
contains any lowercase letter, explain how to change ch
to the equivalent uppercase letter. Write a program to request a line of data and print the first digit on the line. Write a program to request a line of data and print the first letter on the line. Write a program to request a line of data and print the number of digits and letters on the line. Write a program to read a passage from a file and print how many times each vowel appears. Modify Program P6.13 so that it will find negative integers as well. Write a program that reads a file containing a C program and outputs the program to another file with all the //
comments removed. Write a program to read the data, character by character, and store the next number (with or without a decimal point) in a double
variable (dv
, say). For example, given the following data your program should store 43.75
in dv
. Mary works for $43.75 per hour
In the programming language Pascal, comments can be enclosed by {
and }
or by (*
and *)
. Write a program which reads a data file input.pas
containing Pascal code and writes the code to a file output.pas
, replacing each {
with (*
and each }
with *)
. For example, the statements read(ch); {get the first character}
while ch = ' ' do {as long as ch is a blank}
read(ch); {get another character}
writeln('The first non-blank is ', ch);
should be converted to
read(ch); (*get the first character*)
while ch = ' ' do (*as long as ch is a blank*)
read(ch); (*get another character*)
writeln('The first non-blank is ', ch);
You are given the same data as in 17, but now remove the comments altogether. Someone has typed a letter in a file letter.txt
, but does not always start the word after a period with a capital letter. Write a program to copy the file to another file format.txt
so that all words after a period now begin with a capital letter. Also ensure there is exactly one space after each period. For example, the text Things are fine. we can see you now. let us know when is a good time. bye for now.
must be rewritten as Things are fine. We can see you now. Let us know when is a good time. Bye for now.
七、函数
在本章中,我们将解释以下内容:
- 为什么函数在编程中很重要
- 如何编写函数
- 当一个函数被调用时会发生什么
- 在程序中放置函数的地方
- 用几个例子说明与函数有关的一些重要概念
7.1 关于函数
到目前为止,我们所有的程序都由一个名为main
的函数组成。然而,我们使用了预定义的 C 函数,如printf
、scanf
、strcpy
和fopen
。当我们运行一个程序时,它从main
中的第一条语句开始执行,并在到达最后一条语句时结束。
正如我们所见,只用main
就可以编写出相当有用的程序。然而,这种方法有许多限制。要解决的问题可能太复杂,无法用一个函数解决。我们可能需要把它分解成子问题,并尝试逐个解决。在一个函数中解决所有子问题是不切实际的。编写一个单独的函数来解决每个子问题可能更好。
此外,我们可能希望重用常见问题的解决方案。如果一个解决方案是一个更大问题的解决方案的一部分,那么重用它是很困难的。例如,如果我们需要几个地方的两个数字的最大公因数(HCF ),最好编写一个例程来计算两个给定数字的 HCF;每当我们需要找到两个数的 HCF 时,我们就调用这个例程。
一个编写良好的函数执行一些定义良好的任务;例如,在输出中跳过指定数量的行,或者按升序排列一些数字。然而,通常情况下,函数也会返回值;例如,计算一个人的工资并返回答案,或者玩一局游戏并返回该局的分数。返回值通常在调用函数时使用。
之前,我们使用了字符串函数strcmp
,它返回一个值,告诉我们比较两个字符串的结果。我们使用了getchar
和getc
来返回输入中的下一个字符。
我们现在准备学习如何编写我们自己的函数(称为用户定义函数),我们将在本书的其余部分看到几个例子。
7.2 skipLines
我们已经看到,我们可以在 printf 语句中使用\n
来打印一个空行。例如,语句
printf("%d\n\n%d\n", a, b);
将在一行打印a
,跳过一行,在下一行打印b
。我们通常可以通过在printf
语句中写入适当数量的\n
来跳过任意数量的行。
有时我们可能想跳过 3 行,有时 2 行,有时 5 行,等等。如果有一个我们可以用来跳过任意行数的语句就好了。例如,跳过 3 行,我们应该能够写
skipLines(3);
为了跳过 5 行,我们写
skipLines(5);
我们想要的是一个名为skipLines
的函数,它接受一个整数参数(比如说n
)并跳过n
行。在 C 语言中,我们将该函数编写如下:
void skipLines(int n) {
for (int h = 1; h <= n; h++)
printf("\n");
}
注意函数的结构与main
的结构相似。它由一个标题(第一行,除了{
)和一个用大括号括起来的正文组成。单词void
表示该函数不返回值,并且(int n)
将n
定义为一个整数参数。当函数被调用时,我们必须给它提供一个整数值来匹配参数n
。
这是函数skipLines
的定义。我们通过在main
中编写如下语句时调用该函数来使用该函数:
skipLines(3);
(一个函数通常可以从任何其他函数调用,但是,为了集中我们的讨论,我们将假设它是从main
调用的。)
我们说我们用参数调用函数。(在本书中,我们在提到函数的定义时使用术语“参数”,在调用函数时使用术语“自变量”。其他人互换使用这两个术语。)按如下方式执行“调用”:
- 自变量的值被确定。在这种情况下,它只是常量 3,但一般来说,它可以是一个表达式。
- 该值被复制到临时内存位置。该位置被传递给函数,并标有参数名
n
。实际上,参数变量n
被设置为自变量的值。我们可以这样描述: - 执行函数的主体。在这种情况下,由于
n
是 3,for 循环变成for (int h = 1; h <= 3; h++)
并打印\n
三次。 - 当函数完成时,包含自变量的位置被丢弃,控制返回到
main
到skipLines(3)
之后的语句。
注意,我们可以通过在调用时提供不同的参数来让skipLines
打印不同数量的空行。
当一个参数的值被传递给一个函数时,我们说这个参数是“按值”传递的在 C # 中,参数是“按值”传递的
7.3 具有函数的程序
我们编写程序 P7.1 来展示skipLines
如何融入一个完整的程序。
Program P7.1
#include <stdio.h>
int main() {
void skipLines(int);
printf("Sing a song of sixpence\n");
skipLines(2);
printf("A pocket full of rye\n");
} //end main
void skipLines(int n) {
for (int h = 1; h <= n; h++)
printf("\n");
} //end skipLines
当我们希望在main
中使用一个变量时,我们必须在main
中声明这个变量。同样,如果我们想在main
中使用skipLines
,我们必须使用所谓的函数原型告诉 C。函数原型是一种声明,很像函数头。在程序中,我们使用原型:
void skipLines(int);
原型通过声明函数的返回类型(void
,在本例中)、函数的名称(skipLines
)和任何参数的类型(在本例中为int
)来描述函数。如果您愿意,可以在类型后面写一个变量,如下所示:
void skipLines(int a);
只有当编译器需要生成错误信息时,才会使用这个变量。在本书中,我们将只使用类型来编写原型。
注意,函数原型后面是分号,而函数头后面是左括号。
作为另一个例子,原型
int max(int, int);
说明max
是一个接受两个整数参数并返回一个整数值的函数。
初学者常犯的一个错误是忘记写函数原型。然而,这不是一个大问题。如果你忘记了,编译器会提醒你。这就像忘记声明一个变量——编译器会告诉你的。你只要修好它然后继续前进。
在布局上,组成一个 C 程序的函数,包括main
,可以任意顺序出现。然而,习惯上把main
放在第一位,这样可以很容易地看到程序的整体逻辑。
我们强调该程序仅用于说明目的,因为使用该程序可以更容易地生成输出:
printf("Sing a song of sixpence\n\n\n");
printf("A pocket full of rye\n");
7.3.1 函数标题
在我们的例子中,我们使用了函数头
void skipLines(int n)
通常,函数头包括:
- 一个类型(如
void
、int
、double
、char
,指定函数返回值的类型。如果没有返回值,我们使用单词void
。函数skipLines
不返回值,所以我们使用void
。 - 我们为函数起的名字,在例子中是
skipLines
。 - 零个或多个参数,称为参数列表,用括号括起来;示例中使用了一个类型为
int
的参数n
。如果没有参数,括号必须仍然存在,就像在printHeading()
中一样。
函数头后面是函数体的左括号。
参数的指定方式与变量的声明方式相同。事实上,它们确实是声明。以下是 void 函数头的所有有效示例:
void sample1(int m, int n) // 2 parameters
void sample2(double a, int n, char c) // 3 parameters
void sample3(double a, double b, int j, int k) // 4 parameters
每个参数必须单独声明,两个连续的声明用逗号分隔。例如,写是无效的
void sample1(int m, n) //not valid; must write (int m, int n)
很快,我们将看到返回值的函数的例子。
7.3.2 函数如何获取数据
函数就像一个迷你程序。在我们编写的程序中,我们已经说明了必须向程序提供什么数据,必须进行什么处理,以及输出(结果)应该是什么。我们在编写函数时也必须这样做。
当我们编写函数头时,我们使用参数列表来指定在调用函数时必须向函数提供什么数据。该列表指定了数据项的数量、每个数据项的类型以及它们必须被提供的顺序。
比如我们用一个整型参数n
写skipLines
;这意味着在调用skipLines
时必须提供一个整数值。当调用skipLines
时,所提供的参数成为n
的特定值,并且假设n
具有该值,则执行该函数。在调用skipLines(3)
中,参数3
是skipLines
执行其工作所需的数据。
值得强调的是,main
通过使用scanf
以及其他函数来读取和存储变量中的数据。另一方面,函数在被调用时获取数据。参数列表中的变量被设置为调用中使用的相应参数的值。例如,当我们写标题时
void sample(int n, char c, double b)
我们说,当我们调用 sample 时,我们必须使用三个参数:第一个必须是一个int
值,第二个是一个char
值,第三个是一个double
值。
假设num
是int
,ch
是char
,x
是double
,以下都是有效的采样调用:
sample(25, 'T', 7.5);
sample(num, 'A', x);
sample(num, ch, 7); //an int argument can match a double parameter
sample(num + 1, ch, x / 2.0);
如果调用函数时,实参的类型与对应的形参不同,C 会尝试将实参转换为所需的类型。例如,在呼叫中
sample(num, 72, 'E');
值72
被转换为char
,参数c
被设置为'H'
(因为H
的代码是72
);'E'
(即69
)的数值被转换为双精度值69.0
,参数b
被设置为69.0
。
如果无法将参数转换为所需的类型,您将得到一个“类型不匹配”错误,如调用
sample(num, ch, "hi"); // error - cannot convert string to double
如果没有提供所需数量的参数,也会出现错误,如
sample(num, x); // error - must have 3 arguments
最大 7.4
有时我们需要找到两个值中较大的一个。如果a
和b
是两个数字,我们可以将变量max
设置为两者中较大的一个,如下所示:
if (a > b) max = a;
else max = b;
如果两个数相等,max
将被设置为b
(将执行else
部分)。当然,每当我们想得到两个值中较大的一个时,我们都可以写这个语句。但这会变得笨拙和尴尬。如果我们可以简单地编写类似这样的代码,将会更加方便,可读性更好
big = max(a, b);
以至
printf("The bigger is %d\n", max(a, b));
我们可以,如果我们把函数max
写成如下:
int max(int a, int b) {
if (a > b) return a;
return b;
}
第一行(除了{
)是函数头。它包括
- 单词
int
,表示函数返回整数值。 - 我们为函数起的名字,在例子中是
max
。 - 一个或多个参数,称为参数列表,用括号括起来;示例中使用了类型为
int
的两个参数a
和b
。
函数的主体是从{
到}
的部分。这里,我们使用if s
语句来确定a
和b
中较大的一个。如果a
变大,函数“返回”a
;如果没有,则返回b
。
在 C 语言中,函数通过使用return
语句“返回值”。它由单词return
后跟要返回的值组成。该值返回到调用该函数的位置。
为了展示max
如何融入整个程序以及如何使用它,我们编写了程序 P7.2,它读取整数对,并为每一对打印两个整数中较大的一个。当用户输入0 0
时,程序结束。
Program P7.2
#include <stdio.h>
int main() {
int n1, n2;
int max(int, int);
printf("Enter two whole numbers: ");
scanf("%d %d", &n1, &n2);
while (n1 != 0 || n2 != 0) {
printf("The bigger is %d\n", max(n1, n2));
printf("Enter two whole numbers: ");
scanf("%d %d", &n1, &n2);
}
} //end main
int max(int a, int b) {
if (a > b) return a;
return b;
} //end max
以下是 P7.2 的运行示例:
Enter two whole numbers:
24 33
The bigger is 33
Enter two whole numbers:
10 -13
The bigger is 10
Enter two whole numbers:
-5 -8
The bigger is -5
Enter two whole numbers:
0 7
The bigger is 7
Enter two whole numbers:
0 0
为了从main
调用max
,我们必须使用函数原型在main
中“声明”max
int max(int, int);
这表示max
接受两个整数参数并返回一个整数值。
在main
中声明的变量n1
和n2
被认为属于main
。
程序运行时,假设n1
为24
,n2
为33
。当从printf
内用max(n1, n2)
调用该函数时,会发生以下情况:
- 确定自变量
n1
和n2
的值。这些分别是24
和33
。 - 每个值都被复制到一个临时内存位置。这些位置被传递给函数
max
,其中24
用第一个参数a
标记;并且33
标有第二参数b
。我们可以这样描述: - 执行
if
语句;由于a
(24
)不大于b (33
,控制转到语句return b;
,并且33
作为函数值返回。这个值被返回到调用max
的地方(printf
语句)。 - 就在函数返回之前,包含参数的位置被丢弃。由
max
(在我们的例子中是33
)返回的值替换了对max
的调用。因此,max(n1, n2)
被33
和printf
印刷所取代
The bigger is 33
当函数返回值时,在需要值的情况下使用该值是有意义的。上面,我们打印了值。我们也可以将值赋给一个变量,如
big = max(n1, n2);
或者将它用作表达式的一部分,如
ans = 2 * max(n1, n2);
没有意义的是在语句中单独使用它,因此:
max(n1, n2); //a useless statement
在这里,该值没有以任何方式被使用,因此该语句毫无意义。这就好像我们在一行上单独写了一个数字,就像这样
33; //a useless statement
当你调用一个返回值的函数时,请仔细考虑。在你的头脑中要非常清楚你打算用这个值做什么。
如上所述,max
返回两个整数中较大的一个。如果我们想找到两个数字中较大的一个呢?我们可以用max
吗?不幸的是,没有。如果我们用double
值作为参数调用max
,当一个double
数字被赋给一个int
参数时,我们可能会得到奇怪的结果。
另一方面,如果我们用double
参数和double
返回类型编写max
,它将同时适用于double
和int
参数,因为我们可以将int
值赋给double
参数而不会丢失任何信息。
但是,请注意,如果我们用两个字符参数调用max
,它将返回两个代码中较大的一个。例如,max('A', 'C')
将返回C
的代码67
。
Exercise
编写函数来返回两个整数和两个浮点数中较小的一个。
7.5 打印日期
让我们编写一个程序,请求一个从 1 到 7 的数字,并打印出星期几的名称。例如,如果用户输入5
,程序打印Thursday
。程序 P7.3 使用一系列if...else
语句来完成这项工作。
Program P7.3
#include <stdio.h>
int main() {
int d;
printf("Enter a day from 1 to 7: ");
scanf("%d", &d);
if (d == 1) printf("Sunday\n");
else if (d == 2) printf("Monday\n");
else if (d == 3) printf("Tuesday\n");
else if (d == 4) printf("Wednesday\n");
else if (d == 5) printf("Thursday\n");
else if (d == 6) printf("Friday\n");
else if (d == 7) printf("Saturday\n");
else printf("Invalid day\n");
}
现在假设打印一周中某一天的名称是一个更大的程序的一小部分。我们不想让这段代码变得杂乱无章,也不想在每次需要打印一天的名字时都重写这段代码。如果我们能写下printDay(n)
并打印出合适的名字,那就更好了。如果我们写一个函数printDay
来完成这项工作,我们就能做到这一点。
首先要问的是printDay
完成工作需要哪些信息。答案是它需要当天的数字。这立刻暗示了printDay
必须用当天的数字作为参数来写。除此之外,函数体将包含与程序 P7.3 基本相同的代码。此外,printDay
不返回值,因此其“返回类型”为void
。
void printDay(int d) {
if (d == 1) printf("Sunday\n");
else if (d == 2) printf("Monday\n");
else if (d == 3) printf("Tuesday\n");
else if (d == 4) printf("Wednesday\n");
else if (d == 5) printf("Thursday\n");
else if (d == 6) printf("Friday\n");
else if (d == 7) printf("Saturday\n");
else printf(“Invalid day\n”);
}
Tip
当我们写函数时,我们可以为参数使用任何我们想要的变量名。我们永远不必担心函数将如何被调用。很多初学者误以为用printDay(n)
调用函数,那么头中的参数一定是n
。但这不可能是真的,因为它可以用printDay(4)
或printDay(n)
或printDay(j)
甚至printDay(n + 1)
来称呼。这取决于调用函数。
我们需要知道的是,无论参数的值是什么,该值都将被赋给d
(或者我们碰巧用作参数的任何变量),并且函数将在假设参数(在我们的例子中是d
)具有该值的情况下执行。
我们现在将程序 P7.3 重写为 P7.4,以说明该函数如何适合整个程序以及如何使用它。
Program P7.4
#include <stdio.h>
int main() {
int n;
void printDay(int);
printf("Enter a day from 1 to 7: ");
scanf("%d", &n);
printDay(n);
} //end main
void printDay(int d) {
if (d == 1) printf("Sunday\n");
else if (d == 2) printf("Monday\n");
else if (d == 3) printf("Tuesday\n");
else if (d == 4) printf("Wednesday\n");
else if (d == 5) printf("Thursday\n");
else if (d == 6) printf("Friday\n");
else if (d == 7) printf("Saturday\n");
else printf("Invalid day\n");
} //end
printDay
既然我们已经将打印委托给了一个函数,请注意main
是如何变得不那么杂乱的。然而,我们确实需要为main
中的printDay
编写函数原型,这样就可以从main
中调用printDay
。这是原型:
void printDay(int);
与所有 C 程序一样,执行从main
中的第一条语句开始。这将提示用户输入一个数字,然后程序通过调用函数printDay
继续打印当天的名称。
运行示例如下:
Enter a day from 1 to 7:
4
Wednesday
在main
中,假设n
的值为4
。调用printDay(n)
执行如下:
- 确定自变量
n
的值。就是4
。 - 值
4
被复制到临时存储位置。这个位置被传递给函数printDay
,在这里用参数名d
标记。实际上,d
被设置为参数的值。 - 执行函数的主体。在这种情况下,由于
d
是4
,语句printf("Wednesday\n")
将被执行。 - 打印
Wednesday
后,函数完成。包含自变量的位置被丢弃,并且控制返回到调用printDay(n)
之后的语句的main
。在这种情况下,没有更多的语句,所以程序结束。
7.6 最高公因数
在第五章中,我们编写了程序 P5.2,读取两个数,找到它们的最高公因数(HCF)。你应该看一看这个节目来恢复记忆。
如果每当我们想找到两个数字的 HCF(比如说,m
和n
)时,我们可以调用函数hcf(m, n)
来得到答案,那就太好了。例如,调用hcf(42, 24)
将返回答案6
。为了能够做到这一点,我们编写如下函数:
//returns the hcf of m and n
int hcf(int m, int n) {
while (n != 0) {
int r = m % n;
m = n;
n = r;
}
return m;
} //end hcf
查找 HCF 的逻辑与程序 P5.2 中使用的逻辑相同。不同之处在于m
和n
的值将在函数被调用时传递给函数。在 P5.2 中,我们提示用户输入m
和n
的值,并使用scanf
获取它们。
假设用hcf(42, 24)
调用函数。会发生以下情况:
- 每个参数都被复制到一个临时内存位置。这些位置被传递给函数 hcf,其中 42 用第一个参数 m 标记,24 用第二个参数 n 标记。我们可以将此描述为:
- 执行 while 循环,计算出 HCF。在退出循环时,HCF 存储在 m 中,此时将包含 6。这是函数返回到调用它的地方的值。
- 就在函数返回之前,包含参数的位置被丢弃;然后,控制返回到发出调用的地方。
程序 P7.5 通过读取数字对并打印每对的 HCF 来测试函数。对hcf
的调用在printf
语句中进行。如果任一数字小于或等于0
,程序停止。
Program P7.5
#include <stdio.h>
int main() {
int a, b;
int hcf(int, int);
printf("Enter two positive numbers: ");
scanf("%d %d", &a, &b);
while (a > 0 && b > 0) {
printf("The HCF is %d\n", hcf(a, b));
printf("Enter two positive numbers: ");
scanf("%d %d", &a, &b);
}
} //end main
//returns the hcf of m and n
int hcf(int m, int n) {
while (n != 0) {
int r = m % n;
m = n;
n = r;
}
return m;
} //end hcf
以下是 P7.5 的运行示例:
Enter two positive numbers:
42 24
The HCF is 6
Enter two positive numbers:
32 512
The HCF is 32
Enter two positive numbers:
100 31
The HCF is 1
Enter two positive numbers:
84 36
The HCF is 12
Enter two positive numbers:
0 0
我们再次强调,即使函数是用参数m
和n
编写的,它也可以用任意两个整数值来调用——常量、变量或表达式。特别是,它不必用名为m
和n
的变量来调用。在我们的程序中,我们用a
和b
来称呼它。
我们提醒您,为了在main
中使用hcf
,我们必须使用函数原型“声明”它
int hcf(int, int);
如果您愿意,您可以将main
中的两个int
声明写成一个:
int a, b, hcf(int, int);
7.6.1 使用 HCF 查找 LCM
算术中的一个常见任务是找到两个数的最小公倍数(LCM)。例如,8 和 6 的 LCM 是 24,因为 24 是能整除 8 和 6 的最小数。
如果我们知道这两个数字的 HCF,我们可以通过将这两个数字相乘并除以它们的 HCF 来找到 LCM。给定 8 和 6 的 HCF 为 2,我们可以通过算出
找到它们的 LCM
也就是 24。总的来说,
LCM(m, n) = (m x n) / HCF(m, n)
知道了这一点,我们可以很容易地编写一个函数lcm
,给定两个参数m
和n
,返回m
和n
的 LCM。
//returns the lcm of m and n
int lcm(int m, int n) {
int hcf(int, int);
return (m * n) / hcf(m, n);
} //end lcm
既然lcm
使用了hcf
,我们必须通过写它的原型来声明hcf
。我们把它作为一个练习,让你写一个程序来测试lcm
。记得在你的程序中包含hcf
函数。您可以将hcf
放在lcm
之前或之后。
7.7 阶乘
到目前为止,我们已经编写了几个函数,说明了在编写和使用函数时需要了解的各种概念。我们现在写另一个并详细讨论它,加强我们到目前为止遇到的一些概念并引入新的概念。
在我们写函数之前,让我们先写一个程序,它读取一个整数 n 并打印 n!(n 阶乘)其中
0! = 1
n! = n(n - 1)(n - 2)...1 for n > 0
比如 5!= 5.4.3.2.1 = 120.
该程序将基于以下算法:
set nfac to 1
read a number, n
for h = 2 to n do
nfac = nfac * h
endfor
print nfac
用n
的值3
模拟运行该算法,并说服自己它将打印出3!
的值6
。当n
为0
或1
时,检查它是否产生正确的答案。(提示:当n
为0
或1
时,不执行for
循环。)
该算法不会验证n
的值。例如,n
不应该是负数,因为没有为负数定义阶乘。有趣的是,如果n
为负,算法会输出什么?(提示:不执行for
循环。)为了简单起见,我们的程序 P7.6 不验证n
。
Program P7.6
#include <stdio.h>
int main() {
int nfac = 1, n;
printf("Enter a positive whole number: ");
scanf("%d", &n);
for (int h = 2; h <= n; h++)
nfac = nfac * h;
printf("%d! = %d\n", n, nfac);
}
该程序的运行示例如下所示:
Enter a positive whole number:
4
4! = 24
我们现在考虑编写一个函数(我们称之为factorial
)的问题,给定一个整数n
,计算并返回n!
的值。由于n!
是一个整数,所以函数的“返回类型”是int
。
我们首先编写函数头。这是
int factorial(int n)
有趣的是,函数头是我们正确使用函数所需的所有信息。暂时忽略阶乘的其余部分,我们可以这样使用它:
printf("5! = %d\n", factorial(5));
或者像这样:
scanf("%d", &num);
printf("%d! = %d\n", num,factorial(num));
在后一种情况下,如果 num 为 4,printf 将打印:
4! = 24
调用factorial(num)
将值24
直接返回给printf
语句。
按照程序 P7.6 的逻辑,我们编写函数factorial
如下:
int factorial(int n) {
int nfac = 1;
for (int h = 2; h <= n; h++)
nfac = nfac * h;
return nfac;
} //end factorial
比较程序 P7.6 和函数是值得的:
- 程序提示并读取
n
的值;当函数被调用时,函数得到一个值n
,就像在factorial(4)
中一样。试图在此函数中读取n
的值是错误的。 - 除了
n
,程序和函数都需要变量nfac
和h
来表达它们的逻辑。 - 程序和函数计算阶乘的逻辑是相同的。
- 程序打印答案(在
nfac
);该函数将答案(在nfac
中)返回给调用函数。答案回到了factorial
被调用的地方。
对factorial
的其他评论
- 在函数中声明的变量被称为函数的局部变量。因此,
nfac
是一个局部变量,用于保存阶乘。有趣的是,h
对于for
语句是局部的。当factorial
被调用时,存储器被分配给nfac
和h
。这些变量用来计算阶乘。就在函数返回之前,nfac
和h
被丢弃。 - 如果
n
是0
或1 (
,也就是说,它返回1
,你应该验证这个函数是否正常工作。
我们现在详细看看调用factorial
时会发生什么(比如说从main
)。考虑以下陈述(m
和fac
是int
):
m = 3;
fac = factorial(m);
第二条语句执行如下:
- 确定自变量
m
的值;就是3
。 - 这个值被复制到一个临时内存位置,这个位置被传递给函数。该函数用参数名
n
对其进行标记。净效果就好像函数的执行是从语句n = 3;
开始的 - 在编程术语中,我们说参数
m
是“通过值”传递的参数的值被复制到一个临时位置,这个临时位置被传递给函数。该函数无法访问原始参数。在本例中,factorial
无法访问m
,因此不能以任何方式影响它。 - 在
n
被赋予值3
后,阶乘的执行如上所述进行。就在函数返回之前,n
占用的存储位置被丢弃。实际上,参数n
被视为局部变量,只是它被初始化为所提供参数的值。 - 该函数返回的值是存储在
nfac
中的最后一个值。在本例中,分配给nfac
的最后一个值是6
。因此,值6
被返回到发出调用factorial(3)
的地方。 - 将
factorial
返回的值6
赋给fac
。 - 下一条语句(如果有)将继续执行。
使用阶乘
我们通过编写一个完整的程序 P7.7 来说明如何使用阶乘。对于 n = 0,1,2,3,4,5,6 和 7。
Program P7.7
#include <stdio.h>
int main() {
int factorial(int);
printf(" n n!\n\n");
for (int n = 0; n <= 7; n++)
printf("%2d %5d\n", n, factorial(n));
} //end
main
int factorial(int n) {
int nfac = 1;
for (int h = 2; h <= n; h++)
nfac = nfac * h;
return nfac;
} //end factorial
运行时,该程序打印以下内容:
n n!
0 1
1 1
2 2
3 6
4 24
5 120
6 720
7 5040
如你所见,阶乘的值增加得非常快。甚至 8!= 40320,太大了,不适合 16 位整数(可以存储的最大值是 32767)。作为练习,编写从0
到8
的循环,看看会发生什么。
让我们仔细看看main
。第一条语句是factorial
的函数原型。这是必要的,因为factorial
将从main
被调用。
当执行main
时,
printf
打印标题- 使用
n
执行for
循环,假设值为0
、1
、2
、3
、4
、5
、6
、7
。对于n
的每个值,factorial
以n
为自变量被调用。阶乘被计算并返回到printf
中调用它的地方。
我们特意在main
中使用了一个名为n
的变量来说明这个n
并不(也不能)与factorial
的参数n
冲突。假设main
中的n
存储在存储单元865
中,其值为3
。调用factorial(n)
将n
即3
的值存储在一个临时位置(比如说472
)中,并且这个临时位置被传递给factorial
,在那里它被称为n
。这一点说明如下:
我们现在有两个位置叫做n
。在factorial
时,n
是指位置472
;在main
时,n
是指位置865
;factorial
无法访问位置865
。
这里没有发生,但是如果factorial
要改变n
的值,那么位置472
中的值将被改变;位置865
中的值不会受到影响。当factorial
结束时,位置472
被丢弃——即n
不再存在。
从另一个角度来看,factorial
不知道用来调用它的实际参数,因为它只看到参数的值,而不是它是如何被导出的。
我们用main
中的n
作为循环变量来说明上面的观点。然而,我们可以使用任何变量。特别是,我们可以使用h
,这样就不会与函数factorial
的局部变量h
发生冲突。当在factorial
时,h
指局部变量;在main
中,h
是指main
中声明的h
。
组合
假设一个委员会有 7 个人。可以组成多少个 3 人小组委员会?答案用 7 C 3 表示,计算如下:
这使我们的值为 35。我们说 7 个物体有 35 种组合,每次取 3 个。
一般来说, n C r 表示一次取 r 个对象的组合数,由公式
计算
使用factorial
,我们可以编写一个函数combinations
,给定n
和r
,返回一次获取的n
对象的组合数量r
。这是:
int combinations(int n, int r) {
int factorial(int);
return factorial(n) / (factorial(n-r) * factorial(r));
} //end combinations
主体由factorial
的函数原型和一个return
语句组成,其中包含对factorial
的 3 次调用。
我们顺便注意到,这可能是最简单的,但不是最有效的评估方法。例如,如果我们手工计算 7 C 3 ,我们会使用:
而不是
该函数使用的。作为练习,写一个计算组合的有效函数。
为了在一个完整的程序中显示函数factorial
和combinations
以及如何使用它们,我们编写了一个程序来读取n
和r
的值,并打印一次从n
对象获取的组合数r
。
程序 P7.8 展示了它是如何完成的。
Program P7.8
#include <stdio.h>
int main() {
int n, r, nCr, factorial(int), combinations(int, int);
printf("Enter values for n and r: ");
scanf("%d %d", &n, &r);
while (n != 0) {
nCr = combinations(n, r);
if (nCr == 1)
printf("There is 1 combination of %d objects taken "
"%d at a time\n\n", n, r);
else
printf("There are %d combinations of %d objects taken "
"%d at a time\n\n", nCr, n, r);
printf("Enter values for n and r: ");
scanf("%d %d", &n, &r);
}
} //end main
int factorial(int n) {
int nfac = 1;
for (int h = 2; h <= n; h++)
nfac = nfac * h;
return nfac;
} //end factorial
int combinations(int n, int r) {
int factorial(int);
return factorial(n) / (factorial(n-r) * factorial(r));
} //end combinations
程序读取n
和r
的值,并打印组合数。这样做,直到为n
输入了0
的值。以下是运行示例:
Enter values for n and r:
7 3
There are 35 combinations of 7 objects taken 3 at a time
Enter values for n and r:
5 2
There are 10 combinations of 5 objects taken 2 at a time
Enter values for n and r:
6 6
There is 1 combination of 6 objects taken 6 at a time
Enter values for n and r:
3 5
There are 0 combinations of 3 objects taken 5 at a time
Enter values for n and r:
0 0
观察使用if...else
让程序“说出”正确的英语。在语句中,还要注意如何将一个长字符串分成两部分,并将每一部分放在一行中。回想一下,在 C 中,字符串常量的左引号和右引号必须在同一行。当程序被编译时,这些片段将被连接在一起,并作为一个字符串存储在内存中。
7.8 工作费用
在程序 4.6 中,我们读取了工作小时数和零件成本,并计算了一项工作的成本。让我们写一个函数,给定工作时间和零件成本,返回工作的成本。这是:
#define ChargePerHour 100
#define MinJobCost 150
double calcJobCost(double hours, double parts) {
double jobCharge;
jobCharge = hours * ChargePerHour + parts;
if (jobCharge < MinJobCost) return MinJobCost;
return jobCharge;
} //end calcJobCost
当我们说一个函数被给定了一些数据,这立刻意味着这些数据应该被定义为函数的参数。函数的逻辑与程序的逻辑相同。在这里,参数列表指示了当函数被调用时,什么数据将被提供给函数。此外,我们必须指定函数的返回类型;因为作业成本是一个double
值,所以是double
。
当函数被调用时,如
jobCost = calcJobCost(1.5, 87.50);
参数hours
设置为1.5
,parts
设置为87.50
;然后使用这些hours
和parts
的值执行函数体。
作为练习,编写一个完整的程序来读取工作时间和零件成本的几个值,并为每一对打印工作成本。
7.9 计算工资
在程序 P4.7 中,我们读取hours
和rate
的值,并计算净工资。所有的代码都是用main
写的。我们现在编写一个函数,给定hours
和rate
的值,返回如 4.3.1 节所述计算的净工资值。该函数如下所示。
#define MaxRegularHours 40
#define OvertimeFactor 1.5
double calcNetPay(double hours, double rate) {
if (hours <= MaxRegularHours) return hours * rate;
return MaxRegularHours * rate +
(hours - MaxRegularHours) * rate * OvertimeFactor;
} //end
CalcNetPay
如果hours
小于等于MaxRegularHours
,则执行第一个return
;如果为假,则执行第二个return
。注意,这里不需要else
。如果第一个return
被执行,我们退出该函数,第二个return
不能被执行。
如果我们想知道某人以每小时 12 美元的价格工作了 50 个小时的净工资,我们所要做的就是调用calcNetPay(50, 12.00)
。
作为练习,编写一个完整的程序来读取姓名、工作时间和工资率的几个值;并且,打印每个人收到的净工资。提示:学习计划 P5.8。
7.10 整除因子之和
让我们写一个函数来返回给定整数的整除因子之和。我们假设除数包括 1,但不包括给定的数。例如,50 的精确除数是 1、2、5、10 和 25。他们的总和是 43。该函数如下所示。
//returns the sum of the exact divisors of n
int sumDivisors(int n) {
int sumDiv = 1;
for (int h = 2; h <= n / 2; h++)
if (n % h == 0) sumDiv += h;
return sumDiv;
} //end sumDivisors
sumDiv
用于保存整除因子的和;它被设置为1
,因为1
总是一个精确的除数。- 其他可能的约数有
2
、3
、4,
等等,直到n/2
。for
循环依次检查每一项。 - 如果
h
是n
的整除因子,那么n
除以h
的余数就是0
,也就是说n % h
就是0
。如果是这样,h
加到sumDiv
上。 - 最后一条语句将
sumDiv
的值返回到调用sumDivisors
的地方。
在下一个例子中,我们将看到如何使用sumDivisors
。
对数字进行分类
正整数可以根据它们的整除因子的和来分类。如果 n 是整数,s 是它的整除因子之和(包括 1 但不包括 n ),则:
- 如果 s < n,则 n 亏;例如 15(约数 1,3,5;总和 9)
- 如果 s = n,n 是完美的;例如 28(约数 1,2,4,7,14;总和 28)
- 如果 s > n,则 n 是丰富的;例如 12(约数 1,2,3,4,6;总和 16)
让我们编写程序 P7.9 来读取几个数字,并打印出每个数字是不足的、完美的还是丰富的。
Program P7.9
#include <stdio.h>
int main() {
int num, sumDivisors(int);
printf("Enter a number: ");
scanf("%d", &num);
while (num != 0) {
int sum = sumDivisors(num);
if (sum < num) printf("Deficient\n\n");
else if (sum == num) printf("Perfect\n\n");
else printf("Abundant\n\n");
printf("Enter a number: ");
scanf("%d", &num);
}
} //end main
//returns the sum of the exact divisors of n
int sumDivisors(int n) {
int sumDiv = 1;
for (int h = 2; h <= n / 2; h++)
if (n % h == 0) sumDiv += h;
return sumDiv;
} //end sumDivisors
注意,我们调用sumDivisors
一次(针对每个数字)并将结果存储在sum
中。当我们需要“约数之和”而不是每次都重新计算时,我们使用sum
。
以下是程序 P7.9 的运行示例:
Enter a number:
15
Deficient
Enter a number:
12
Abundant
Enter a number:
28
Perfect
Enter a number:
0
作为练习,写一个程序找出所有小于 10,000 的完全数。
7.11 一些字符函数
在这一节中,我们编写了几个与字符相关的函数。
也许最简单的是一个以字符为自变量的函数;如果字符是数字,它返回1
,否则返回0
。(回想一下,在 C 语言中,零值被解释为false
,非零值被解释为true
。)这个描述表明我们必须编写一个函数,它接受一个char
参数并返回一个int
值。我们就叫它isDigit
。这是:
int isDigit(char ch) {
return ch >= '0' && ch <= '9';
} //end isDigit
如果ch
位于'0'
和'9'
之间,则布尔表达式(ch >= '0' && ch <= '9')
为true
;也就是说,如果ch
包含一个数字。因此,如果ch
包含一个数字,函数返回1
(代表true
);如果ch
不包含数字,则返回0
(代表false
)。
我们可以将函数体写成
if (ch >= '0' && ch <= '9') return 1;
return 0;
但是上面使用的单个return
语句是首选方式。
类似地,我们可以编写函数isUpperCase,
,如果它的参数是大写字母,则返回1
,如果不是,则返回0
,因此:
int isUpperCase(char ch) {
return ch >= 'A' && ch <= 'Z';
} //end isUpperCase
接下来我们有函数isLowerCase,
,如果它的参数是小写字母,则返回1
,如果不是,则返回0
。
int isLowerCase(char ch) {
return ch >= 'a' && ch <= 'z';
} //end isLowerCase
如果我们想知道字符是否是一个字母(大写或小写),我们可以用isUpperCase
和isLowerCase
写isLetter,
。
int isLetter(char ch) {
int isUpperCase(char), isLowerCase(char);
return isUpperCase(ch) || isLowerCase(ch);
} //end isLetter
注意,我们需要包含isUpperCase
和isLowerCase
的函数原型。
7.11.1 字母表中字母的位置
让我们写一个函数,给定一个字符,如果它不是英文字母表中的一个字母,返回0
;否则,它返回字母在字母表中的位置(一个整数值)。无论字符是大写字母还是小写字母,该函数都应该工作。例如,给定'T'
或't'
,函数应该返回20
。
该函数接受一个char
参数并返回一个int
值。使用函数isUpperCase
和isLowerCase
,我们编写函数(我们称之为position
)如下:
int position(char ch) {
int isUpperCase(char), isLowerCase(char);
if (isUpperCase(ch)) return ch - 'A' + 1;
if (isLowerCase(ch)) return ch - 'a' + 1;
return 0;
} //end position
我们用isUpperCase
和isLowerCase
来确立我们拥有什么样的性格。如果都不是,控制转到最后一条语句,我们返回0
。
如果我们有一个大写字母,我们通过从字母的代码中减去A
的代码来找到字母和A
之间的距离。例如,A
和A
之间的距离是0
,A
和F
之间的距离是5
。添加1
给出字母在字母表中的位置。在这里,添加1
得到A
的1
和F
的6
。
如果我们有一个小写字母,我们通过从字母的代码中减去a
的代码来得到字母和a
之间的距离。例如,a
和b
之间的距离是1
,a
和z
之间的距离是25
。添加1
给出字母在字母表中的位置。在这里,添加1
得到b
的2
和z
的26
。
为了说明如何使用该函数,我们编写了程序 P7.10,它读取一行输入;对于行中的每个字符,如果不是字母,它将打印出0
,如果是字母,则打印出它在字母表中的位置。
Program P7.10
#include <stdio.h>
int main() {
char c;
int position(char);
printf("Type some letters and non-letters and press 'Enter'\n");
while ((c = getchar()) != '\n')
printf("%c%2d\n", c, position(c));
} //end main
int isUpperCase(char ch) {
return ch >= 'A' && ch <= 'Z';
} //end isUpperCase
int isLowerCase(char ch) {
return ch >= 'a' && ch <= 'z';
} //end isLowerCase
int position(char ch) {
int isUpperCase(char), isLowerCase(char);
if (isUpperCase(ch)) return ch - 'A' + 1;
if (isLowerCase(ch)) return ch - 'a' + 1;
return 0;
} //end isPosition
以下是 P7.10 的运行示例:
Type some letters and non-letters and press "Enter"
FaT($hY``&
F 6
a 1
T 20
( 0
$ 0
h 8
Y 25
&  0
n 14
我们已经编写了函数isDigit
、isUpperCase
、isLowerCase,
和isLetter
来说明关于角色函数的基本概念。然而,C 提供了许多预定义的函数(实际上是宏,但区别对我们来说并不重要)来处理字符。其中有isdigit
(测试数字)、isupper
(测试大写字母)、islower
(测试小写字母)和isalpha
(测试字母)。要使用这些函数,您需要放置指令
#include <ctype.h>
在你项目的最前面。作为练习,使用isupper
和islower
重写 P7.10。没有isUpperCase
、isLowerCase
和它们的原型,你的程序会短很多。
7.12 获取下一个整数
之前我们编写了程序 P6.13,一个字符一个字符的读取数据,构造并存储在变量中找到的下一个整数,最后打印出这个整数。
现在让我们编写一个函数getInt
,它逐字符读取数据并返回找到的下一个整数。该函数不接受任何参数,但括号仍必须写在名称之后。代码与 P6.13 中的基本相同,除了我们使用了预定义的函数isdigit
。这里是getInt
:
int getInt() {
char ch = getchar();
// as long as the character is not a digit, keep reading
while (!isdigit(ch)) ch = getchar() ;
// at this point, ch contains the first digit of the number
int num = 0;
while (isdigit(ch)) { // as long as we get a digit
num = num * 10 + ch - '0'; // update num
ch = getchar();
}
return num;
} //end getInt
注意到
while (ch < '0' || ch > '9')
程序 P6.13 的替换为
while (!isdigit(ch))
和
while (ch >= '0' && ch <= '9')
被替换为
while (isdigit(ch))
我们相信这使得程序更具可读性。
该函数需要变量ch
和num
来完成它的工作;ch
保存数据中的下一个字符,num
保存到目前为止构建的数字。我们在函数中声明它们,使它们成为局部变量。这样,它们就不会与程序中其他地方声明的同名变量发生冲突。这使得函数是独立的——它不依赖于其他地方声明的变量。
该函数可以在中使用
id = getInt();
这将从输入中获取下一个正整数,不管它前面有多少个字符和什么类型的字符,并将它存储在id
中。回想一下,scanf("%d", &id)
只有在下一个整数前面有零个或多个空白字符时才起作用。我们的getInt
比较笼统。
我们通过重写程序 P4.2 来测试它,该程序请求以米和厘米为单位给出的两个长度,并求出总和。我们注意到数据必须只用数字输入。例如,如果我们输入了3m 75cm
,我们会得到一个错误,因为3m
不是一个有效的整数常量。使用getInt
,我们将能够在3m 75cm
表格中输入数据。新程序显示为程序 P7.11
Program P7.11
//find the sum of two lengths given in meters and centimeters
#include <stdio.h>
#include <ctype.h>
int main() {
int m1, cm1, m2, cm2, mSum, cmSum, getInt();
printf("Enter first length: ");
m1 = getInt();
cm1 = getInt();
printf("Enter second length: ");
m2 = getInt();
cm2 = getInt();
mSum = m1 + m2; //add the meters
cmSum = cm1 + cm2; //add the centimeters
if (cmSum >= 100) {
cmSum = cmSum - 100;
mSum = mSum + 1;
}
printf("\nSum is %dm %dcm\n", mSum, cmSum);
} //end main
int getInt() {
char ch = getchar();
// as long as the character is not a digit, keep reading
while (!isdigit(ch)) ch = getchar() ;
// at this point, ch contains the first digit of the number
int num = 0;
while (isdigit(ch)) { // as long as we get a digit
num = num * 10 + ch - '0'; // update num
ch = getchar();
}
return num;
} //end getInt
示例运行如下:
Enter first length:
3m 75cm
Enter second length:
5m 50cm
Sum is 9m 25cm
我们鼓励您执行以下操作:
- 修改
getInt
,使其适用于负整数。 - 编写一个函数
getDouble,
,返回输入中的下一个浮点数。即使下一个数字不包含小数点,它也应该工作。
EXERCISES 7Explain why functions are important in writing a program. Given the function header void test(int n)
explain carefully what happens when the call test(5)
is made. Given the function header double fun(int n)
explain carefully what happens when the following statement is executed: printf("The answer is %f\n", fun(9));
Given the function header void test(int m, int n, double x)
say whether each of the following calls is valid or invalid. If invalid, state why. test(1, 2, 3);
test(-1, 0.0, 3.5);
test(7, 2);
test(14, '7', 3.14);
Write a function sqr,
which given an integer n
, returns n2
. Write a function isEven,
which given an integer n
, returns 1
if n
is even and 0
if n
is odd. Write a function isOdd,
which given an integer n
, returns 1
if n
is odd and 0
if n
is even. Write a function isPerfectSquare,
which given an integer n
, returns 1
if n
is a perfect square (e.g., 25, 81) and 0
if it is not. Use only elementary arithmetic operations. Hint: Try numbers starting at 1
. Compare the number times itself with n
. Write a function isVowel,
which given a character c
, returns 1
if c
is a vowel and 0
if it is not. Write a function, which given an integer n
, returns the sum 1 + 2 +...+ n
Write a function, which given an integer n
, returns the sum 1
2
+ 2
2
+...+ n
2
Write a function, which given three integer values representing the sides of a triangle, returns:
0
如果值不能是任何三角形的边。如果任何一个值为负或零,或者任何一条边的长度大于或等于其他两条边的长度之和,就会出现这种情况。1
如果三角形是不规则的(所有边都不同)。2
如果三角形是等腰的(两边相等)。3
如果三角形是等边的(三条边相等)。
Write a function, which given three integer values representing the sides of a triangle, returns 1
if the triangle is right angled and 0
if it is not. Write a function power,
which given a double
value x
and an integer n
, returns xn
. Using the algorithm of problem 10, Exercises 4, write a function, which given a year between 1900 and 2099, returns an integer value indicating the day on which Easter Sunday falls in that year. If d
is the day of the month, return d
if the month is March and -d
if the month is April. For example, if the year is 1999, return -4
since Easter Sunday fell on April 4 in 1999. Assume that the given year is valid. Write a program, which reads two years, y1
and y2
, and, using the function above, prints the day on which Easter Sunday falls for each year between y1
and y2
. Given values for month
and year
, write a function to return the number of days in the month. Write a function numLength,
which given an integer n
, returns the number of digits in the integer. For example, given 309
, the function returns 3
. Write a function max3,
which given 3 integers, returns the biggest. Write a function isPrime
, which given an integer n, returns 1 if n is a prime number and 0 if it is not. A prime number is an integer > 1, which is divisible only by 1 and itself. Using isPrime
, write a program to prompt for an even number n greater than 4 and print all pairs of prime numbers that add up to n. Print an appropriate message if n is not valid. For example, if n is 22, your program should print 3 19
5 17
11 11
You are required to generate a sequence of integers from a given positive integer n, as follows. If n is even, divide it by 2. If n is odd, multiply it by 3 and add 1. Repeat this process with the new value of n, stopping when n = 1. For example, if n is 13, the following sequence will be generated: 13 40 20 10 5 16 8 4 2 1
Write a function, which given n, returns the length of the sequence generated, including n and 1. For n = 13, your function should return 10
. Using the function, write a program to read two integers m and n (m < n), and print the maximum sequence length for the numbers between m and n, inclusive. Also print the number that gives the maximum length. For example, if m = 1 and n = 10, your program should print 9 generates the longest sequence of length 20
We can code the 52 playing cards using the numbers 1 to 52. We can assign 1 to the Ace of Spades, 2 to the Two of Spades, and so on, up to 13 to the King of Spades. We can then assign 14 to the Ace of Hearts, 15 to the Two of Hearts, and so on, up to 26 to the King of Hearts. Similarly, we can assign the numbers 27–39 to Diamonds and 40–52 to Clubs. Write a function, which given integers rank
and suit
, returns the code for that card. Assume rank
is a number from 1
to 13
with 1
meaning Ace and 13
meaning King; suit
is 1
, 2
, 3,
or 4
representing Spades, Hearts, Diamonds, and Clubs, respectively.
八、数组
在本章中,我们将解释以下内容:
- 什么是数组以及如何声明数组
- 如何在数组中存储值
- 如何使用
for
循环将已知数量的值读入数组 - 如何使用
for
循环处理数组元素 - 如何使用
while
循环将未知数量的值读入数组 - 如何从带下标的数组中提取所需的元素
- 如何计算数组中存储的数字之和
- 如何计算存储在数组中的数字的平均值
- 如何使用数组保存多个计数
- 如何使用作为字符数组的
string
- 如何反转数组中的元素
- 如何编写一个函数来判断一个短语是否是回文
- 如何将数组作为参数传递给函数
- 如何找到数组中的最大值和最小值
8.1 简单变量与数组变量
到目前为止我们一直在使用的变量(如ch
、n
、sum
)通常被称为简单变量。在任何给定的时间,一个简单的变量可以用来存储一项数据:例如,一个数字或一个字符。当然,如果我们愿意,存储在变量中的值是可以改变的。但是,在许多情况下,我们希望存储一组相关的项目,并能够通过一个公共名称来引用它们。数组变量允许我们这样做。
例如,假设我们希望存储学生在一次测试中的 60 个分数的列表。我们可以通过发明 60 个不同的 int 变量并将一个分数存储在一个变量中来实现。但是编写代码来操作这 60 个变量将是非常乏味、麻烦、笨拙和耗时的。(想想你会如何给这 60 个变量赋值。)如果我们需要处理 200 个分数呢?
更好的方法是使用一个数组来存储 60 个分数。我们可以认为这个数组有 60 个“位置”——我们使用一个位置来存储一个元素,在本例中是一个分数。为了表示特定的分数,我们使用下标。例如,如果score
是数组的名称,那么score[5]
指的是位置5
中的分数——这里5
用作下标。写在方括号内,[
和]
。
一般来说,数组可以用来存储相同类型的值的列表;例如,我们称之为整数数组、字符数组、字符串数组或浮点数数组。正如您将看到的,使用数组允许我们以简单、系统的方式处理列表,不管它有多大。我们可以使用一个简单的循环来处理全部或部分项目。我们还可以做一些事情,比如在列表中搜索一个项目,或者按照升序或降序对列表进行排序。
8.2 数组声明
在使用数组之前,必须声明它。例如,考虑以下语句:
int score[60];
这声明了 score 是一个“整数数组”或一个下标从0
到59
的“?? 数组”。数组声明由以下部分组成
- 类型(本例中为 int)
- 数组的名称(本例中为 score)
- 一个左方括号,[
- 数组的大小(本例中为 60)
- 一个右方括号,]
在 C 语言中,如果n
是数组的大小,数组下标从0
开始,一直到n-1
。
我们可以认为这个声明创建了 60 个int
变量,这些变量可以被数组变量score
共同引用。为了引用这些分数中的一个特定分数,我们在数组名后使用一个写在方括号中的下标。在这个例子中,
score[0]
指第一次得分
score[1]
指第二个分数
score[2]
指第三个分数
。
。
score[58]
指第 59 分
score[59]
指第 60 分
如你所见,数组下标在 C 中有点笨拙;如果score[i]
是指第i
个分数,那就更好了(也更符合逻辑)。我们将很快看到如何解决这个问题。
试图引用下标允许范围之外的元素是错误的。如果您这样做,您将得到一个“数组下标”错误。例如,您不能引用score[60]
、score[-1]
和score[99]
,因为它们不存在。
下标可以用常量(如25
)、变量(如n
)或表达式(如i+1
)来写。下标的值决定了引用的是哪个元素。
在我们的例子中,数组的每个元素都是一个int
,可以像普通的int
变量一样以任何方式使用。特别是,一个值可以存储在其中,它的值可以打印出来,并且可以与另一个int
进行比较。
我们可以把score
描绘成图 8-1 。
图 8-1。
Declaration of int score[60]
就像一个简单的变量,当一个数组被声明时,它的元素的值保持未定义,直到我们执行在其中存储值的语句。这将在接下来的 8.3 节中讨论。
再举一个例子,假设我们需要存储 100 件商品的商品编号(整数)和价格(浮点数)。我们可以用一个数组(item
)保存商品编号,用另一个数组(price
)保存价格。这些可以用这个来声明:
int item[100];
double price[100];
item
的元素范围从item[0]
到item[99]
,而price
的元素范围从price[0]
到price[99]
。当我们在这些数组中存储值时(见下一步),我们将确保
price[0]
持有item[0]
的价格;
price[1]
持有item[1]
的价格;
总的来说,
price[i]
持有价格of item[i]
。
8.3 将值存储在数组中
考虑数组score
。如果愿意,我们可以将选定的元素设置为特定值,如下所示:
score[3] = 56;
score[7] = 81;
但是如果我们希望将 60 个位置设置为 60 个分数呢?我们需要像下面这样写 60 条语句吗?
score[0] = 45;
score[1] = 63;
score[2] = 39;
.
.
score[59] = 78;
这当然是完成工作的一种方式,但是它非常乏味、耗时而且不灵活。更简洁的方法是让下标是变量而不是常量。例如,score[h]
可以用来引用位置h
的分数;哪个分数取决于h
的价值。如果h
的值是47
,那么score[h]
指的是score[47]
,位置47
的分数。
注意,score[h]
可以通过简单地改变h
的值来引用另一个分数,但是,在任何时候,score[h]
都是指一个特定的分数,由h
的当前值决定。
假设 60 个分数存储在一个文件scores.txt
中。下面的代码将读取 60 个分数,并将它们存储在数组score
中:
FILE * in = fopen("scores.txt", "r");
for (int h = 0; h < 60; h++)
fscanf(in, "%d", &score[h]);
假设文件scores.
txt
以下列数据开始:
45 63 39 ...
执行for
循环时,h
的值范围为0
到59
:
- 当
h
为0
时,第一个分数45
被读取并存储在score[0]
中; - 当
h
为1
时,第二个分数63
被读取并存储在score[1]
中; - 当
h
为2
时,第三个分数39
被读取并存储在score[2]
中;
依此类推,直到
- 当
h
为59
时,第 60 个分数被读取并存储在score[59]
中。
注意,这个方法比写 60 条赋值语句要简洁得多。我们使用一种说法
fscanf(in, "%d", &score[h]);
在 60 个不同的地方储存分数。这通过改变下标h
的值来实现。这种方法也更加灵活。如果我们必须处理 200 个分数,比方说,我们只需要在score
的声明和for
语句中将 60 个分数改为 200 个分数(并在数据文件中提供 200 个分数)。前面的方法需要我们写 200 条赋值语句。
如果我们希望在阅读时打印分数,我们可以像这样编写for
循环:
for (int h = 0; h < 60; h++) {
fscanf(in, "%d", &score[h]);
printf("%d\n", score[h]);
}
另一方面,如果我们希望在分数被读取并存储在数组中之后打印分数,我们可以编写另一个for
循环:
for (h = 0; h < 60; h++)
printf("%d\n", score[h]);
我们使用了用于读取分数的同一个循环变量h
。但是并不要求我们这样做。任何其他循环变量都会有同样的效果。例如,我们可以这样写:
for (int x = 0; x < 60; x++)
printf("%d\n", score[x]);
重要的是下标的值,而不是用作下标的变量。
我们经常需要将一个数值数组的所有元素设置为0
。这可能是必要的,例如,如果我们要用它们来保存总数,或者作为计数器。例如,要将 score 的 60 个元素设置为0
,我们可以写:
for (int h = 0; h < 60; h++)
score[h] = 0;
for
循环执行 60 次,其中h
取值为0
至59
:
- 第一次循环时,
h
是0
,所以score[0]
被设置为0
。 - 第二次循环时,
h
是1
,所以score[1]
被设置为0
。
以此类推,直到
- 第 60 次循环时,
h
为59
,因此score[59]
被设置为0
。
如果我们想将元素设置为不同的值(-1
),我们可以这样写:
for (int h = 0; h < 60; h++)
score[h] = -1;
应该注意的是,尽管我们已经声明score
的大小为 60,但并不要求我们使用所有的元素。例如,假设我们只想将 score 的前 20 个元素设置为0
,我们可以通过以下方式实现:
for (int h = 0; h < 20; h++)
score[h] = 0;
这设置了元素score[0]
、score[1]
、score[2]
,直到score[19]
到0
。元素score[20]
至score[59]
仍未定义。
c 提供了另一种初始化数组的方法——在声明中。考虑一下这个:
int score[5] = {75, 43, 81, 52, 68};
这将score
声明为大小为5
的数组,并将score[0]
设置为75
、score[1]
设置为43
、score[2]
设置为81
、score[3]
设置为52
以及score[4]
设置为68
。
初始值用大括号括起来,并用逗号分隔。最后一个值后面不需要逗号,但是放一个逗号也不是错误。
如果提供的值少于5
个,那么0
将用于填充数组。例如,《宣言》
int score[5] = {75, 43};
将score[0]
设置为75
、score[1]
设置为43
、score[2]
设置为0
、score[3]
设置为0
、score[4]
设置为0
。
如果提供了超过5
的值,您会得到一个警告或错误,这取决于您的编译器设置。例如,由于有8
值,下面将生成一个警告或错误:
int score[5] = {75, 43, 81, 52, 68, 49, 66, 37};
可以省略数组的大小,例如这样写:
int score[] = {75, 43, 81, 52, 68, 49, 66, 37};
在这种情况下,编译器计算值的数量来确定数组的大小。这里,值的个数是8
,所以就好像我们写了这个声明一样:
int score[8] = {75, 43, 81, 52, 68, 49, 66, 37};
作为另一个例子,假设我们想存储闰年中一个月的天数。我们可以用这个:
int month[] = {31,29,31,30,31,30,31,31,30,31,30,31};
这将把month[0]
设置为31
,month[2]
设置为29
,等等。,我们必须记住,month[0]
指的是一月,month[1]
指的是二月,以此类推。我们可以通过使用以下方法来解决这个问题:
int month[] = {0,31,29,31,30,31,30,31,31,30,31,30,31};
现在,month[1]
是31
并且指代一月,month[2]
是29
,指代二月,以此类推——这比之前的声明更自然。元素month[0]
是0
,但是我们忽略了它(见下一条)。
8.3.1 关于不使用元素0
正如我们已经看到的,当我们不得不说“第三个元素存储在位置 2”时,从元素0
开始可能有点尴尬和不自然;下标与元素的位置“不同步”。说“第一个元素存储在位置 1”或“第五个元素存储在位置 5”会更合理、更符合逻辑
对于这种情况,最好忽略元素0
,假设下标从1
开始。但是,您必须声明数组的大小比您实际需要的大 1。例如,如果我们想要满足 60 个分数,我们将不得不声明score
为
int score[61];
这创建了元素score[0]
到score[60]
。我们可以忽略score[0]
,只用score[1]
到score[60]
。不得不声明一个额外的元素,这是以一种更自然和更符合逻辑的方式处理我们的问题的小小代价。
有时候使用位置0
的数组会更好。但是,当它不是的时候,我们将声明我们的数组大小比需要的大一,并忽略位置0
中的元素。更好的编程实践是使用该语言来满足您的目的,而不是将自己局限于该语言的特性。
假设我们要满足 60 分的要求。实现这一点的一个好方法如下:
#define MaxScores 60
...
int score[MaxScores + 1];
我们现在可以处理元素score[1]
到score[MaxScores]
。
8.4 平均值和与平均值的差异
考虑寻找一组数字(整数)的平均值以及每个数字与平均值的差值的问题。为了求平均值,我们需要知道所有的数字。在第 5.3.1 节中,我们看到了如何通过一次读取和存储一个数字来求平均值。每读一个新的数字都取代了前一个。最后,我们可以计算平均值,但是我们丢失了所有的数字。
现在,如果我们还想知道每个数字与平均值相差多少,我们需要存储原始数字,以便在计算平均值后可以使用它们。我们将把它们存储在一个数组中。该计划将基于以下假设:
- 将提供不超过 100 个号码;需要此信息来声明数组的大小;
- 数字将由
0
终止;假设0
不是其中一个数字。
下面显示了我们希望该程序如何工作:
Enter up to 100 numbers (end with 0)
2 7 5 3 0
Numbers entered: 4
Sum of numbers: 17
The average is 4.25
Numbers and differences from average
2 -2.25
7 2.75
5 0.75
3 -1.25
程序 P8.1 显示了如何编写这样的程序。
Program P8.1
//find average and difference from average
#include <stdio.h>
#define MaxNum 100
int main() {
int a, num[MaxNum];
int n = 0;
double sum = 0;
printf("Enter up to %d numbers (end with 0)\n", MaxNum);
scanf("%d", &a);
while (a != 0) {
sum += a;
num[n++] = a; //store in location n, then add 1 to n
scanf("%d", &a);
}
if (n == 0) printf("No numbers entered\n");
else {
printf("\nNumbers entered: %d\n", n);
printf("Sum of numbers: %1.0f\n\n", sum);
double average = sum / n;
printf("The average is %3.2f\n", average);
printf("\nNumbers and differences from average\n");
for (int h = 0; h < n; h++)
printf("%4d %6.2f\n", num[h], num[h] - average);
}
}
关于程序 P8.1 的注意事项:
- 使用
#define
,我们将符号常量MaxNum
设置为100
;我们用它来声明数组并在提示符下输入数字。这使得程序很容易修改,如果我们改变主意,希望迎合不同数量的数字。 - 当读取的数字不是
0
时,我们进入while
循环。在循环内部,我们将它添加到总和中,存储在数组中,并对其进行计数。每次我们到达循环的末尾,n
的值就是到目前为止数组中存储的数字的数量。 - 在从
while
循环退出时,我们测试n
。如果仍然是0
,那么没有提供数字,也没有其他事情可做。如果是0
,程序不会犯试图除以n
的错误。如果n
是正的,我们自信地用总和除以它来求平均值。 for
循环“遍历”数组,打印数字以及它们与平均值的差值。这里,n
是实际使用的数组元素的数量,不一定是整个数组。使用的元素有num[0]
到num[n-1]
。- 这个程序计算出读取的数字的总和。如果我们需要找到存储在数组中的前
n
个元素的和,我们可以用下面的代码来做:sum = 0;
for(int h = 0; h < n; h++) sum += num[h];
程序 P8.1 完成基础工作。但是如果用户输入了超过 100 个数字呢?回想一下,正如声明的那样,num
的元素范围从num[0]
到num[99]
。
现在假设n
是100
,这意味着100
数字已经存储在数组中。如果输入了另一个,并且不是0
,程序将进入while
循环并尝试执行该语句
num[n++] = a;
由于n
是100
,这现在与
num[100] = a;
但是没有元素num[100]
——你会得到一个“数组下标”错误。当开始使用数组时,必须非常小心,不要让程序逻辑超出下标的范围。如果是这样,你的程序就会崩溃。
为了适应这种可能性,我们可以将while
条件写成
while (a != 0 && n < MaxNum) { ...
如果n
等于MaxNum
( 100
,意味着我们已经在数组中存储了100
个值,没有空间再存储了。在这种情况下,循环条件将是false
,不会进入循环,程序也不会试图在数组中存储另一个值。
这是另一个防御性编程的例子:试图让我们的程序不受外力影响。现在,用户操作没有办法通过超出数组的边界来导致程序崩溃。
8.5 字母频率计数
让我们写一个程序来计算输入中每个字母的出现频率。该程序将把一个大写字母和它的小写字母视为同一个字母;例如,E
和e
递增同一个计数器。
在程序 P7.10 中,我们写了一个函数,position
,给定一个字符,如果该字符不是字母,则返回0
;如果它是一个字母,它返回它在字母表中的位置。我们将使用position
来解决这个问题。然而,我们将使用预定义的角色函数isupper
和islower
重写它。
为了解决这个问题,我们需要保留 26 个计数器,每个计数器代表字母表中的一个字母。我们需要一个用于a
和A
的计数器,一个用于b
和B
的计数器,一个用于c
和C
的计数器,等等。我们可以声明 26 个变量,称为a
、b
、c
、...
,直到z
;a
保存a
和A
的计数,b
保存b
和B
的计数,依此类推。而且,在我们的程序中,我们可以编写以下形式的语句(假设ch
包含下一个字符):
if (ch == 'a' || ch == 'A') a++;
else if (ch == 'b' || ch == 'B') b++;
else if (ch == 'c' || ch == 'C') c++;
else if ...
这很快就会令人厌倦。而且我们要打印结果的时候也会有类似的问题。不得不处理 26 个变量来解决这样一个小问题既不合适也不方便。正如我们将看到的,数组让我们更容易解决这个问题。
我们需要一个包含 26 个元素的int
数组来保存字母表中每个字母的计数。因为使用元素 1(而不是元素 0)来保存a
和A
的计数,使用元素 2(而不是元素 1)来保存b
和B
的计数,等等,更自然,所以我们将数组letterCount
声明为
int letterCount[27];
我们将忽略letterCount[0]
并使用以下内容:
letterCount[1]
保存a
和A
的计数letterCount[2]
保存b
和B
的计数letterCount[3]
保存c
和C
的计数- 等等。
letterCount[26]
保存z
和Z
的计数
完整的程序如程序 P8.2 所示。它从文件passage.txt
中读取数据,并将输出发送到文件output.txt
。
Program P8.2
#include <stdio.h>
#include <ctype.h>
int main() {
char ch;
int n, letterCount[27], position(char);
FILE * in = fopen("passage.txt", "r");
FILE * out = fopen("output.txt", "w");
for (n = 1; n <= 26; n++) letterCount[n] = 0; //set counts to 0
while ((ch = getc(in)) != EOF) {
n = position(ch);
if (n > 0) ++letterCount[n];
}
//print the results
fprintf(out, "Letter Frequency\n\n");
for (n = 1; n <= 26; n++)
fprintf(out, "%4c %8d\n", 'a' + n - 1, letterCount[n]);
fclose(in);
fclose(out);
} //end main
int position(char ch) {
if (isupper(ch)) return ch - 'A' + 1;
if (islower(ch)) return ch - 'a' + 1;
return 0;
} //end position
假设passage.txt
包含以下内容:
The quick brown fox jumps over the lazy dog.
If the quick brown fox jumped over the lazy dog then
Why did the quick brown fox jump over the lazy dog?
程序 P8.2 将以下输出发送到文件output.txt
:
Letter Frequency
a 3
b 3
c 3
d 6
e 11
f 4
g 3
h 8
i 5
j 3
k 3
l 3
m 3
n 4
o 12
p 3
q 3
r 6
s 1
t 7
u 6
v 3
w 4
x 3
y 4
z 3
当读取一个字符ch
时,我们调用函数position
,就像这样:
n = position(ch);
如果n
大于0
,我们知道ch
包含一个字母,n
是该字母在字母表中的位置。例如,如果ch
包含Y
,那么n
就是25
,因为Y
是字母表中的第 25 个字母。如果我们将1
加到letterCount[n]
上,我们将1
加到ch
包含的字母的计数上。这里,如果我们将1
加到letterCount[25]
上,我们将1
加到Y
的计数上。下面的语句完成了这项工作:
if (n > 0) ++letterCount[n];
看看打印一行输出的fprintf
语句:
fprintf(out, "%4c %8d\n", 'a' + n - 1, letterCount[n]);
这将打印一个字母(小写)及其计数。让我们看看如何。'a'
的代码是97
。当n
为1
时,
'a' + n - 1
被评价为97+1-1
,也就是97
;当97
被印上%c
时,它被解释为一个字符,所以字母a
被印上。当n
为2
时,
'a' + n - 1
被评价为97+2-1
,也就是98
;当98
被印上%c
时,它被解释为一个字符,所以b
被印上。当n
为3
时,
'a' + n - 1
被评价为97+3-1
,也就是99
;当99
被印上%c
时,它被解释为一个字符,所以c
被印上。等等。随着n
呈现从1
到26
的值,
'a' + n - 1
将承担从'a'
到'z'
的字母代码。
有趣的是,我们可以使用前面描述的以下特殊形式的for
语句来获得相同的结果。这是:
for (ch = 'a', n = 1; n <= 26; ch++, n++)
fprintf(out, "%4c %8d\n", ch, letterCount[n]);
从1
到26
的n
循环仍然被执行。但是,与n
同步,它也随着ch
从'a'
到'z'
而被执行。注意使用ch++
移动到下一个字符。
8.6 更好地利用fopen
考虑以下陈述:
FILE * in = fopen("passage.txt", "r");
这表示“打开文件passage.txt
进行读取”它假设已经创建了文件,并且在其中存储了适当的数据。但是,如果用户忘记创建文件或者把文件放在了错误的位置(例如,放在了错误的文件夹中),该怎么办呢?我们可以使用fopen
来检查这一点。如果fopen
找不到文件,它返回预定义的值NULL
(在stdio.h
中定义)。我们可以按如下方式对此进行测试:
FILE * in = fopen("passage.txt", "r");
if (in == NULL) {
printf("File cannot be found\n");
exit(1);
}
如果in
为NULL
,程序打印一条信息并停止。如果in
不是NULL
,程序照常进行。
预定义函数exit
用于终止程序的执行,并将控制返回给操作系统。通常使用exit(0)
表示正常终止;其他参数用于指示某种错误。
要使用exit
,我们必须编写指令
#include <stdlib.h>
在我们程序的开头,由于exit
是在“标准库”中定义的,stdlib.h
。其中,这个库包含处理随机数的函数、搜索函数和排序函数。
像往常一样,我们可以给in
赋值,并用下面的方法测试NULL
:
FILE * in;
if ((in = fopen("passage.txt", "r")) == NULL) {
printf("File cannot be found\n");
exit(1);
}
注意,我们不能在if
条件中使用FILE * in
,因为这里不允许声明。
同样,当我们写作时
FILE * out = fopen("output.txt", "w");
我们假设文件output.txt
存在或者可以被创建。如果它不存在并且不能被创建(例如,磁盘可能被写保护或者已满),fopen
将返回NULL
。我们可以按如下方式对此进行测试:
FILE * out;
if ((out = fopen("output.txt", "w")) == NULL) {
printf("File cannot be found or created\n");
exit(1);
}
到目前为止,我们已经在fopen
语句中写入了我们的文件名。要使用不同的文件,我们必须改变语句中的名称,并且必须重新编译程序。如果我们让用户在程序运行时告诉我们文件名,我们的程序会更加灵活。
我们可以声明dataFile
(说)来保存文件名
char dataFile[40];
您可以将40
更改为您想要的任何尺寸。如果in
已经被声明为FILE *
,我们可以提示用户输入文件名并测试是否一切正常:
printf("Enter name of file: ");
scanf("%s", dataFile);
if ((in = fopen(dataFile, "r")) == NULL) {
printf("File cannot be found\n");
exit(1);
}
因为我们使用%s
来读取文件的名称,所以名称不能包含空格。如果您的文件名可能包含空格,您可以使用gets
。
8.7 作为函数参数的数组
在第七章中,我们看到了参数是如何传递给函数的。在 C # 中,参数是“按值”传递的当参数“通过值”传递时,会用参数的值创建一个临时位置,并将该临时位置传递给函数。该函数永远无法访问原始参数。
我们还看到,例如,当我们使用gets(item)
将一个字符串读入字符数组item
时,该函数能够将该字符串放入参数item
。这意味着该函数可以访问实际的参数,不涉及副本。
在 C 语言中,数组名表示其第一个元素的地址。当我们使用一个数组名作为一个函数的参数时,第一个元素的地址被传递给这个函数,因此它可以访问这个数组。
我们现在仔细看看用数组参数编写函数所涉及的一些问题。
我们将编写一个函数sumList
,它返回传递给函数的数组中整数的和。例如,如果数组包含以下内容:
该函数应该返回24
。
我们可以这样写函数头:
int sumList(int num[])
数组参数就像数组声明一样编写,但是没有指定大小。但是,方括号必须存在,以便与简单的参数区分开来。例如,如果我们写了 int num
,这将意味着num
是一个普通的int
变量。
如果愿意,可以使用常量、符号常量或任何可以在程序编译时计算的整数表达式来指定大小。(C99 和更高版本的 C 允许可变长度数组,其中数组下标可以在运行时指定。我们将在 9.4.1 节看到一个例子。)然而,如果你不这样做,你的程序将会更加灵活。
现在,假设score
在main
中被声明为
int score[10];
我们打电话
sumList(score);
我们可以简单的认为,在函数中,score
是由名字num
知道的;对num
的任何引用都是对原始参数score
的引用。
更精确的解释是这样的:由于名称score
表示score[0]
的地址,这个地址被传递给函数,在那里它成为num
、num[0]
的第一个元素的地址。事实上,任何地址都可以传递给函数,在那里它将被当作num[0]
的地址。
该函数可以随意为num
设定任何大小。显然,如果我们试图处理不存在的数组元素,这会给我们带来麻烦。因此,告诉函数要处理多少个元素是很好的编程习惯。我们使用另一个参数来实现这一点,如:
int sumList(int num[], int n)
现在,调用函数可以通过为n
提供一个值来告诉sumList
要处理多少个元素。使用score
的宣言,如上,调用
sumList(score, 10);
告诉函数处理score
(整个数组)的第一个10
元素。但是,这也是这种方法的优势所在:我们也可以进行这样的调用
sumList(score, 5);
让函数处理score
的第一个5
元素。
使用这个函数头,我们写sumList
如下:
int sumList(int num[], int n) {
int sum = 0;
for (int h = 0; h < n; h++) sum += num[h];
return sum;
}
该函数使用一个for
循环从num[0]
到num[n-1]
遍历数组。每次通过循环,它都向sum
添加一个元素。从循环中退出时,sum
的值作为函数值返回。
构造
for (h = 0; h < n; h++)
通常用于处理数组的第一个n
元素。
要使用该功能,请考虑main
中的以下代码:
int sumList(int[], int), score[10];
for (int h = 0; h < 5; h++) scanf("%d", &score[h]);
printf("Sum of scores is %d\n", sumList(score, 5));
像往常一样,任何想要使用sumList
的函数都必须使用函数原型来声明它。注意使用int[]
来表示第一个参数是一个整数数组。如果我们愿意,我们可以在声明原型时使用标识符,如下所示:
int sumList(int list[], int);
实际使用的标识符并不重要。我们可以用任何有效的标识符代替list
。
for
循环将5
值读入数组。注意,由于数组元素就像一个普通的变量,我们必须在scanf
中写入&score[h]
,以将一个值读入score[h]
。
假设读入score
的值如下:
在printf
中,调用
sumList(score, 5)
会得到函数返回score
的前5
个元素之和:也就是24
。你现在应该知道了,为了找到第一个3
元素的和,我们可以写
sumList(score, 3)
8.8 字符串–字符数组
在 2.7 节中,我们向您展示了如何在“字符数组”中存储字符串现在我们对数组有了一些了解,我们可以解释字符串实际上是如何存储的。
在 C # 中,字符串存储在字符数组中。字符串中的每个字符存储在数组中的一个位置,从位置0
开始。空字符0
放在最后一个字符之后。这样做是为了让程序知道什么时候到达了字符串的末尾。例如,字符串
"Enter rate:"
存储如下(表示空格):
(当然,在计算机内部,每个字符都用它的数字代码来表示,用二进制表示。)
空字符串,一个没有字符的字符串,被写成""
(两个连续的双引号)并像这样存储:
字符串常量"a"
存储如下:
这不要和字符常量'a'
混淆,它有一个数值(它的整数代码值)与之关联,可以用在算术表达式中。没有与字符串"a."
相关联的数值
我们可以使用关系运算符==,!=,和> =,但是我们不能这样比较两个字符串,即使是像"a"
和"h,"
这样的单字符字符串。为了比较两个字符串,我们可以使用标准的字符串函数strcmp
。
假设我们打算在变量name
中存储一个名字,声明为
char name[25];
如果我们使用
gets(name);
或者
scanf("%s", name);
c 将把\0
放在存储的最后一个字符之后。(这被称为用\0
正确终止字符串。)我们必须确保数组中有足够的空间来存储\0
。因此,如果我们声明一个大小为25
的数组,我们最多可以在其中存储一串24
字符,因为我们必须为\0
保留一个位置。
例如,假设Alice Wonder
被键入以响应gets(name)
。数组name
看起来像这样(只显示使用过的位置):
因为name
是一个数组,如果我们愿意,我们可以处理单个字符。例如,name[0]
指的是第一个字符,name[1]
指的是第二个,以此类推。一般来说,我们可以用name[i]
来指代i
位置上的人物。正如我们已经看到的,我们可以使用name
本身来引用存储在数组中的字符串。
字符串的长度定义为其中的字符数,不算\0
。预定义的字符串函数strlen
将一个字符数组作为其参数,并返回存储在其中的字符串的长度。在这个例子中,strlen(name)
将返回12
,"Alice Wonder."
中的字符数,作为一个兴趣点,strlen
从数组的开头开始计算字符数,直到找到\0
;\0
不算。
事实上,所有标准的字符串函数(如strlen
、strcpy
、strcat,
和strcmp
)都假设我们给它们的字符串以\0
结束。否则,将会出现不可预测的结果。想象一下会发生什么,例如,如果我们给strlen
一个字符数组,但是没有\0
来表示字符串的结束。它将永远继续寻找\0
。
当我们编写如下语句时:
char name[25] = "Alice Wonder";
或者
strcpy(name, "Alice Wonder");
c 将在最后一个字符后存储\0
,所以我们不必担心。
但是,如果我们自己把字符存储在一个数组里,一定要小心,要在末尾加上\0
。如果我们打算对字符串使用任何标准的字符串函数,或者如果我们打算用%s
打印它,这是非常重要的。例如,考虑以下代码:
char word[10];
int n = 0;
char ch = getchar();
while (!isalpha(ch)) ch = getchar(); //read and ignore non-letters
while (isalpha(ch)) {
word[n++] = ch;
ch = getchar();
}
word[n] = '\0';
代码从输入中读取字符,并存储在数组word
中找到的第一个单词。这里,单词被定义为任何连续的字母字符串。第一个while
循环读取所有非字母字符。当它找到第一个字母字符时退出。只要读取的字符是字母,就执行第二个while
循环。它使用n
遍历数组中的位置,从位置0
开始。在退出该循环时,\0
被存储在位置n
,因为此时,n
表示最后一个字母被存储的位置。
举例来说,假设数据是:
123$#%&First Caribbean7890
第一个while
循环将读取字符,直到到达F
,因为F
是数据中的第一个字母字符。第二个循环将存储
F in word[0]
i in word[1]
r in word[2]
s in word[3]
t in word[4]
由于n
在每个字符被存储后递增,所以在这个阶段n
的值是5
。当t
之后的空间被读取时,while
循环退出,并且\0
被存储在word[5]
中,适当地终止字符串。数组word
将如下所示:
我们现在可以将word
与任何标准字符串函数一起使用,并可以使用%s
打印它,如下所示:
printf("%s", word);
%s
到达\0
时将停止打印字符。
上面的代码并不完美——我们使用它主要是为了说明的目的。由于word
的大小为10
,我们可以在其中存储最多9
个字母(加上\0
)。如果下一个单词长于 9 个字母(例如,serendipity
,代码将试图访问不存在的word[10]
,给出一个“数组下标”错误。
作为一个练习,考虑你将如何处理比你所能应付的更长的单词。(提示:在word[n]
中存储任何内容之前,请检查n
是否有效。)
为了说明如何处理字符串中的单个字符,我们编写了一个函数numSpaces
,来计算并返回字符串str
中的空格数:
int numSpaces(char str[]) {
int h = 0, spaces = 0;
while (str[h] != '\0') {
if (str[h] == ' ') spaces++;
h++;
}
return spaces;
} //end numSpaces
考虑代码:
char phrase[] = "How we live and how we die";
printf("Number of spaces is %d\n", numSpaces(phrase));
第一条语句创建了一个大小正好可以容纳字符串加上\0
的数组。由于短语包含26
字符(字母和空格),数组phrase
的大小为 27,其中phrase[0]
包含H
, phrase[25]
包含e
,phrase[26]
包含\0
。
在printf
中,调用numSpaces(phrase)
将把控制转移给函数,其中phrase
将被称为str
。在该函数中,while
循环将遍历数组,直到到达\0
。对于每个字符,它将检查它是否是一个空格。如果是,1
加到spaces
上。在退出循环时,spaces
的值作为函数值返回。对于样本phrase
,返回值将为6
。
有趣的是,while
循环的主体可以写成:
if (str[h++] == ' ') spaces++;
这里,h
在我们测试了str[h]
是否包含空格后递增。
练习
Write a function to return the number of digits in a string str
. Write a function to return how many vowels there are in a string str
. Hint: it would be useful to write a function isVowel
that, given a character ch
, returns 1
if ch
is a vowel and 0
if it is not.
8.8.1 反转字符串中的字符
作为另一个例子,我们编写代码来反转字符串str
中的字符。比如str
包含lived
,我们就必须把它改成devil
。为了说明代码将如何工作,我们将str
描述如下:
我们将首先交换str[0]
、l
和str[4]
、d
,假设:
接下来,我们将交换str[1]
、i
和str[3]
、e
,给出这个:
str[2]
已经就位(中间的字母不动),所以没有更多的事情要做,该方法以反转的str
结束。
看起来我们将需要两个变量:一个将采用从0
开始并递增的下标值,而另一个将采用从length(str)-1
开始并递减的下标值。我们称它们为lo
和hi
。最初,我们会将lo
设置为0
,将hi
设置为length(str)-1
。
该算法的基本思想如下:
1\. set lo to 0
2\. set hi to length(str)-1
3\. exchange the characters in positions lo and hi
4\. add 1 to lo
5\. subtract 1 from hi
6\. repeat from step 3
我们什么时候停止?当没有更多的角色可以交换时,我们可以停止。这将在lo
大于或等于hi
时发生。或者换句话说,只要lo
小于hi
,我们就必须不停地交换角色。我们现在可以将算法编写如下:
set lo to 0
set hi to length(str) - 1
while lo < hi do
exchange the characters in positions lo and hi
add 1 to lo
subtract 1 from hi
endwhile
在这种形式下,很容易转换成 C 如下(假设c
为char
):
lo = 0;
hi = strlen(str) - 1;
while (lo < hi) {
c = str[lo];
str[lo] = str[hi];
str[hi] = c;
lo++; hi--;
}
然而,我们可以利用for
语句的表达能力,将它写得更简洁,也许更易读,如下所示:
for (lo = 0, hi = strlen(str) - 1; lo < hi; lo++, hi--) {
c = str[lo];
str[lo] = str[hi];
str[hi] = c;
}
交换字符串中的两个字符是我们经常要做的事情。编写一个函数(swap
)来完成这个任务会很方便。当我们调用swap
时,我们会给它字符串和我们想要交换的字符的下标。例如,如果word
是一个char
数组,则调用
swap(word, i, j);
会交换角色word[i]
和word[j]
。因为word
是一个数组,所以原始数组(不是副本)被传递给swap
。当函数交换两个字符时,它是在实际参数中交换它们,word
。
该函数可以写成如下形式:
void swap(char str[], int i, int j) {
char c = str[i];
str[i] = str[j];
str[j] = c;
} //end swap
在函数中,实际的参数(word
,比方说)被称为str
。
使用swap
,我们可以用另一个函数reverse
反转字符,如下所示:
void reverse(char str[]) {
void swap(char [], int, int);
int lo, hi;
for (lo = 0, hi = strlen(str) - 1; lo < hi; lo++, hi--)
swap(str, lo, hi);
} //end reverse
由于reverse
使用swap
,我们必须在reverse
中声明swap
的原型。再次注意,原型类似于函数头,除了我们省略了变量名。然而,如果你愿意,你可以把名字包括进去——任何名字都可以。
使用这些函数,我们编写了程序 P8.3,它读取一个字符串,反转它,并打印它。
Program P8.3
#include <stdio.h>
#include <string.h>
int main() {
char sample[100];
void reverse(char s[]);
printf("Type some data and I will reverse it\n");
gets(sample);
reverse(sample);
printf("%s\n", sample);
} //end main
void reverse(char str[]) {
void swap(char [], int, int);
int lo, hi;
for (lo = 0, hi = strlen(str) - 1; lo < hi; lo++, hi--)
swap(str, lo, hi);
} //end reverse
void swap(char str[], int i, int j) {
char c = str[i];
str[i] = str[j];
str[j] = c;
} //end swap
以下是 P8.3 的运行示例:
Type some data and I will reverse it
Once upon a time
emit a nopu ecnO
反转一个字符串本身似乎并不重要,但是有时候我们需要反转一个数组的元素。例如,我们可以将学生成绩列表存储在一个数组中,并按升序排序,如下所示:
如果我们想让标记按降序排列,我们所要做的就是反转数组,就像这样:
8.9 回文
考虑确定给定字符串是否是回文的问题(向前或向后拼写都一样)。回文的例子(忽略大小写、标点和空格)有:
civic
Race car
Madam, I’m Adam.
A man, a plan, a canal, Panama.
如果所有字母都是相同的大小写(大写或小写),并且字符串(word
)不包含空格或标点符号,我们可以如下解决问题:
assign word to another string, temp
reverse the letters in temp
if temp = word then word is a palindrome
else word is not a palindrome
换句话说,如果一个词的反义词和这个词相同,那就是回文。听起来合乎逻辑且正确。但是,效率不高。让我们看看为什么。
假设这个词是thermostat
。这个方法将反转thermostat
得到tatsomreht
。两者对比告诉我们thermostat
不是回文。但是我们可以更快地得到如下答案:
compare the first and last letters,``t``and
they are the same, so
compare the second and second to last letters,``h``and
these are different so the word is not a palindrome
我们将编写一个名为palindrome
的函数,给定一个字符串word
,如果 word 是回文,则返回1
,如果不是,则返回0
。目前,我们将假设word
全部大写或全部小写,并且不包含空格或标点符号。该功能将基于以下理念:
compare the first and last letters
if they are different, the string is not a palindrome
if they are the same, compare the second and second to last letters
if they are different, the string is not a palindrome
if they are the same, compare the third and third to last letters
诸如此类;我们继续下去,直到我们找到一个不匹配的对(这不是一个回文)或者没有更多的对来比较(这是一个回文)。我们可以用伪代码来表达这个逻辑,如下所示:
set lo to 0
set hi to length(word) - 1
while lo < hi do //while there are more pairs to compare
if word[lo] != word[hi] then return 0 // not a palindrome
//the letters match, move on to the next pair
lo = lo + 1
hi = hi - 1
endwhile
return 1 // all pairs match, it is a palindrome
while
循环比较字母对;如果它发现一个不匹配的对,它立即返回0
。如果所有配对都匹配,当lo
不再小于hi
时,它将以正常方式退出。在这种情况下,它返回1
。
函数回文显示在程序 P8.4 中,它通过读取几个单词并打印每个单词是否是回文来测试它。
Program P8.4
#include <stdio.h>
#include <string.h>
int main() {
char aWord[100];
int palindrome(char str[]);
printf("Type a word. (To stop, press 'Enter' only): ");
gets(aWord);
while (strcmp(aWord, "") != 0) {
if (palindrome(aWord)) printf("is a palindrome\n");
else printf("is not a palindrome\n");
printf("Type a word. (To stop, press 'Enter' only): ");
gets(aWord);
}
} //end main
int palindrome(char word[]) {
int lo = 0;
int hi = strlen(word) - 1;
while (lo < hi)
if (word[lo++] != word[hi--]) return 0;
return 1;
} //end palindrome
在函数中,我们使用单个语句
if (word[lo++] != word[hi--]) return 0;
来表达上述算法中 while 循环体的所有逻辑。由于我们用++
和--
做后缀,word[lo]
和word[hi]
比较后lo
和hi
就变了。
当然,我们可以把它表达为:
if (word[lo] != word[hi]) return 0;
lo++;
hi--;
该程序提示用户键入一个单词,并告诉她这是否是一个回文。然后它会提示输入另一个单词。要停止,用户只需按“输入”或“返回”键。当她这样做时,空字符串被存储在aWord
中。while
条件通过比较aWord
和""
来检查这一点(两个连续的双引号表示空字符串)。以下是程序 P8.4 的运行示例:
Type a word. (To stop, press "Enter" only):
racecar
is a palindrome
Type a word. (To stop, press "Enter" only):
race car
is not a palindrome
Type a word. (To stop, press "Enter" only):
Racecar
is not a palindrome
Type a word. (To stop, press "Enter" only):
DEIFIED
is a palindrome
Type a word. (To stop, press "Enter" only):
请注意,race car
不是回文,因为'e'
与' '
不同,Racecar
也不是回文,因为'R'
与'r'
不同。我们将很快解决这个问题。
8.9.1 更好的回文函数
我们编写的函数适用于全大写或全小写的单词回文。我们现在处理更困难的问题,检查可能包含大写字母、小写字母、空格和标点符号的单词或短语。为了说明我们的方法,请考虑以下短语:
Madam, I’m Adam
我们将把所有的字母转换成一个大小写(比如说小写),去掉所有的空格和非字母,给出
madamimadam
我们现在可以使用我们在程序 P8.4 中编写的函数来测试这是否是一个回文。
让我们编写一个函数lettersOnlyLower
,给定一个字符串短语,将所有字母转换成小写,并删除所有空格和非字母。该函数将转换后的字符串存储在第二个参数中。这是:
void lettersOnlyLower(char phrase[], char word[]) {
int i = 0, n = 0;
char c;
while ((c = phrase[i++]) != '\0')
if (isalpha(c)) word[n++] = tolower(c);
word[n] = '\0';
}
功能注释lettersOnlyLower
i
用于索引给定短语,存储在phrase
中。n
用于索引转换后的短语,存储在word
中。while
循环依次查看phrase
的每个字符。如果是字母,用预定义的函数tolower
转换成小写,存储在word
中的下一个位置;要使用tolower
,您的程序前面必须有指令#include <ctype.h>
- 从
while
退出时,word
与\0
正确终止。
将所有东西放在一起,我们得到程序 P8.5,它测试我们的新函数letterOnlyLower
。
Program P8.5
#include <stdio.h>
#include <string.h>
#include <ctype.h>
int main() {
char aPhrase[100], aWord[100];
void lettersOnlyLower(char p[], char w[]);
int palindrome(char str[]);
printf("Type a phrase. (To stop, press 'Enter' only): ");
gets(aPhrase);
while (strcmp(aPhrase, "") != 0) {
lettersOnlyLower(aPhrase, aWord);
printf("Converted to: %s\n", aWord);
if (palindrome(aWord)) printf("is a palindrome\n");
else printf("is not a palindrome\n");
printf("Type a word. (To stop, press 'Enter' only): ");
gets(aPhrase);
} //end while
} //end main
void lettersOnlyLower(char phrase[], char word[]) {
int j = 0, n = 0;
char c;
while ((c = phrase[j++]) != '\0')
if (isalpha(c)) word[n++] = tolower(c);
word[n] = '\0';
} //end lettersOnlyLower
int palindrome(char word[]) {
int lo = 0;
int hi = strlen(word) - 1;
while (lo < hi)
if (word[lo++] != word[hi--]) return 0;
return 1;
} //end
palindrome
程序提示用户输入一个短语,并告诉她这是否是一个回文。我们还打印了转换后的短语,向您展示该函数是如何工作的。
这里显示了一个运行示例:
Type a phrase. (To stop, press "Enter" only):
Madam I’m Adam
Converted to: madamimadam
is a palindrome
Type a phrase. (To stop, press "Enter" only):
Flo, gin is a sin. I golf.
Converted to: floginisasinigolf
is a palindrome
Type a phrase. (To stop, press "Enter" only):
Never odd or even.
Converted to: neveroddoreven
is a palindrome
Type a phrase. (To stop, press "Enter" only):
Thermostat
Converted to: thermostat
is not a palindrome
Type a phrase. (To stop, press "Enter" only):
Pull up if I pull up.
Converted to: pullupifipullup
is a
palindrome
Type a phrase. (To stop, press "Enter" only):
8.10 字符串数组-重访日名称
在程序 P7.4 中,我们编写了一个函数printDay
,
,它打印出一天的名称,给定一天的数字。我们现在要写一个函数nameOfDay
,它有两个参数:第一个是一天的数字,第二个是一个字符数组。该函数将在数组中存储与日期编号相对应的日期名称。例如,调用
nameOfDay(6, dayName);
会将Friday
存储在dayName
中,假设dayName
是一个字符数组。
我们展示了如何使用一个数组来写nameOfDay
来存储日期的名称。假设我们有一个数组day
,如图 8-2 ( day[0]
未使用,未显示)。
图 8-2。
The array day
如果d
包含从1
到7
的值,那么day[d]
包含对应于d
的日期名称。比如d
是3
,day[d]
包含Tuesday
。但是我们如何在一个数组中存储日期的名称呢?我们需要什么样的阵列?
我们将需要一个数组,其中每个元素可以保存一个字符串-一个字符串数组。但是字符串本身存储在一个字符数组中。所以我们需要一个“字符数组”的数组——我们需要一个二维数组。考虑宣言
char day[8][10];
我们可以认为 day 有 8 行 10 列。如果我们在每行中存储一天的名称,那么我们可以存储 8 个名称。每个名字存储在一个由 10 个字符组成的数组中。行从0
到7
编号,列从0
到9
编号。如上图所示,我们将不使用行0
。我们将把名字存储在行1
到7
中。如果我们将日期的名称存储在这个数组中,它将看起来像这样(我们将空字符串""放入day[0]
):
c 让我们用day[i]
来引用第i
行。如果需要,我们可以用day[i][k]
来指代i
行k
列的人物。比如day[3][2]
就是e
,day[7][4]
就是r
。
我们可以声明数组 day,并用日期的名称初始化它,如下所示:
char day[8][10] = {"", "Sunday", "Monday", "Tuesday",
"Wednesday", "Thursday", "Friday", "Saturday"};
该声明将创建如图 8-3 所示的数组。要放入数组中的字符串用{
和}
括起来,用逗号分隔,最后一个后面没有逗号。第一个字符串(空字符串)放在day[0]
中,第二个放在day[1]
中,第三个放在day[2]
中,依此类推。
图 8-3。
The 2-dimensional array day
完整的功能nameOfDay
如程序 P8.6 所示,其中main
仅用于测试功能。
Program P8.6
#include <stdio.h>
#include <string.h>
int main() {
void nameOfDay(int, char[]);
int n;
char dayName[12];
printf("Enter a day from 1 to 7: ");
scanf("%d", &n);
nameOfDay(n, dayName);
printf("%s\n", dayName);
} //end main
void nameOfDay(int n, char name[]) {
char day[8][10] = {"", "Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday"};
if (n < 1 || n > 7) strcpy(name, "Invalid day");
else strcpy(name, day[n]);
} //end nameOfDay
在函数中,以下语句检查n
的值。
if (n < 1 || n > 7) strcpy(name, "Invalid day");
else strcpy(name, day[n]);
如果n
不是从1
到 7 的值,该功能将Invalid day
存储在name
中。如果是有效的天数,它将day[n]
的值存储在name
中。例如,如果n
为6
,则函数将day[6]
,即Friday
存储在name
中。
在main
中,dayName
被声明为大小为12
,因为如果日期无效,它需要保存字符串"Invalid day"
。
8.11 灵活的getString
功能
到目前为止,我们已经使用格式规范%s
读取不包含空白字符的字符串,使用函数gets
读取直到行尾的字符串。然而,这两者都不允许我们读取由双引号分隔的字符串。假设我们有如下格式的数据:
"Margaret Dwarika" "Clerical Assistant"
我们将无法使用%s
或gets
轻松读取这些数据。
我们将编写一个函数getString
,它让我们读取一个包含在“分隔符”字符中的字符串。例如,我们可以指定一个字符串为$John Smith$
或"John Smith."
,这是一种非常灵活的指定字符串的方式。每个字符串都可以指定自己的分隔符,这些分隔符对于下一个字符串可能是不同的。它对于指定可能包含特殊字符(如双引号)的字符串特别有用,而不必使用像\"
这样的转义序列。
例如,为了在 C 中指定以下字符串:
"Don’t move!" he commanded.
我们必须写:
"\"Don’t move!\" he commanded."
使用getString
,该字符串可以作为
$"Don’t move!" he commanded.$
或者
%"Don’t move!" he commanded.%
或者使用任何其他字符作为分隔符,只要它不是字符串中的字符之一。我们甚至可以使用这样的东西:
7"Don’t move!" he commanded."7
但是通常会使用特殊字符如"
、$
、%
或#
作为分隔符。
我们将用两个参数来编写getString
:一个由in
指定的文件和一个字符数组str
。该函数将从in
中读取下一个字符串,并将其存储在str
中。
该函数假设第一个非空白字符 met ( delim
)是分隔符。读取并存储字符,直到再次遇到delim
,表示字符串结束。分隔符不存储,因为它们不是字符串的一部分。
假设我们在main
中有以下声明:
FILE * input = fopen("quizdata.txt", "r");
char country[50];
并且文件quizdata.txt
包含如上所述分隔的字符串。我们将能够从文件中读取下一个字符串,并将其存储在country
中,如下所示:
getString(input, country);
由我们来确保country
足够大以容纳下一个字符串。否则,程序可能会崩溃或出现无意义的结果。
这里是getString
:
void getString(FILE * in, char str[]) {
//stores, in str, the next string within delimiters
// the first non-whitespace character is the delimiter
// the string is read from the file 'in'
char ch, delim;
int n = 0;
str[0] = '\0';
// read over white space
while (isspace(ch = getc(in))) ; //empty while body
if (ch == EOF) return;
delim = ch;
while (((ch = getc(in)) != delim) && (ch != EOF))
str[n++] = ch;
str[n] = '\0';
} // end getString
对getString
的评论
- 如果预定义函数
isspace
的char
参数是空格、制表符或换行符,则返回1
(真),否则返回0
(假)。 - 如果
getString
在找到非空白字符(分隔符)之前遇到文件尾,那么空字符串将在str
中返回。否则,它通过一次读取一个字符来构建字符串;字符串在下一次出现分隔符或文件结尾时终止,以先出现者为准。 - 我们可以用第一个参数
stdin
调用getString
,从标准输入(键盘)中读取一个字符串。
8.12 地理问答节目
让我们写一个程序,询问用户关于国家和他们的首都。该计划将说明一些有用的编程概念,如从键盘和文件读取,并在用户输入方面非常灵活。下面是程序的运行示例,表明我们希望完成的程序如何工作。用户有两次机会回答一个问题。如果她两次都答错了,程序会告诉她正确的答案。
What is the capital of Trinidad?
Tobago
Wrong. Try again.
What is the capital of Trinidad?
Port of Spain
Correct!
What is the capital of Jamaica?
Kingston
Correct!
What is the capital of Grenada?
Georgetown
Wrong. Try again.
What is the capital of Grenada?
Castries
Wrong. Answer is St. George’s
我们将把这些国家的名称和它们的首都存储在一个文件中。对于每个国家,我们将存储其名称、首都和一个仅由首都字母组成的特殊字符串,全部转换为大写。这最后一个字符串将用于使用户能够非常灵活地键入他们的答案,它将使我们能够编写一个更有效的程序。提供最后一个字符串不是绝对必要的,因为我们可以让程序为我们创建它(参见程序 P8.7 后的注释)。字符串"*"
用于表示数据的结束。下面显示了一些示例数据:
"Trinidad" "Port of Spain" "PORTOFSPAIN"
"Jamaica" "Kingston" "KINGSTON"
"Grenada" "St. George’s" "STGEORGES"
"*"
我们每行显示 3 个字符串,但这不是必需的。唯一的要求是它们以正确的顺序提供。如果你愿意,你可以每行有 1 根弦或 6 根弦,或者每行有不同数量的弦。此外,您可以使用任何字符来分隔字符串,只要它不是字符串中的字符。您可以为不同的字符串使用不同的分隔符。提供以下上述数据是完全可以的:
"Trinidad" $Port of Spain$ *PORTOFSPAIN*
%Jamaica% "Kingston" &KINGSTON&
$Grenada$ %St. George’s% ^STGEORGES^
#*#
我们能做到这一点是因为getString
的灵活性。我们将使用getString
从文件中读取字符串,使用gets
获取用户在键盘上输入的答案。
假设一个国家的数据分别读入变量country
、capital
和CAPITAL
。(记住,在 C 中,capital
是与CAPITAL.
不同的变量)当用户键入一个答案时(answer
,比方说),它必须与capital
进行比较。如果我们用一个简单的比较,比如
if (strcmp(answer, capital) == 0) ...
为了检查answer
是否与capital
相同,那么像"Portof Spain,""port of spain,"" Port ofSpain,"
和"st georges"
这样的答案都会被认为是错误的。如果我们希望这些答案是正确的(我们可能应该这样做),我们必须在比较之前将所有用户的答案转换成一种通用的格式。
我们认为,只要所有的字母都在那里,顺序正确,无论大小写,答案就被认为是正确的。当用户键入答案时,我们忽略空格和标点符号,只将字母转换成大写。然后将其与CAPITAL
进行比较。例如,上面的答案将被转换成"PORTOFSPAIN"
和"STGEORGES"
,并引发"Correct!"
响应。
在回文程序(P8.5)中,我们编写了一个函数lettersOnlyLower
,它只保存字符串中的字母,并将它们转换成小写字母。在这里,我们想要相同的功能,但我们转换成大写。我们将这个函数命名为lettersOnlyUpper
。除了将tolower
替换为toupper
之外,代码与lettersOnlyLower
相同。我们对正确性的测试现在变成了这样:
lettersOnlyUpper(answer, ANSWER);
if (strcmp(ANSWER, CAPITAL) == 0) printf("Correct!\n");
所有细节都记录在 P8.7 程序中。
Program P8.7
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <stdlib.h>
#define MaxLength 50
int main() {
void getString(FILE *, char[]);
void askOneQuestion(char[], char[], char[]);
char EndOfData[] = "*", country[MaxLength+1] ;
char capital[MaxLength+1], CAPITAL[MaxLength+1];
FILE * in = fopen("quizdata.txt", "r");
if (in == NULL){
printf("Cannot find file\n");
exit(1);
}
getString(in, country);
while (strcmp(country, EndOfData) != 0) {
getString(in, capital);
getString(in, CAPITAL);
askOneQuestion(country, capital, CAPITAL);
getString(in, country);
}
} // end main
void askOneQuestion(char country[], char capital[], char CAPITAL[]) {
void lettersOnlyUpper(char [], char[]);
char answer[MaxLength+1], ANSWER[MaxLength+1];
printf("\nWhat is the capital of %s?", country);
gets(answer);
lettersOnlyUpper(answer, ANSWER);
if (strcmp(ANSWER, CAPITAL) == 0) printf("Correct!\n");
else {
printf("Wrong. Try again\n");
printf("\nWhat is the capital of %s?", country);
gets(answer);
lettersOnlyUpper(answer, ANSWER);
if (strcmp(ANSWER, CAPITAL) == 0) printf("Correct!\n");
else printf("Wrong. Answer is %s\n", capital);
}
} // end askOneQuestion
void lettersOnlyUpper(char word[], char WORD[]) {
// stores the letters in word (converted to uppercase) in WORD
int i = 0, n = 0;
char c;
while ((c = word[i++]) != '\0')
if (isalpha(c)) WORD[n++] = toupper(c);
WORD[n] = '\0';
} // end lettersOnlyUpper
void getString(FILE * in, char str[]) {
//stores, in str, the next string within delimiters
// the first non-whitespace character is the delimiter
// the string is read from the file 'in'
char ch, delim;
int n = 0;
str[0] = '\0';
// read over white space
while (isspace(ch = getc(in))) ; //empty while body
if (ch == EOF) return;
delim = ch;
while (((ch = getc(in)) != delim) && (ch != EOF))
str[n++] = ch;
str[n] = '\0';
} // end getString
如前所述,在文件中存储CAPITAL
并不是绝对必要的。我们可以只存储country
和capital
,当这些被读取时,用
lettersOnlyUpper(capital, CAPITAL);
你可以用这个程序的思想写很多类似的。在地理主题上,可以问山和高,河流和长度,国家和人口,国家和首相等等。对于不同的应用程序,您可以使用它来训练用户的英语-西班牙语(或任何其他语言组合)词汇。您的问题可以采取以下形式:
What is the Spanish word for water?
或者,如果你喜欢,
What is the English word for agua?
更好的是,让用户选择是给她英语单词还是西班牙语单词。
你可以询问书籍和作者,歌曲和歌手,电影和明星。作为一个练习,想一想这个程序的思想可以用来测试用户的其他五个领域。
8.13 找出最大的数字
让我们考虑寻找存储在数组中的一组值的最大值的问题。寻找最大值的原则与我们在 5.6 节中讨论的相同。假设整数数组num
包含以下值:
我们可以很容易地看到,最大的数字是 84,它位于位置 4。但是程序如何确定这一点呢?一种方法如下:
- 假设第一个元素(位置
0
的元素)最大;我们通过将big
设置为0
来实现这一点。当我们遍历数组时,我们将使用big
来保存到目前为止遇到的最大数的位置;num[big]
泛指实际人数。 - 接下来,从位置
1
开始,我们查看每个连续位置中的数字,直到6
,并将该数字与位置big
中的数字进行比较。 - 第一次,我们比较
num[1]
和num[0]
;由于num[1]
、72
大于num[0]
、25
,我们将big
更新为1
。这意味着到目前为止最大的数量位于位置1
。 - 接下来我们比较一下
num[2]
、17
、num[big]
(也就是num[1]
)、72
;由于num[2]
小于num[1]
,我们继续下一个数字,将big
留在1
。 - 接下来我们比较一下
num[3]
、43
、num[big]
(也就是num[1]
)、72
;由于num[3]
小于num[1]
,我们继续下一个数字,将big
留在1
。 - 接下来我们比较一下
num[4]
、84
、num[big]
(也就是num[1]
)、72
;由于num[4]
大于num[1]
,我们将big
更新为4
。这意味着到目前为止最大的数字位于位置4
。 - 接下来我们比较一下
num[5]
、14
、num[big]
(也就是num[4]
)、84
;由于num[5]
比num[4]
小,我们继续下一个数字,在4
留下大的。 - 接下来我们比较一下
num[6]
、61
、num[big]
(也就是num[4]
)、84
;由于num[6]
比num[4]
小,我们继续下一个数字,在4
留下大的。 - 由于没有下一个数字,该过程结束,其中
big
的值是最大数字的位置4
。实际数字用num[big]
表示;既然big
是4
,这就是num[4]
,也就是84
。
我们可以用下面的伪代码来表达刚才描述的过程:
big = 0
for h = 1 to 6
if num[h] > num[big] then big = h
endfor
print "Largest is ", num[big], " in position ", big
我们现在将编写一个函数getLargest
,来寻找数组中的最大值。一般来说,我们将指定在数组的哪个部分搜索值。这很重要,因为大多数时候,我们声明一个数组有最大的大小(比如说 ??),但是并不总是把100
的值放在数组中。
当我们声明数组的大小为100
时,我们是在迎合100
值。但是,在任何时候,阵列的容量都可能少于这个数量。我们使用另一个变量(n
,比方说)来告诉我们数组中当前存储了多少个值。例如,如果n
是36
,则表示值存储在数组的元素0
到35
中。
所以当我们寻找最大值时,我们必须指定要搜索数组中的哪些元素。我们将编写这样的函数,它接受三个参数——数组num
和两个整数lo
和hi
——并返回从num[lo]
到num[hi]
的最大数的位置。由调用者来确保lo
和hi
在为数组声明的下标范围内。例如,呼叫
getLargest(score, 0, 6)
将从score[0]
到score[6]
返回最大数的位置;那电话呢getLargest(mark, 10, 20)
将从mark[10]
到mark[20]
返回最大数的位置。
下面是函数,getLargest
:
int getLargest(int num[], int lo, int hi) {
int big = lo;
for (int h = lo + 1; h <= hi; h++)
if (num[h] > num[big]) big = h;
return big;
} //end getLargest
通过将big
设置为lo
,该功能假定最大数位于第一个位置lo
。接着,它将位置lo+1
到hi
的数字与位置big
的数字进行比较。如果找到一个更大的,则big
被更新到更大数字的位置。
8.14 找出最小的数字
函数getLargest
可以很容易地修改,以找到数组中的最小值。简单地将big
改为small
,并将>替换为
int getSmallest(int num[], int lo, int hi) {
int small = lo;
for (int h = lo + 1; h <= hi; h++)
if (num[h] < num[small]) small = h;
return small;
} //end getSmallest
该函数返回从num[lo]
到num[hi]
的最小元素的位置。稍后,我们将向您展示如何使用该函数来按升序排列一组数字。
我们已经展示了如何在一个整数数组中找到最大值和最小值。对于其他类型的数组,如double
、char
或float
,该过程完全相同。唯一要做的改变是数组的声明。请记住,当我们比较两个字符时,“较大的”字符是具有较高数字代码的字符。
8.15 投票问题
我们现在说明如何使用刚才讨论的一些想法来解决下面的问题。
- 在一次选举中,有七名候选人。每个选民可以投自己选择的候选人一票。投票记录为从 1 到 7 的数字。投票人数事先未知,但投票会因
0
的投票而终止。任何不是从 1 到 7 的数字的投票都是无效的。 - 文件
votes.txt
包含候选人的姓名。第一个名字被视为候选人 1,第二个被视为候选人 2,依此类推。名字后面是投票。写一个程序来读取数据并评估选举的结果。打印所有输出到文件,results.txt
。 - 您的输出应该指定总票数、有效票数和无效票数。接下来是每位候选人和选举获胜者获得的票数。
假设您在文件中得到以下数据,votes.txt
:
Victor Taylor
Denise Duncan
Kamal Ramdhan
Michael Ali
Anisa Sawh
Carol Khan
Gary Olliverie
3 1 2 5 4 3 5 3 5 3 2 8 1 6 7 7 3 5
6 9 3 4 7 1 2 4 5 5 1 4 0
您的程序应该将以下输出发送到文件中,results.txt
:
Invalid vote: 8
Invalid vote: 9
Number of voters: 30
Number of valid votes: 28
Number of spoilt votes: 2
Candidate Score
Victor Taylor 4
Denise Duncan 3
Kamal Ramdhan 6
Michael Ali 4
Anisa Sawh 6
Carol Khan 2
Gary Olliverie 3
The winner(s):
Kamal Ramdhan
Anisa Sawh
我们需要存储 7 名候选人的姓名和他们各自获得的票数。我们将使用一个int
数组进行投票。为了自然地与候选人 1 到 7 一起工作,我们将编写声明
int vote[8];
并用vote[1]
到vote[7]
统计候选人的票数;vote[c]
将为候选人c
计票。我们不会使用vote[0]
。
但是,既然名字本身存储在一个char
数组中,我们可以为名字使用哪种数组呢?我们将需要一个“数组的数组”——一个二维数组。考虑宣言
char name[8][15];
我们可以认为name
有 8 行 15 列。如果我们每行存储一个名字,那么我们可以存储 8 个名字。每个名字存储在一个由 15 个字符组成的数组中。行从0
到7
编号,列从0
到14
编号。在我们的程序中,我们将不使用行0
。我们将把名字存储在第 1 行到第 7 行。如果我们将样本名称存储在该数组中,它将如下所示:
为了适应更长的名字,我们将使用下面的声明来存储候选人的名字:
char name[8][31];
我们将在name[c]
中存储候选人c
的姓名;name[0]
不会使用。
为了使程序灵活,我们将定义以下符号常量:
#define MaxCandidates 7
#define MaxNameLength 30
并且,在main
中,使用这些声明:
char name[MaxCandidates + 1][MaxNameLength + 1];
int vote[MaxCandidates + 1];
#define
指令将放在程序的顶部,在main
之前。当我们这样做时,任何需要使用符号常量的函数都可以使用它们。
一般来说,在任何函数之外声明的变量和标识符都是外部的,可以被同一个文件中跟在它后面的任何函数使用。(规则比这复杂一点,但这足以满足我们的目的。)因此,如果将声明放在程序的顶部,那么程序中的所有函数都可以使用变量和标识符,假设整个程序存储在一个文件中(我们的程序就是这种情况)。
该程序必须做的第一件事是读取名字并将投票计数设置为0
。我们将编写一个函数initialize
来做这件事。这也将让我们向您展示如何将一个二维数组传递给一个函数。
如前所述,我们将从两个部分(名和姓)读取候选人的名字,然后将它们连接在一起,创建一个名字,并存储在name[c]
中。下面是函数:
void initialize(char name[][MaxNameLength + 1], int vote[]) {
char lastName[MaxNameLength];
for (int c = 1; c <= MaxCandidates; c++) {
fscanf(in, "%s %s", name[c], lastName);
strcat(name[c], " ");
strcat(name[c], lastName);
vote[c] = 0;
}
} //end initialize
正如我们在参数vote
的例子中看到的,我们只需要方括号来表示vote
是一维数组。但是,在二维数组名的情况下,我们必须指定第二维的大小,并且我们必须使用一个常量或表达式,其值可以在编译程序时确定。(C99 和更高版本的 C 允许可变长度数组,其中数组下标可以在运行时指定。我们将在 9.4.1 节看到一个例子。)第一维的大小可以保持未指定,如空方括号所示。这适用于任何用作参数的二维数组。
接下来,我们必须读取和处理投票。处理投票v
包括检查它是否有效。如果是,我们想给候选人v
的分数加 1。我们将使用以下内容读取和处理投票:
fscanf(in, "%d", &v);
while (v != 0) {
if (v < 1 || v > MaxCandidates) {
fprintf(out, "Invalid vote: %d\n", v);
++spoiltVotes;
}
else {
++vote[v];
++validVotes;
}
fscanf(in, "%d", &v);
}
这里的关键陈述是
++vote[v];
这是一个聪明的方法,使用投票v
作为下标,为正确的候选人加 1。例如,如果v
是3
,我们有一个候选人3
、Kamal Ramdhan
的投票。我们希望将1
添加到候选人3
的投票计数中。该计数存储在vote[3]
中。当v
为3
时,语句变为
++vote[3];
这就把1
加到了vote[3]
上。美妙之处在于,根据v
的值,相同的语句将为任何候选项加 1。这说明了使用数组的一些威力。有 7 个候选人还是 700 个候选人并不重要;一个声明将适用于所有人。
现在我们知道了如何读取和处理投票,剩下的只是确定获胜者并打印结果。我们将把这个任务委托给函数printResults
。
使用示例数据,在计算完所有投票后,数组 vote 将包含以下值(记住我们没有使用 vote[0])。
要找到获胜者,我们必须首先找到数组中的最大值。为此,我们将调用getLargest
(第 8.13 节)
int win = getLargest(vote, 1, MaxCandidates);
这将把win
设置为从vote[1]
到vote[7]
的最大值的下标(因为MaxCandidates
是7
)。在我们的示例中,win
将被设置为3
,因为最大值6
位于位置3
。(6
也在位置5
中,但是按照编写代码的方式,它将返回包含最大值的第一个位置,如果有多个位置的话。)
现在我们知道最大值在vote[win]
中,我们可以“遍历”数组,寻找具有该值的候选值。这样,我们将找到所有得票最高的候选人(一个或多个),并宣布他们为获胜者。
细节在程序 P8.8 的函数printResults
中给出,这是我们对本节开始时提出的投票问题的解决方案。
Program P8.8
#include <stdio.h>
#include <string.h>
#define MaxCandidates 7
#define MaxNameLength 30
FILE *in, *out;
int main() {
char name[MaxCandidates + 1][MaxNameLength + 1];
int vote[MaxCandidates + 1];
int v, validVotes = 0, spoiltVotes = 0;
void initialize(char [][MaxNameLength + 1], int []);
void printResults(char [][``MaxNameLeng
in = fopen("votes.txt", "r");
out = fopen("results.txt", "w");
initialize(name, vote);
fscanf(in, "%d", &v);
while (v != 0) {
if (v < 1 || v > MaxCandidates) {
fprintf(out, "Invalid vote: %d\n", v);
++spoiltVotes;
}
else {
++vote[v];
++validVotes;
}
fscanf(in, "%d", &v);
}
printResults(name, vote, validVotes, spoiltVotes);
fclose(in);
fclose(out);
} // end main
void initialize(char name[][MaxNameLength + 1], int vote[]) {
char lastName[MaxNameLength];
for (int c = 1; c <= MaxCandidates; c++) {
fscanf(in, "%s %s", name[c], lastName);
strcat(name[c], " ");
strcat(name[c], lastName);
vote[c] = 0;
}
} // end initialize
int getLargest(int num[], int lo, int hi) {
int big = lo;
for (int h = lo + 1; h <= hi; h++)
if (num[h] > num[big]) big = h;
return big;
} //end getLargest
void printResults(char name[][MaxNameLength + 1], int vote[],
int valid, int spoilt) {
int getLargest(int v[], int, int);
fprintf(out, "\nNumber of voters: %d\n", valid + spoilt);
fprintf(out, "Number of valid votes: %d\n", valid);
fprintf(out, "Number of spoilt votes: %d\n", spoilt);
fprintf(out, "\nCandidate Score\n\n");
for (int c = 1; c <= MaxCandidates; c++)
fprintf(out, "%-15s %3d\n", name[c], vote[c]);
fprintf(out, "\nThe winner(s)\n");
int win = getLargest(vote, 1, MaxCandidates);
int winningVote = vote[win];
for (int c = 1; c <= MaxCandidates; c++)
if (vote[c] == winningVote) fprintf(out, "%s\n", name[c]);
} //end printResults
EXERCISES 8Explain the difference between a simple variable and an array variable. Write array declarations for each of the following: (a) a floating-point array of size 25 (b) an integer array of size 50 (c) a character array of size 32. What is a subscript? Name three ways in which we can write a subscript. What values are stored in an array when it is first declared? Name two ways in which we can store a value in an array element. Write a function which, given a number from 1
to 12
and a character array, stores the name of the month in the array. For example, given 8
, it stores August
in the array. Store the empty string if the number given is not valid. You declare an array of size 500. Must you store values in all elements of the array? Write code to read 200 names from a file and store them in an array. An array num
is of size 100
. You are given two values i
and k
, with 0 ≤ i < k ≤ 99
. Write code to find the average of the numbers from num[i]
to num[k]
, inclusive. Write a function, which, given a string of arbitrary characters, returns the number of consonants in the string. Modify the letter frequency count program (Program P8.2) to count the number of non-letters as well. Make sure you do not count the end-of-line characters. Write a function that, given an array of integers and an integer n, reverses the first n elements of the array. Write a program to read names and phone numbers into two arrays. Request a name and print the person’s phone number. Use at least one function. Write a function indexOf
that, given a string s
and a character c
, returns the position of the first occurrence of c
in s
. If c
is not in s
, return -1
. For example, indexOf("brother",'h')
returns 4
but indexOf("brother", 'a')
returns -1
. Write a function substring
that, given two strings s1
and s2
, returns the starting position of the first occurrence of s1
in s2
. If s1
is not in s2
, return -1
. For example, substring("mom","thermometer")
returns 4
but substring("dad","thermometer")
returns -1
. Write a function remove
that, given a string str
and a character c
, removes all occurrences of c
from str
. For example, if str
contains "brother,"remove(str,'r')
should change str
to "bothe."
Write a program to read English words and their equivalent Spanish words into two arrays. Request the user to type several English words. For each, print the equivalent Spanish word. Choose a suitable end-of-data marker. Modify the program so that the user types Spanish words instead. The number 27472
is said to be palindromic since it reads the same forwards or backwards. Write a function that, given an integer n
, returns 1
if n
is palindromic and 0
if it is not. Write a program to find out, for a class of students, the number of families with 1, 2, 3, ... up to 8 or more children. The data consists of the number of children in each pupil’s family, terminated by 0
. (Why is 0
a good value to use?) A survey of 10 pop artists is made. Each person votes for an artist by specifying the number of the artist (a value from 1 to 10). Write a program to read the names of the artists, followed by the votes, and find out which artist is the most popular. Choose a suitable end-of-data marker. The children’s game of ‘count-out’ is played as follows. n children (numbered 1 to n) are arranged in a circle. A sentence consisting of m words is used to eliminate one child at a time until one child is left. Starting at child 1, the children are counted from 1 to m and the mth child is eliminated. Starting with the child after the one just eliminated, the children are again counted from 1 to m and the mth child eliminated. This is repeated until one child is left. Counting is done circularly and eliminated children are not counted. Write a program to read values for n (assumed <= 100) and m (> 0) and print the number of the last remaining child. The prime numbers from 1 to 2500 can be obtained as follows. From a list of the numbers 1 to 2500, cross out all multiples of 2 (but not 2 itself). Then, find the next number (n, say) that is not crossed out and cross out all multiples of n (but not n). Repeat this last step provided that n has not exceeded 50 (the square root of 2500). The numbers remaining in the list (except 1) are prime. Write a program that uses this method to print all primes from 1 to 2500. Store your output in a file called primes.out
. This method is called the Sieve of Eratosthenes, named after the Greek mathematician, geographer, and philosopher. There are 500 light bulbs (numbered 1 to 500) arranged in a row. Initially, they are all OFF. Starting with bulb 2, all even numbered bulbs are turned ON. Next, starting with bulb 3, and visiting every third bulb, it is turned ON if it is OFF, and it is turned OFF if it is ON. This procedure is repeated for every 4th bulb, then every 5h bulb, and so on up to the 500th bulb. Write a program to determine which bulbs are OFF at the end of the above exercise. There is something special about the bulbs that are OFF. What is it? Can you explain why it is so?
九、搜索、排序和合并
在本章中,我们将解释以下内容:
- 如何使用顺序搜索来搜索列表
- 如何使用选择排序对列表进行排序
- 如何使用插入排序对列表进行排序
- 如何对字符串列表进行排序
- 如何对并行数组进行排序
- 如何使用二分搜索法搜索排序列表
- 如何合并两个排序列表
9.1 顺序搜索
在许多情况下,数组用于存储信息列表。存储信息后,可能需要在列表中找到给定的项目。例如,一个数组可以用来存储 50 个人的名单。然后可能需要找到给定名字(Indira
)在列表中的存储位置。
我们需要开发一种技术来搜索给定特定数组的元素。因为给定的项可能不在数组中,所以我们的技术也必须能够确定这一点。不管数组中元素的类型如何,搜索项的技术都是相同的。然而,对于不同类型的元素,该技术的实现可能是不同的。
我们将使用一个整数数组来说明称为顺序搜索的技术。考虑七个整数的数组num
:
我们希望确定数字61
是否被存储。在搜索术语中,61
被称为搜索关键字,或者简称为关键字。搜索过程如下:
- 将
61
与第一个数字num[0]
进行比较,第一个数字是35
;它们不匹配,所以我们继续下一个数字。 - 将
61
与第二个数字num[1]
比较,第二个数字是17
;它们不匹配,所以我们继续下一个数字。 - 将
61
与第三个数字num[2]
相比较,第三个数字是48
;它们不匹配,所以我们继续下一个数字。 - 将
61
与第 4 个数字num[3]
进行比较,第 4 个数字是25
;它们不匹配,所以我们继续下一个数字。 - 将
61
与第 5 个数字num[4]
进行比较,第 5 个数字是61
;它们匹配,所以搜索停止,我们断定钥匙在位置4
。
但是如果我们在找32
呢?在这种情况下,我们将比较32
和数组中的所有数字,没有一个匹配。我们断定32
不在阵列中。
假设数组包含n
个数字,我们可以将上述逻辑表达如下:
for h = 0 to n - 1
if (key == num[h]) then key found, exit the loop
endfor
if h < n then key found in position h
else key not found
在这种情况下,我们可能想在查看完数组中的所有元素之前退出循环。另一方面,我们可能必须查看所有元素,然后才能得出结论,关键不在那里。
如果我们找到了密钥,我们就退出循环,h
将小于n
。如果我们因为h
变成n
而退出循环,那么这个键不在数组中。
让我们用函数search
来表达这种技术,给定一个int
数组num
,一个整数key
,以及两个整数lo
和hi
,从num[lo]
到num[hi]
搜索key
。如果找到,函数返回数组中的位置。如果没有找到,则返回-1
。例如,考虑以下语句:
n = search(num, 61, 0, 6);
这将在num[0]
到num[6]
中搜索61
。它将在位置4
找到它并返回4
,然后存储在n
中。电话
search(num, 32, 0, 6)
将返回-1
,因为32
没有存储在数组中。search
功能如下:
int search(int num[], int key, int lo, int hi) {
//search for key from num[lo] to num[hi]
for (int h = lo; h <= hi; h++)
if (key == num[h]) return h;
return -1;
} //end search
我们首先设置h
到lo
从那个位置开始搜索。for
循环“遍历”数组的元素,直到找到键或者h
通过hi
。
为了举例说明如何使用搜索,考虑一下上一章的投票问题。计完票数后,我们的数组name
和vote
如下所示(记住我们没有使用name[0]
和vote[0]
):
假设我们想知道收到了多少张选票。我们必须在name
数组中搜索她的名字。当我们找到它时(在6
的位置),我们可以从vote[6]
检索她的投票。一般来说,如果一个名字在n
的位置,得到的票数将是vote[n]
。
我们修改我们的搜索函数,在name
数组中查找一个名字:
//search for key from name[lo] to name[hi]
int search(char name[][MaxNameLength+1], char key[], int lo, int hi) {
for (int h = lo; h <= hi; h++)
if (strcmp(key, name[h]) == 0) return h;
return -1;
}
回想一下,我们使用strcmp
来比较两个字符串。为了使用任何预定义的字符串函数,我们必须使用指令
#include <string.h>
我们节目的负责人。
我们可以如下使用该函数:
n = search(name, "Carol Khan", 1, 7);
if (n > 0) printf("%s received %d vote(s)\n", name[n], vote[n]);
else printf("Name not found\n");
使用我们的样本数据,search
将返回存储在n
中的6,
。从6 > 0
开始,代码将被打印
Carol Khan received 2 vote(s)
9.2 选择排序
考虑 8.15 节的投票程序。在程序 P8.8 中,我们按照给出名字的顺序打印结果。但是,假设我们想按姓名的字母顺序或按收到的票数顺序打印结果,获胜者排在第一位。我们必须按照我们想要的顺序重新排列名字或选票。我们说我们必须将名字按升序排序,或者将选票按降序排序。
排序是将一组值按升序或降序排列的过程。排序的原因有很多。有时我们排序是为了产生更可读的输出(例如,产生一个按字母顺序排列的列表)。教师可能需要按姓名或平均分对学生进行排序。如果我们有一个很大的值集,并且我们想要识别重复项,我们可以通过排序来实现;重复的值将一起出现在排序列表中。排序的方式有很多种。我们将讨论一种叫做选择排序的方法。
考虑以下阵列:
使用选择排序按升序对num
进行排序的过程如下:
第一遍
-
找出位置
0
到6
的最小数字;最小的是15
,位于4
位置。 -
Interchange the numbers in positions
0
and4
. We get this:
第二遍
-
找出位置
1
到6
的最小数字;最小的是33
,位于5
位置。 -
Interchange the numbers in positions
1
and5
. We get this:
第三遍
-
找出位置
2
到6
的最小数字;最小的是48
,位于5
位置。 -
Interchange the numbers in positions
2
and5
. We get this:
第四遍
-
找出位置
3
到6
的最小数字;最小的是52
,位于6
位置。 -
Interchange the numbers in positions
3
and6
. We get this:
第五遍
-
找出位置
4
到6
的最小数字;最小的是57
,位于4
位置。 -
Interchange the numbers in positions
4
and4
. We get this:
第六遍
-
找出位置
5
到6
的最小数字;最小的是65
,位于6
位置。 -
Interchange the numbers in positions
5
and6
. We get this:
现在数组已经完全排序了。
如果我们让h
从0
到5
,在每一遍中:
- 我们从位置
h
到6
找到最小的数字。 - 如果最小的数字在位置
s
,我们交换位置h
和s
的数字。 - 对于大小为
n
的数组,我们进行n-1
遍。在我们的示例中,我们在六次传递中对七个数字进行了排序。
以下是该算法的概要:
for h = 0 to n - 2
s = position of smallest number from num[h] to num[n-1]
swap num[h] and num[s]
endfor
在 8.14 节,我们写了一个函数来返回整数数组中最小数的位置。这里是为了便于参考:
//find position of smallest from num[lo] to num[hi]
int getSmallest(int num[], int lo, int hi) {
int small = lo;
for (int h = lo + 1; h <= hi; h++)
if (num[h] < num[small]) small = h;
return small;
} //end getSmallest
我们还编写了一个函数swap
,它交换了一个字符数组中的两个元素。我们现在重写swap
来交换整数数组中的两个元素:
//swap elements num[i] and num[j]
void swap(int num[], int i, int j) {
int hold = num[i];
num[i] = num[j];
num[j] = hold;
} //end swap
有了getSmallest
和swap
,我们可以将上面的算法编码成函数selectionSort
。为了强调我们可以为参数使用任何名称,我们编写了一个函数来对名为list
的整数数组进行排序。为了通用,我们还通过指定下标lo
和hi
来告诉函数对数组的哪一部分进行排序。与算法中从0
到n-2
的循环不同,现在是从lo
到hi-1
——这只是一个微小的变化,以获得更大的灵活性。
//sort list[lo] to list[hi] in ascending order
void selectionSort(int list[], int lo, int hi) {
int getSmallest(int [], int, int);
void swap(int [], int, int);
for (int h = lo; h < hi; h++) {
int s = getSmallest(list, h, hi);
swap(list, h, s);
}
} //end selectionSort
我们现在编写程序 P9.1 来测试selectionSort
是否正常工作。程序请求最多 10 个数字(因为数组被声明为大小为 10),将它们存储在数组num
中,调用selectionSort
,然后打印排序后的列表。
Program P9.1
#include <stdio.h>
int main() {
void selectionSort(int [], int, int);
int v, num[10];
printf("Type up to 10 numbers followed by 0\n");
int n = 0;
scanf("%d", &v);
while (v != 0) {
num[n++] = v;
scanf("%d", &v);
}
//n numbers are stored from num[0] to num[n-1]
selectionSort(num, 0, n-1);
printf("\nThe sorted numbers are\n");
for (int h = 0; h < n; h++) printf("%d ", num[h]);
printf("\n");
} //end main
void selectionSort(int list[], int lo, int hi) {
//sort list[lo] to list[hi] in ascending order
int getSmallest(int [], int, int);
void swap(int [], int, int);
for (int h = lo; h < hi; h++) {
int s = getSmallest(list, h, hi);
swap(list, h, s);
}
} //end selectionSort
int getSmallest(int num[], int lo, int hi) {
//find position of smallest from num[lo] to num[hi]
int small = lo;
for (int h = lo + 1; h <= hi; h++)
if (num[h] < num[small]) small = h;
return small;
} //end getSmallest
void swap(int num[], int i, int j) {
//swap elements num[i] and num[j]
int hold = num[i];
num[i] = num[j];
num[j] = hold;
} //end swap
以下是该程序的运行示例:
Type up to 10 numbers followed by 0
57 48 79 65 15 33 52 0
The sorted numbers are
15 33 48 52 57 65
79
对程序 P9.1 的评论
这个程序演示了如何在一个数组中读取和存储未知数量的值。该程序最多支持 10 个数字,但如果提供的数字更少,它也必须工作。我们使用n
给数组加下标并计数。最初,n
是0
。下面描述了样本数据的情况:
-
读取第一个数字
57
;它不是0
,所以我们进入 while 循环。我们将57
存储在num[0]
中,然后将1
添加到n
,使其成为1
;已读取一个数字,且n
为1
。 -
读取第二个数字
48
;它不是0
,所以我们进入the while
循环。我们将48
存储在num[1]
中,然后将1
加到n
,使其成为2
;已读取两个数字,n
为2
。 -
读取第三个数字
79
;它不是0
,所以我们进入while
循环。我们将79
存储在num[2]
中,然后将1
加到n
,使其成为3
;已经读取了三个数字,n
是3
。 -
第 4 个数字,
65
,读出;它不是0
,所以我们进入while
循环。我们将65
存储在num[3]
中,然后将1
加到n
,使其成为4
;已经读取了四个数字,n
是4
。 -
第 5 个数字,
15
,读出;它不是0
,所以我们进入while
循环。我们将15
存储在num[4]
中,然后将1
加到n
,使其成为5
;已读取五个数字,n
为5
。 -
第 6 个数字,
33
,读;它不是0
,所以我们进入while
循环。我们将33
存储在num[5]
中,然后将1
加到n
,使其成为6
;已读取六个数字,n
为6
。 -
第 7 个数字,
52
,读;它不是0
,所以我们进入while
循环。我们将52
存储在num[6]
中,然后将1
加到n
,使其成为7
;已读取七个数字,n
为7
。 -
The 8th number,
0
, is read; it is0
so we exit thewhile
loop and the array looks like this:
在任何阶段,n
的值表示到那时为止已经存储了多少个数字。最后,n
是7
,数组中存储了七个数字。程序的其余部分可以假设n
给出了数组中实际存储的值的数量;从num[0]
到num[n-1]
存储数值。
例如,调用
selectionSort(num, 0, n-1);
是对num[0]
到num[n-1]
进行排序的请求,但是,由于n
是7
,所以是对num[0]
到num[6]
进行排序的请求。
如前所述,如果用户在输入0
之前输入超过 10 个数字,程序将会崩溃。当读取第 11 个数字时,将试图将其存储在不存在的num[10]
中,给出“数组下标”错误。
我们可以通过将while
条件改为这样来处理:
while (v != 0 && n < 10)
现在,如果n
达到10
,则不进入循环(因为10
不小于10
,并且不会尝试存储第 11 个数字。事实上,第 10 个数字之后的所有数字都将被忽略。
通常,最好在整个程序中使用一个设置为10
的符号常量(MaxNum
),并使用MaxNum
,而不是常量10
。
我们已经按升序对数组进行了排序。我们可以用下面的算法对num[0]
到num[n-1]
进行降序排序:
for h = 0 to n - 2
b = position of biggest number from num[h] to num[n-1]
swap num[h] and num[b]
endfor
我们建议您尝试练习 1 和 2,按姓名升序和收到的票数降序打印投票问题的结果。
9.2.1 选择排序分析
为了找到 k 个项目中最小的一个,我们进行 k-1 次比较。在第一遍中,我们进行 n-1 次比较,找出 n 个项目中最小的一个。在第二遍中,我们进行 n-2 次比较,找出 n-1 项中最小的一项。以此类推,直到最后一遍,我们进行一次比较,找出两个项目中较小的一个。一般来说,在第 I 遍中,我们进行 n-i 次比较,以找到 n-i+1 项中最小的一项。因此:
比较总数= 1 + 2 +...+ n-1 = n(n-1) ≈ n 2
我们说选择排序的顺序是 O(n 2 )(“大 on 的平方”)。常量在“大 O”符号中并不重要,因为当 n 变得很大时,常量变得无关紧要。
每一次,我们用三个任务交换两个项目。我们进行了 n-1 次传递,因此我们总共进行了 3(n-1)次分配。使用“大 O”符号,我们说赋值的个数是 O(n)。常量 3 和 1 并不重要,因为 n 变大了。
如果数据是有序的,选择排序的性能会更好吗?不。一种方法是给它一个排序列表,看看它做什么。如果你完成了这个算法,你会发现这个方法不考虑数据的顺序。不管数据如何,它每次都会进行相同次数的比较。
作为一个练习,修改程序代码,使其计算使用选择排序对列表进行排序时的比较和赋值次数。
9.3 插入排序
考虑与之前相同的阵列:
把数字想象成桌子上的卡片,按照它们在数组中出现的顺序一次拿起一张。因此,我们首先拿起57
,然后是48
,然后是79
,以此类推,直到我们拿起52
。然而,当我们拿起每一个新的数字时,我们把它加到我们手上,这样我们手上的数字都被排序了。
当我们拿起57
时,我们手中只有一个数字。我们认为有一个数字需要排序。
当我们拿起48
时,我们把它加在57
前面,这样我们的手就包含了
48 57
当我们拿起79
时,我们把它放在57
之后,这样我们的手就包含了
48 57 79
当我们拿起65
时,我们把它放在57
之后,这样我们的手就包含了
48 57 65 79
在这个阶段,四个数字已经被挑选出来,我们的手将它们按顺序排列。
当我们拿起15
时,我们把它放在48
之前,这样我们的手就包含了
15 48 57 65 79
当我们拿起33
时,我们把它放在15
之后,这样我们的手就包含了
15 33 48 57 65 79
最后,当我们拿起52
时,我们把它放在48
之后,这样我们的手就包含了
15 33 48 52 57 65 79
这些数字已按升序排列。
所描述的方法说明了插入排序背后的思想。从左到右,一次处理一个数组中的数字。这相当于从表中选取数字,一次一个。由于第一个数字本身是已排序的,我们将从第二个数字开始处理数组中的数字。
当我们开始处理num[h]
时,我们可以假设num[0]
到num[h-1]
被排序。然后,我们尝试在num[0]
到num[h-1]
之间插入num[h]
,以便对num[0]
到num[h]
进行排序。然后,我们将继续处理num[h+1]
。当我们这样做时,我们假设元素num[0]
到num[h]
被排序将是正确的。
使用插入排序按升序对num
进行排序的过程如下:
第一遍
-
Process
num[1]
, that is,48
. This involves placing48
so that the first two numbers are sorted;num[0]
andnum[1]
now contain the following:
数组的其余部分保持不变。
第二遍
-
Process
num[2]
, that is,79
. This involves placing79
so that the first three numbers are sorted;num[0]
tonum[2]
now contain the following:
数组的其余部分保持不变。
第三遍
-
Process
num[3]
, that is,65
. This involves placing65
so that the first four numbers are sorted;num[0] to num[3]
now contain the following:
数组的其余部分保持不变。
第四遍
-
Process
num[4]
, that is,15
. This involves placing15
so that the first five numbers are sorted. To simplify the explanation, think of15
as being taken out and stored in a simple variable (key
, say) leaving a “hole” innum[4]
. We can picture this as follows:
将15
插入其正确位置的过程如下:
-
Compare
15
with79
; it is smaller, so move79
to location4
, leaving location3
free. This gives the following: -
Compare
15
with65
; it is smaller, so move65
to location3
, leaving location2
free. This gives the following: -
Compare
15
with57
; it is smaller, so move57
to location2
, leaving location1
free. This gives the following: -
Compare
15
with48
; it is smaller, so move48
to location1
, leaving location0
free. This gives the following: -
There are no more numbers to compare with
15
, so it is inserted in location0
, giving the following: -
我们可以把
15
(key
)的摆放逻辑用它和它左边的数字比较来表达,从最近的一个开始。只要key
小于num[k]
,对于某些k
,我们就把num[k]
移到num[k+1]
位置,继续考虑num[k-1]
,前提是它存在。当k
实际上是0
的时候就不会存在了。在这种情况下,过程停止,并且key
插入位置0
。
第五遍
-
流程
num[5]
,即33
。这包括放置33
,以便对前六个数字进行排序。这是按如下方式完成的:- 将
33
存储在key
中,留下位置5
空闲。 - 比较
33
和79
;它变小了,所以把79
移到位置5
,留下位置4
空闲。 - 比较
33
和65
;它变小了,所以把65
移到位置4
,留下位置3
空闲。 - 比较
33
和57
;它变小了,所以把57
移到位置3
,留下位置2
空闲。 - 比较
33
和48
;它变小了,所以把48
移到位置2
,留下位置1
空闲。
- 将
-
Compare
33
with15
; it is bigger, so insert33
in location1
. This gives the following: -
我们可以通过与它左边的数字比较来表达放置
33
的逻辑,从最近的一个开始。只要key
小于num[k]
,对于某些k
,我们就把num[k]
移到位置num[k+1]
,继续考虑num[k-1]
,前提是它存在。如果某些k
的key
大于或等于num[k]
,则key
插入k+1
位置。这里,33
大于num[0]
,所以插入num[1]
。
第六遍
-
流程
num[6]
,即52
。这包括放置52
,以便对前七个(所有)数字进行排序。这是按如下方式完成的:- 将
52
存储在key
中,留下位置6
空闲。 - 比较
52
和79
;它变小了,所以把79
移到位置6
,留下位置5
空闲。 - 比较
52
和65
;它变小了,所以把65
移到位置5
,留下位置4
空闲。 - 比较
52
和57
;它变小了,所以把57
移到位置4
,留下位置3
空闲。
- 将
-
Compare
52
with48
; it is bigger, so insert52
in location3
. This gives the following:
数组现在已经完全排序了。
以下是使用插入排序对数组num
的前n
个元素进行排序的概要:
for h = 1 to n - 1 do
insert num[h] among num[0] to num[h-1] so that
num[0] to num[h] are sorted
endfor
使用这个大纲,我们使用参数list
编写函数insertionSort
。
void insertionSort(int list[], int n) {
//sort list[0] to list[n-1] in ascending order
for (int h = 1; h < n; h++) {
int key = list[h];
int k = h - 1; //start comparing with previous item
while (k >= 0 && key < list[k]) {
list[k + 1] = list[k];
--k;
}
list[k + 1] = key;
} //end for
} //end insertionSort
while
语句是排序的核心。它声明,只要我们在数组(k >= 0
)中,并且当前数字(key
)小于数组(key < list[k]
)中的数字,我们就将list[k]
向右移动(list[k+1] = list[k]
),并继续移动到左边的下一个数字(--k
)。
对于某些k
,如果k
等于-1
或者如果key
大于或等于list[k]
,我们退出while
循环。无论哪种情况,key
都被插入到list[k+1]
中。如果k
为-1
,则表示当前数字小于列表中所有之前的数字,必须插入到list[0]
中。但是当k
为-1
时list[k+1]
为list[0]
,所以在这种情况下key
插入正确。
该函数按升序排序。要按降序排序,我们所要做的就是改变while
条件中的< to >,因此:
while (k >= 0 && key > list[k])
现在,如果一个键变大了,它就会向左移动。
我们编写程序 P9.2 来测试insertionSort
是否正确工作。
Program P9.2
#include <stdio.h>
int main() {
void insertionSort(int [], int);
int v, num[10];
printf("Type up to 10 numbers followed by 0\n");
int n = 0;
scanf("%d", &v);
while (v != 0) {
num[n++] = v;
scanf("%d", &v);
}
//n numbers are stored from num[0] to num[n-1]
insertionSort(num, n);
printf("\nThe sorted numbers are\n");
for (int h = 0; h < n; h++) printf("%d ", num[h]);
printf("\n");
} //end main
void insertionSort(int list[], int n) {
//sort list[0] to list[n-1] in ascending order
for (int h = 1; h < n; h++) {
int key = list[h];
int k = h - 1; //start comparing with previous item
while (k >= 0 && key < list[k]) {
list[k + 1] = list[k];
--k;
}
list[k + 1] = key;
} //end for
} //end insertionSort
程序请求最多 10 个数字(因为数组被声明为大小为 10),将它们存储在数组num
中,调用insertionSort
,然后打印排序后的列表。以下是 P9.2 的运行示例:
Type up to 10 numbers followed by 0
57 48 79 65 15 33 52 0
The sorted numbers are
15 33 48 52 57 65
79
9.3.1 插入排序分析
在处理 j 项时,我们可以进行少至一次的比较(如果num[j]
大于num[j-1]
)或多达 j-1 次的比较(如果num[j]
小于前面所有的项)。对于随机数据,平均来说,我们应该进行(j-1)次比较。因此,对 n 个项目进行排序的平均总比较次数如下:
我们说插入排序的阶数为 O(n 2 )(“大 on 的平方”)。随着n
变大,常量并不重要。
每次我们做一个比较,我们也做一个分配。因此,分配的总数也是 n(n-1) ≈ n 2 。
我们强调这是随机数据的平均值。与选择排序不同,插入排序的实际性能取决于所提供的数据。如果给定的数组已经排序,插入排序将通过进行 n-1 次比较来快速确定这一点。在这种情况下,它以 O(n)时间运行。人们会认为,数据中的顺序越多,插入排序的性能就越好。
如果给定的数据是降序排列的,插入排序的性能最差,因为每个新数字都必须一直移动到列表的开头。在这种情况下,比较的次数是 n(n-1) ≈ n 2 。分配数也是 n(n-1) ≈ n 2 。
因此,通过插入排序进行比较的次数从 n-1(最佳)到 n 2 (平均)到 n 2 (最差)。赋值的次数总是与比较的次数相同。
作为一个练习,修改程序代码,使其计算使用插入排序对列表进行排序时的比较和赋值次数。
9.3.2 在适当的位置插入一个元素
插入排序使用向已经排序的列表中添加新元素的思想,以便列表保持排序。我们可以把它本身当作一个问题(与插入排序无关)。具体来说,给定一个从list[m]
到list[n]
的排序列表,我们想要向列表中添加一个新的条目(比如说newItem
),以便对list[m]
到list[n+1]
进行排序。
添加新项目会使列表的大小增加 1。我们假设数组有空间容纳新的项目。我们编写函数insertInPlace
来解决这个问题。
void insertInPlace(int newItem, int list[], int m, int n) {
//list[m] to list[n] are sorted
//insert newItem so that list[m] to list[n+1] are sorted
int k = n;
while (k >= m && newItem < list[k]) {
list[k + 1] = list[k];
--k;
}
list[k + 1] = newItem;
} //end insertInPlace
现在我们有了insertInPlace
,我们可以将insertionSort
(称之为insertionSort2
)重写如下:
void insertionSort2(int list[], int lo, int hi) {
//sort list[lo] to list[hi] in ascending order
void insertInPlace(int, int [], int, int);
for (int h = lo + 1; h <= hi; h++)
insertInPlace(list[h], list, lo, h - 1);
} //end insertionSort2
请注意,insertionSort2
的原型现在是这样的:
void insertionSort2(int [], int, int);
为了对包含n
项的数组num
进行排序,我们必须这样调用它:
insertionSort2(num, 0, n-1);
9.4 对字符串数组进行排序
考虑按字母顺序排列姓名列表的问题。我们已经看到,在 C 中,每个名字都存储在一个字符数组中。为了存储几个名字,我们需要一个二维字符数组。例如,考虑下面的名字列表。
| | `0` | `1` | `2` | `3` | `4` | `5` | `6` | `7` | `8` | `9` | `10` | `11` | `12` | `13` | `14` | | `0` | `S` | `a` | `m` | `l` | `a` | `l` | `,` | | `R` | `a` | `w` | `l` | `E` | `\0` | | | `1` | `W` | `i` | `l` | `l` | `i` | `a` | `m` | `s` | `,` | | `M` | `a` | `r` | `k` | `\0` | | `2` | `D` | `e` | `l` | `w` | `i` | `n` | `,` | | `M` | `a` | `c` | `\0` | | | | | `3` | `T` | `a` | `y` | `l` | `o` | `r` | `,` | | `V` | `i` | `c` | `t` | `o` | `r` | `\0` | | `4` | `M` | `o` | `h` | `a` | `m` | `e` | `d` | `,` | | `A` | `b` | `u` | `\0` | | | | `5` | `S` | `i` | `n` | `g` | `h` | `,` | | `K` | `R` | `i` | `s` | `h` | `n` | `a` | `\0` | | `6` | `T` | `a` | `w` | `a` | `r` | `i` | `,` | | `T` | `a` | `u` | `\0` | | | | | `7` | `A` | `b` | `d` | `o` | `o` | `l` | `,` | | `Z` | `a` | `i` | `d` | `\0` | | |为了存储这个列表,我们需要一个如下所示的声明:
char list[8][15];
为了迎合更长的名字,我们可以增加 15 个,为了迎合更多的名字,我们可以增加 8 个。
排序list
的过程本质上与排序整数数组相同。主要区别在于,我们使用<
来比较两个数字,而我们必须使用strcmp
来比较两个名字。在前面显示的函数insertionSort
中,while
条件由此改变:
while (k >= lo && key < list[k])
到下面,其中key
现在被声明为char key[15]
:
while (k >= lo && strcmp(key, list[k]) < 0)
此外,我们现在必须使用strcpy
(因为我们不能对字符串使用=
)来为另一个位置分配名称。我们将在下一节看到完整的功能。
可变长度数组
我们将用这个例子来展示可变长度数组(vla)在 C 语言中的用法,这个特性只在 C99 及更高版本的 C 语言中可用。其思想是数组的大小可以在运行时指定,而不是在编译时指定。
在下面的函数中,注意参数列表中list
( char list[][max]
)的声明。与一维数组一样,第一维的大小没有指定。使用参数max
指定第二维的尺寸;调用函数时会指定max
的值。这给了我们更多的灵活性,因为我们可以在运行时指定第二维的大小。
void insertionSort3(int lo, int hi, int max, char list[][max]) {
//Sort the strings in list[lo] to list[hi] in alphabetical order.
//The maximum string size is max - 1 (one char taken up by \0).
char key[max];
for (int h = lo + 1; h <= hi; h++) {
strcpy(key, list[h]);
int k = h - 1; //start comparing with previous item
while (k >= lo && strcmp(key, list[k]) < 0) {
strcpy(list[k + 1], list[k]);
--k;
}
strcpy(list[k + 1], key);
} //end for
} // end insertionSort3
我们编写一个简单的main
例程来测试insertionSort3
,如程序 P9.3 所示。
Program P9.3
#include <stdio.h>
#include <string.h>
#define MaxNameSize 14
#define MaxNameBuffer MaxNameSize+1
#define MaxNames 8
int main() {
void insertionSort3(int, int, int max, char [][max]);
char name[MaxNames][MaxNameBuffer] =
{"Samlal, Rawle", "Williams, Mark","Delwin, Mac",
"Taylor, Victor", "Mohamed, Abu","Singh, Krishna",
"Tawari, Tau", "Abdool, Zaid" };
insertionSort3(0, MaxNames-1, MaxNameBuffer, name);
printf("\nThe sorted names are\n\n");
for (int h = 0; h < MaxNames; h++) printf("%s\n", name[h]);
} //end main
void insertionSort3(int lo, int hi, int max, char list[][max]) {
//Sort the strings in list[lo] to list[hi] in alphabetical order.
//The maximum string size is max - 1 (one char taken up by \0).
char key[max];
for (int h = lo + 1; h <= hi; h++) {
strcpy(key, list[h]);
int k = h - 1; //start comparing with previous item
while (k >= lo && strcmp(key, list[k]) < 0) {
strcpy(list[k + 1], list[k]);
--k;
}
strcpy(list[k + 1], key);
} //end for
} // end insertionSort3
name
的声明用前面显示的八个名字初始化它。运行时,该程序产生以下输出:
The sorted names are
Abdool, Zaid
Delwin, Mac
Mohamed, Abu
Samlal, Rawle
Singh, Krishna
Tawari, Tau
Taylor, Victor
Williams,
Mark
9.5 排序并行数组
在不同的数组中有相关的信息是很常见的。例如,假设除了name
,我们还有一个整数数组id
,使得id[h]
是与name[h]
相关联的标识号,如下所示。
在不同的数组中有相关的信息是很常见的。例如,假设除了name
,我们还有一个整数数组id
,使得id[h]
是与name[h]
相关联的标识号,如下所示。
考虑按字母顺序排列名字的问题。最后,我们希望每个名字都有正确的 ID 号。所以,比如排序完成后,name[0]
应该包含Abdool, Zaid
,id[0]
应该包含7746
。
为此,在排序过程中,每次移动一个姓名时,相应的 ID 号也必须移动。因为姓名和 ID 号必须“并行”移动,所以我们说我们正在进行并行排序,或者我们正在对并行数组进行排序。
我们重写insertionSort3
来说明如何对并行数组进行排序。我们只需添加代码,以便在移动名称时移动 ID。我们称之为parallelSort
。
void parallelSort(int lo, int hi, int max, char list[][max], int id[]) {
//Sort the names in list[lo] to list[hi] in alphabetical order, ensuring
//that each name remains with its original id number.
//The maximum string size is max - 1 (one char taken up by \0).
char key[max];
for (int h = lo + 1; h <= hi; h++) {
strcpy(key, list[h]);
int m = id[h]; // extract the id number
int k = h - 1; //start comparing with previous item
while (k >= lo && strcmp(key, list[k]) < 0) {
strcpy(list[k + 1], list[k]);
id[k+ 1] = id[k]; // move up id when we move a name
--k;
}
strcpy(list[k + 1], key);
id[k + 1] = m; // store id in the same position as the name
} //end for
} //end parallelSort
我们通过编写下面的main
例程来测试parallelSort
:
#include <stdio.h>
#include <string.h>
#define MaxNameSize 14
#define MaxNameBuffer MaxNameSize+1
#define MaxNames 8
int main() {
void parallelSort(int, int, int max, char [][max], int[]);
char name[MaxNames][MaxNameBuffer] =
{"Samlal, Rawle", "Williams, Mark","Delwin, Mac",
"Taylor, Victor", "Mohamed, Abu","Singh, Krishna",
"Tawari, Tau", "Abdool, Zaid" };
int id[MaxNames] = {8742,5418,4833,4230,8583,2458,5768,3313};
parallelSort(0, MaxNames-1, MaxNameBuffer, name, id);
printf("\nThe sorted names and IDs are\n\n");
for (int h = 0; h < MaxNames; h++)
printf("%-18s %d\n", name[h], id[h]);
} //end main
运行时,它会产生以下输出:
The sorted names and IDs are
Abdool, Zaid 3313
Delwin, Mac 4833
Mohamed, Abu 8583
Samlal, Rawle 8742
Singh, Krishna 2458
Tawari, Tau 5768
Taylor, Victor 4230
Williams, Mark 5418
我们顺便注意到,使用 C 结构可以更方便地存储“并行数组”。在我们学习了一些结构之后,我们将在 10.9 节讨论一个例子。
9.6 二分搜索法
如果列表是有序的(升序或降序),二分搜索法是搜索给定项目列表的一种非常快速的方法。如果列表不有序,可以使用前面描述的任何方法进行排序。
为了说明该方法,考虑一个由 11 个数字组成的列表,按升序排列。
假设我们希望搜索56
。搜索过程如下:
- 首先,我们找到列表中的中间项。这是在位置
5
的49
。我们将56
与49
相比较。由于56
更大,我们知道如果56
在列表中,它一定在位置5
之后,因为数字是升序排列的。下一步,我们将搜索范围限制在位置6
到10
。 - 接下来,我们从位置
6
到10
找到中间的项目。这是8
位置的物品,即72
。 - 我们比较
56
和72
。由于56
较小,我们知道如果56
在列表中,它一定在位置8
之前,因为数字是按升序排列的。下一步,我们将搜索范围限制在位置6
到7
。 - 接下来,我们从位置
6
到7
找到中间的项目。在这种情况下,我们可以选择项目6
或项目7
。我们要写的算法会选择6
项,也就是56
。 - 我们比较
56
和56
。由于它们是相同的,我们的搜索成功结束,在位置6
找到了所需的项目。
假设我们正在搜索60
。搜索将如上进行,直到我们将60
与56
(在位置6
)进行比较。
- 由于
60
更大,我们知道如果60
在列表中,它一定在位置6
之后,因为数字是按升序排列的。下一步,我们将搜索范围限制在7
到7
的地点。这只是一个地点。 - 我们将
60
与7
项进行比较,即66
。由于60
较小,我们知道如果60
在列表中,它一定在位置7
之前。由于它不可能在位置6
之后,位置7
之前,所以我们断定它不在列表中。
在搜索的每个阶段,我们将搜索限制在列表的某个部分。让我们使用变量lo
和hi
作为定义这一部分的下标。换句话说,我们的搜索将被限制在从num[lo]
到num[hi]
的数字范围内。
最初,我们想要搜索整个列表,因此在本例中,我们将把lo
设置为0
,把hi
设置为10
。
我们如何找到中项的下标?我们将使用计算
mid = (lo + hi) / 2;
因为将执行整数除法,所以分数(如果有的话)将被丢弃。例如当lo
为0
,hi
为10
,mid
变为5
;当lo
为6
且hi
为10
时,mid
变为8
;当lo
为6
且hi
为7
时,mid
变为6
。
只要lo
小于或等于hi
,它们就定义了要搜索的列表的非空部分。当lo
等于hi
时,它们定义了要搜索的单个项目。如果lo
变得比hi
大,这意味着我们已经搜索了整个列表,但没有找到该项目。
基于这些想法,我们现在可以编写一个函数binarySearch
。更一般地说,我们将编写它,以便调用例程可以指定它希望搜索数组的哪个部分来查找该项。
因此,必须给该函数指定要搜索的项(key
)、数组(list
)、搜索的开始位置(lo
)和搜索的结束位置(hi
)。例如,为了在上面的数组num
中搜索数字56
,我们可以发出下面的调用:
binarySearch(56, num, 0, 10)
这个函数必须告诉我们搜索的结果。如果找到了该项,该函数将返回它的位置。如果没有找到,将返回-1
。
int binarySearch(int key, int list[], int lo, int hi) {
//search for key from list[lo] to list[hi]
//if found, return its location; otherwise, return -1
int mid;
while (lo <=``h
mid = (lo + hi) / 2;
if (key == list[mid]) return mid; // found
if (key < list[mid]) hi = mid - 1;
else lo = mid + 1;
}
return -1; //lo and hi have crossed; key not found
} //end binarySearch
如果item
包含一个要搜索的数字,我们可以编写以下代码来调用binarySearch
并检查搜索的结果:
int ans = binarySearch(item, num, 0, 12);
if (ans == -1) printf(“%d not found\n”, item);
else printf(“%d found in location %d\n”, item, ans);
如果我们希望从位置i
到j
搜索item
,我们可以这样写:
int ans = binarySearch(item, num, i, j);
9.7 词频计数
让我们写一个程序来阅读一篇英语文章,并统计每个单词出现的次数。输出由单词及其频率的字母列表组成。
我们可以使用以下大纲来开发我们的程序:
while there is input
get a word
search for word
if word is in the table
add 1 to its count
else
add word to the table
set its count to 1
endif
endwhile
print table
这是典型的“搜索并插入”情况。我们在目前存储的单词中搜索下一个单词。如果搜索成功,我们只需要增加它的计数。如果搜索失败,该单词将被放入表中,并且其计数设置为 1。
这里的一个主要设计决策是如何搜索表,这反过来又取决于新单词在表中的插入位置和插入方式。以下是两种可能性:
A new word is inserted in the next free position in the table. This implies that a sequential search must be used to look for an incoming word since the words would not be in any particular order. This method has the advantages of simplicity and easy insertion, but searching takes longer as more words are put in the table. A new word is inserted in the table in such a way that the words are always in alphabetical order. This may entail moving words that have already been stored so that the new word may be slotted in the right place. However, since the table is in order, a binary search can be used to search for an incoming word.
对于这种方法,搜索速度更快,但是插入速度比(1)慢。因为一般来说,搜索比插入更频繁,(2)可能更好。
(2)的另一个优点是,在最后,单词已经按字母顺序排列,不需要排序。如果使用(1),则需要对单词进行排序,以获得字母顺序。
我们将使用(2)中的方法编写程序。完整的程序如程序 P9.4 所示。
Program P9.4
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <stdlib.h>
#define MaxWords 50
#define MaxLength 10
#define MaxWordBuffer MaxLength+1
int main() {
int getWord(FILE *, char[]);
int binarySearch(int, int, char [], int max, char [][max]);
void addToList(char[], int max, char [][max], int[], int, int);
void printResults(FILE *, int max, char [][max], int[], int);
char wordList[MaxWords][MaxWordBuffer], word[MaxWordBuffer];
int frequency[MaxWords], numWords = 0;
FILE * in = fopen("passage.txt", "r");
if (in == NULL){
printf("Cannot find file\n");
exit(1);
}
FILE * out = fopen("output.txt", "w");
if (out == NULL){
printf("Cannot create output file\n");
exit(2);
}
for (int h = 1; h <= MaxWords ; h++) frequency[h] = 0;
while (getWord(in, word) != 0) {
int loc = binarySearch (0, numWords-1, word, MaxWordBuffer,
wordList);
if (strcmp(word, wordList[loc]) == 0)
++frequency[loc]; //word found
else //this is a new word
if (numWords < MaxWords) { //if table is not full
addToList(word, MaxWordBuffer, wordList, frequency, loc,
numWords-1);
++numWords;
}
else fprintf(out, "'%s' not added to table\n", word);
}
printResults(out, MaxWordBuffer, wordList, frequency, numWords);
} // end main
int getWord(FILE * in, char str[]) {
// store the next word, if any, in str; convert word to lowercase
// return 1 if a word is found; 0, otherwise
char ch;
int n = 0;
// read over white space
while (!isalpha(ch = getc(in)) && ch != EOF) ; //empty while body
if (ch == EOF) return 0;
str[n++] = tolower(ch);
while (isalpha(ch = getc(in)) && ch != EOF)
if (n < MaxLength) str[n++] = tolower(ch);
str[n] = '\0';
return 1;
} // end getWord
int binarySearch(int lo, int hi, char key[], int max, char list[][max]) {
//search for key from list[lo] to list[hi]
//if found, return its location;
//if not found, return the location in which it should be inserted
//the calling program will check the location to determine if found
while (lo <= hi) {
int mid = (lo + hi) / 2;
int cmp = strcmp(key, list[mid]);
if (cmp == 0) return mid; // found
if (cmp < 0) hi = mid - 1;
else lo = mid + 1;
}
return lo; //not found; should be inserted in location lo
} //end binarySearch
void addToList(char item[], int max, char list[][max],
int freq[], int p, int n) {
//adds item in position list[p]; sets freq[p] to 1
//shifts list[n] down to list[p] to the right
for (int h = n; h >= p; h--) {
strcpy(list[h+1], list[h]);
freq[h+1] = freq[h];
}
strcpy(list[p], item);
freq[p] = 1;
} //end addToList
void printResults(FILE *out, int max, char list[][max],
int freq[], int n) {
fprintf(out, "\nWords Frequency\n\n");
for (int h = 0; h < n; h++)
fprintf(out, "%-15s %2d\n", list[h], freq[h]);
} //end printResults
假设文件passage.txt
包含以下数据(来自 Rudyard Kipling 的 If):
If you can dream—and not make dreams your master;
If you can think—and not make thoughts your aim;
If you can meet with Triumph and Disaster
And treat those two impostors just the same...
If you can fill the unforgiving minute
With sixty seconds’ worth of distance run,
Yours is the Earth...
使用这些数据运行程序 P9.4 时,它会产生以下输出:
Words Frequency
aim 1
and 4
can 4
disaster 1
distance 1
dream 1
dreams 1
earth 1
fill 1
if 4
impostors 1
is 1
just 1
make 2
master 1
meet 1
minute 1
not 2
of 1
run 1
same 1
seconds 1
sixty 1
the 3
think 1
those 1
thoughts 1
treat 1
triumph 1
two 1
unforgivin 1
with 2
worth 1
you 4
your 2
yours
1
对 P9.4 计划的评论
- 出于我们的目的,我们假设一个单词以字母开头,并且只由字母组成。如果您想包含其他字符(如连字符或撇号),您只需更改
getWord
函数。 MaxWords
表示满足的不同单词的最大数量。为了测试程序,我们使用了50
作为这个值。如果文章中不同单词的数量超过了MaxWords
(50
),那么50
th 之后的所有单词都将被读取,但不会被存储,并且会打印一条大意如此的消息。然而,如果再次遇到,已经存储的单词的计数将增加。MaxLength
(我们用10
来测试)表示一个单词的最大长度。字符串使用MaxLength+1
(定义为MaxWordBuffer
)来声明,以迎合\0
,它必须添加在每个字符串的末尾。main
检查输入文件是否存在,输出文件是否可以创建。接下来,它将频率计数初始化为0
。然后,它根据本节开始时显示的大纲处理文章中的单词。getWord
读取输入文件并存储在其字符串参数中找到的下一个单词。如果找到一个单词,它返回1
,否则返回0
。如果一个单词比MaxLength
长,则只存储第一个MaxLength
字母;其余的被读取并丢弃。例如,使用字长10
将unforgiving
截断为unforgivin
。- 所有单词都被转换成小写,例如,
The
和the
被视为同一个单词。 - 我们编写了
binarySearch
,这样如果找到这个单词,就会返回它的位置(loc
)。如果没有找到,则返回单词应该插入的位置。测试 if (strcmp(word, wordList[loc]) == 0)
- 确定是否找到了它。
addToList
被赋予插入新单词的位置。该位置右侧的单词(包括该位置)将被移动,以便为新单词腾出空间。 - 在声明函数原型时,一些编译器允许像在
char [][]
中那样声明一个二维数组参数,没有为任何一个维度指定大小。其他要求必须指定第二维的大小。指定第二维的大小应该适用于所有编译器。在我们的程序中,我们使用参数max
指定第二维度,调用函数时将提供该参数的值。
9.8 合并排序列表
合并是将两个或多个有序列表合并成一个有序列表的过程。例如,给定两个数字列表,A
和B
,如下所示:
A: 21 28 35 40 61 75
B: 16 25 47 54
它们可以组合成一个有序列表C
,如下所示:
C: 16 21 25 28 35 40 47 54 61 75
列表C
包含列表A
和B
中的所有数字。如何执行合并?
一种思考方式是想象给定列表中的数字存储在卡片上,每张卡片一个,卡片面朝上放在桌子上,最小的放在顶部。我们可以如下想象列表 A 和 B:
21 16
28 25
35 47
40 54
61
75
我们看最上面的两张卡,21
和16
。较小的16
被移除并放置在C
中。这就暴露了25
这个数字。我们有这个:
21 25
28 47
35 54
40
61
75
现在最上面的两张卡是21
和25
。较小的21
被移除并添加到C
,?? 现在包含了16 21
。这就暴露了数字28
。我们有这个:
28
25
35 47
40 54
61
75
现在最上面的两张卡是28
和25
。较小的25
被移除并添加到C
,?? 现在包含了16 21 25
。这就暴露了数字47
。我们有这个:
28 47
35 54
40
61
75
现在最上面的两张卡是28
和47
。较小的28
被移除并添加到C
,?? 现在包含了16 21 25 28
。这就暴露了数字35
。我们有这个:
35 47
40 54
61
75
现在最上面的两张卡是35
和47
。较小的35
被移除并添加到C
,?? 现在包含了16 21 25 28 35
。这就暴露了数字40
。我们有这个:
40 47
61 54
75
现在最上面的两张卡是40
和47
。较小的40
被移除并添加到C
,?? 现在包含了16 21 25 28 35 40
。这就暴露了数字61
。我们有这个:
61 47
75 54
现在最上面的两张卡是61
和47
。较小的47
被移除并添加到C
,?? 现在包含了16 21 25 28 35 40 47
。这就暴露了数字54
。我们有这个:
61 54
75
现在最上面的两张卡是61
和54
。较小的54
被移除并添加到C
,?? 现在包含了16 21 25 28 35 40 47 54
。列表B
没有更多数字。
我们将A
的剩余元素(61 75
)复制到C
,现在包含以下内容:
16 21 25 28 35 40 47 54 61 75
合并现已完成。
在合并的每一步,我们将最小剩余数A
与最小剩余数B
进行比较。其中较小的被添加到C
。如果较小的数字来自于A
,我们继续前进到A
的下一个数字;如果较小的数字来自B
,我们将继续处理B
中的下一个数字。
重复这一过程,直到使用完A
或B
中的所有号码。如果A
中的所有号码都已被使用,我们将从B
到C
的剩余号码相加。如果B
中的所有数字都已被使用,我们将从A
到C
的剩余数字相加。
我们可以将合并的逻辑表达如下:
while (at least one number remains in both A and B) {
if (smallest in A < smallest in B)
add smallest in A to C
move on to next number in A
else
add smallest in B to C
move on to next number in B
endif
}
if (A has ended) add remaining numbers in B to C
else add remaining numbers in A to
C
实施合并
假设数组A
包含存储在A[0]
到A[m-1],
中的 m 个数字,数组B
包含存储在B[0]
到B[n-1]
中的 n 个数字。假设数字按升序存储。我们希望将A
和B
中的数字合并到另一个数组C
中,这样C[0]
到C[m+n-1]
就包含了A
和B
中按升序排序的所有数字。
我们将使用整数变量i
、j,
和k
来分别下标数组A
、B
和C
。在数组中“移动到下一个位置”可以通过给下标变量加 1 来实现。我们可以用下面的代码实现合并:
i = 0; //i points to the first (smallest) number in A
j = 0; //j points to the first (smallest) number in B
k = -1; //k will be incremented before storing a number in C[k]
while (i < m && j < n) {
if (A[i] < B[j]) C[++k] = A[i++];
else C[++k] = B[j++];
}
if (i == m) //copy B[j] to B[n-1] to C
for ( ; j < n; j++) C[++k] = B[j];
else // j == n, copy A[i] to A[m-1] to C
for ( ; i < m; i++) C[++k] = A[i];
程序 P9.5 显示了一个简单的主函数,它测试了我们方法的逻辑。我们把合并写成一个函数,给定参数A
、m
、B
、n
和C
,执行合并并返回C
中的元素数量m+n
。运行时,程序打印出C
的内容,如下所示:
16 21 25 28 35 40 47 54 61 75
Program P9.5
#include <stdio.h>
int main () {
int merge(int[], int, int[], int, int[]);
int A[] = {21, 28, 35, 40, 61, 75};
int B[] = {16, 25, 47, 54};
int C[20];
int n = merge(A, 6, B, 4, C);
for (int h = 0; h < n; h++) printf("%d ", C[h]);
printf("\n\n");
} //end main
int merge(int A[], int m, int B[], int n, int C[]) {
int i = 0; //i points to the first (smallest) number in A
int j = 0; //j points to the first (smallest) number in B
int k = -1; //k will be incremented before storing a number in C[k]
while (i < m && j < n) {
if (A[i] < B[j]) C[++k] = A[i++];
else C[++k] = B[j++];
}
if (i == m) ///copy B[j] to B[n-1] to C
for ( ; j < n; j++) C[++k] = B[j];
else // j == n, copy A[i] to A[m-1] to C
for ( ; i < m; i++) C[++k] = A[i];
return m + n;
} //end merge
有趣的是,我们也可以如下实现 merge:
int merge(int A[], int m, int B[], int n, int C[]) {
int i = 0; //i points to the first (smallest) number in A
int j = 0; //j points to the first (smallest) number in B
int k = -1; //k will be incremented before storing a number in C[k]
while (i < m || j < n) {
if (i == m) C[++k] = B[j++];
else if (j == n) C[++k] = A[i++];
else if (A[i] < B[j]) C[++k] = A[i++];
else C[++k] = B[j++];
}
return m + n;
} //end merge
while
循环表达了以下逻辑:只要在A
或B
中至少有一个元素要处理,我们就进入循环。如果我们完成了A
( i == m
,从B
到C
复制一个元素。如果我们完成了B
( j == n
),将一个元素从A
复制到C
。否则,将A[i]
和B[j]
中较小的一个复制到C
。每当我们从一个数组中复制一个元素,我们就给这个数组的下标加 1。
虽然以前的版本以一种简单的方式实现了合并,但是说这个版本更简洁似乎是合理的。
EXERCISES 9In the voting problem of Section 8.15, print the results in alphabetical order by candidate name. Hint: in sorting the name
array, when you move a name, make sure and move the corresponding item in the vote
array. In the voting problem of Section 8.15, print the results in descending order by candidate score. Write a function to sort a double
array in ascending order using selection sort. Do the sort by finding the largest number on each pass. Write a program to find out, for a class of students, the number of families with 1, 2, 3, ... up to 8 or more children. The data consists of the number of children in each pupil’s family, terminated by 0
. Print the results in decreasing order by family-size popularity. That is, print the most popular family-size first and the least popular family-size last. A survey of 10 pop artists is made. Each person votes for an artist by specifying the number of the artist (a value from 1 to 10). Write a program to read the names of the artists, followed by the votes, and find out which artist is the most popular. Choose a suitable end-of-data marker. Print a table of the results with the most popular artist first and the least popular last. The median of a set of n numbers (not necessarily distinct) is obtained by arranging the numbers in order and taking the number in the middle. If n is odd, there is a unique middle number. If n is even, then the average of the two middle values is the median. Write a program to read a set of n positive integers (assume n < 100) and print their median; n is not given but 0
indicates the end of the data. The mode of a set of n numbers is the number that appears most frequently. For example, the mode of 7 3 8 5 7 3 1 3 4 8 9
is 3
. Write a program to read a set of n arbitrary positive integers (assume n < 100) and print their mode; n is not given but 0
indicates the end of the data. Write an efficient program to find the mode if it is known that the numbers all lie between 1 and 999, inclusive, with no restriction on the amount of numbers supplied; 0
ends the data. An array num
contains k
numbers in num[0]
to num[k-1]
, sorted in descending order. Write a function insertInPlace
which, given num
, k
and another number x
, inserts x
in its proper position such that num[0]
to num[k]
are sorted in descending order. Assume the array has room for x
. A multiple-choice examination consists of 20 questions. Each question has 5 choices, labeled A, B, C, D, and E. The first line of data contains the correct answers to the 20 questions in the first 20 consecutive character positions, for example: BECDCBAADEBACBAEDDBE
Each subsequent line contains the answers for a candidate. Data on a line consists of a candidate number (an integer), followed by 1 or more spaces, followed by the 20 answers given by the candidate in the next 20 consecutive character positions. An X is used if a candidate did not answer a particular question. You may assume all data are valid and stored in a file exam.dat
. A sample line is: 4325 BECDCBAXDEBACCAEDXBE
There are at most 100 candidates. A line containing a “candidate number” 0
only indicates the end of the data. Points for a question are awarded as follows:– correct answer: 4 points; wrong answer: -1 point; no answer: 0 points. Write a program to process the data and print a report consisting of candidate number and the total points obtained by the candidate, in ascending order by candidate number. At the end, print the average number of points gained by the candidates. An array A
contains integers that first increase in value and then decrease in value, for example:
It is unknown at which point the numbers start to decrease. Write efficient code to copy the numbers from A
to another array B
so that B
is sorted in ascending order. Your code must take advantage of the way the numbers are arranged in A
. (Hint: perform a merge starting at both ends.) You are given two integer arrays A
and B
each of maximum size 500. If A[0]
contains m
, say, then m
numbers are stored in arbitrary order from A[1]
to A[m]
. If B[0]
contains n
, say, then n
numbers are stored in arbitrary order from B[1]
to B[n]
. Write code to merge the elements of A
and B
into another array C
such that C[0]
contains m+n
and C[1]
to C[m+n]
contain the numbers in ascending order. An anagram is a word or phrase formed by rearranging the letters of another word or phrase. Examples of one-word anagrams are: sister/resist and senator/treason. We can get more interesting anagrams if we ignore letter case and punctuation marks. Examples are: Time-table/Bet I’m Late, Clint Eastwood/Old West Action, and Astronomers/No More Stars. Write a function that, given two strings, returns 1
if the strings are anagrams of each other and 0
if they are not. An input file contains one word or phrase per line. Write a program to read the file and output all words/phrases (from the file) that are anagrams of each other. Print a blank line between each group of anagrams.
十、结构
在本章中,我们将解释以下内容:
- 什么是结构
- 如何声明一个结构
- 如何使用 typedef 更方便地处理结构
- 如何使用结构数组
- 如何搜索结构数组
- 如何对结构数组进行排序
- 如何声明嵌套结构
- 如何使用结构处理分数
- 如何使用结构存储并行数组
- 如何将结构传递给函数
10.1 对结构的需求
在 C 语言中,结构是一个或多个变量的集合,这些变量可能是不同类型的,为了方便处理,它们被组合在一个名字下。
在许多情况下,我们希望处理关于某个实体或对象的数据,但是这些数据由各种类型的项目组成。例如,学生的数据(学生记录)可能由几个字段组成,如姓名、地址和电话号码(都是字符串类型);参加的课程数量(整数);应付费用(浮点);课程名称(字符串);获得的成绩(性格);等等。
汽车的数据可能包括制造商、型号和注册号码(字符串);座位容量和燃料容量(整数);以及里程和价格(浮点)。对于一本书,我们可能想存储作者和书名(字符串);价格(浮点);页数(整数);装订类型:精装、平装、螺旋(线装);和库存份数(整数)。
假设我们想在一个程序中存储 100 名学生的数据。一种方法是每个字段有一个单独的数组,并使用下标将字段链接在一起。因此,name[i]
、address[i]
、fees[i]
等等,引用第i
个学生的数据。
这种方法的问题是,如果有许多字段,那么处理几个并行数组就变得笨拙而不实用。例如,假设我们想通过参数列表将学生的数据传递给一个函数。这将涉及几个数组的传递。此外,如果我们按姓名对学生进行排序,比方说,每次两个姓名互换时,我们都必须编写语句来交换其他数组中的数据。在这种情况下,使用 C 结构很方便。
10.2 如何声明一个结构
考虑在程序中存储日期的问题。日期由三部分组成:日、月和年。这些部分中的每一个都可以用一个整数来表示。例如,日期“2006 年 9 月 14 日”可以用日 14 来表示;月份,9;2006 年。我们说日期由三个字段组成,每个字段都是整数。
如果我们愿意,我们也可以用月份的名称来表示日期,而不是月份的数字。在这种情况下,日期由三个字段组成,其中一个是字符串,另外两个是整数。
在 C 中,我们可以使用关键字struct
将日期类型声明为一个结构。请考虑以下声明:
struct date {int day, month, year;};
它由关键字struct
组成,后跟我们选择的结构名称(在本例中为date
);接下来是用左右括号括起来的字段声明。注意右括号前声明末尾的分号——这是分号结束声明的常见情况。右括号后面是分号,结束了struct
声明。
我们也可以编写如下声明,其中每个字段都是单独声明的:
struct date {
int day;
int month;
int year;
};
这可以按如下方式编写,但上面的样式更具可读性:
struct date {int day; int month; int year;};
给定struct
声明,我们可以声明类型struct date
的变量,如下所示:
struct date dob; //to hold a "date of birth"
这将dob
声明为date
类型的“结构变量”。它有三个字段,分别叫做day
、month
和year
。这可以如下图所示:
我们将日字段称为dob.day
,月字段称为dob.month
,年字段称为dob.year
。在 C 语言中,这里使用的句点(.
)被称为结构成员运算符。
一般来说,字段是由结构变量名指定的,后面跟一个句点,再后面跟字段名。
我们可以一次声明多个变量,如下所示:
struct date borrowed, returned; //for a book in a library, say
这些变量中的每一个都有三个字段:day
、month
和year
。borrowed
的字段由borrowed.day
、borrowed.month
和borrowed.year
引用。返回的字段由returned.day
、returned.month
和returned.year
引用。
在这个例子中,每个字段都是一个int
,并且可以在任何可以使用int
变量的上下文中使用。例如,要将日期“2015 年 11 月 14 日”指定给dob
,我们可以这样使用:
dob.day = 14;
dob.month = 11;
dob.year = 2015;
这可以如下图所示:
我们也可以用下面的代码读取day
、month
和year
的值:
scanf("%d %d %d", &dob.day, &dob.month, &dob.year);
假设today
被声明如下:
struct date today;
假设我们已经在today
中存储了一个值,那么我们可以将today
的所有字段分配给dob
,如下所示:
dob = today;
这条语句相当于以下语句:
dob.day = today.day;
dob.month = today.month;
dob.year = today.year;
我们可以这样打印dob
的“值”:
printf("The party is on %d/%d/%d\n", dob.day, dob.month, dob.year);
对于此示例,将打印以下内容:
The party is on 14/11/2015
请注意,每个字段都必须单独打印。我们也可以编写一个函数printDate
,比如说,它打印一个作为参数给出的日期。下面的程序展示了如何编写和使用printDate
。
#include <stdio.h>
struct date {
int day;
int month;
int year;
};
int main() {
struct date dob;
void printDate(struct date);
dob.day = 14 ;
dob.month = 11;
dob.year = 2015;
printDate(dob);
}
void printDate(struct date d) {
printf("%d/%d/%d \n", d.day, d.month, d.year);
}
运行时,该程序打印
14/11/2015
我们顺便注意到,C 在标准库中提供了一个日期和时间结构tm
。除了日期之外,它还提供了精确到秒的时间。要使用它,您的程序前面必须有以下内容:
#include <time.h>
与单个单词类型int
或double
相比,struct date
这个结构使用起来有点麻烦。幸运的是,C 为我们提供了typedef
来使处理结构变得更加方便。
10.2.1 typedef
我们可以使用typedef
给某个现有的类型命名,然后这个名称可以用来声明该类型的变量。我们还可以使用typedef
为预定义的 C 类型或用户声明的类型(如结构)构造更短或更有意义的名称。例如,下面的语句声明了一个新的类型名Whole
,它与预定义类型int
同义:
typedef int Whole;
请注意,Whole
出现在与变量相同的位置,而不是紧接在单词typedef
之后。然后我们可以声明Whole
类型的变量,如下所示:
Whole amount, numCopies;
这完全等同于
int amount, numCopies;
对于那些习惯了 Pascal 或 FORTRAN 等语言的术语real
的人来说,下面的语句允许他们声明类型为Real
的变量:
typedef float Real;
在本书中,我们至少使用一个大写字母来区分使用typedef
声明的类型名。
我们可以用下面的声明给前面显示的日期结构取一个简短而有意义的名字Date
:
typedef struct date {
int day;
int month;
int year;
} Date;
回想一下,C 区分大写字母和小写字母,使得date
不同于Date
。如果我们愿意,我们可以使用任何其他标识符,比如DateType
,而不是Date
。
我们现在可以声明Date
类型的“结构变量”,如下所示:
Date dob, borrowed, returned;
请注意,与下面的代码相比,这段代码要简洁得多:
struct date dob, borrowed, returned;
由于几乎没有任何理由使用第二种形式,我们可以从上面的声明中省略date
,写成:
typedef struct {
int day;
int month;
int year;
} Date;
此后,只要需要struct
,我们就可以使用Date
。例如,我们可以将printDate
改写如下:
void printDate(Date d) {
printf("%d/%d/%d \n", d.day, d.month, d.year);
}
为了实现日期示例,假设我们想要存储“短”名称——月份的前三个字母,例如Aug
。我们将需要使用这样的声明:
typedef struct {
int day;
char month[4]; //one position for \0 to end string
int year;
} Date;
我们可以在变量dob
的Date
中表示日期“2015 年 11 月 14 日”,如下所示:
dob.day = 14;
strcpy(dob.month, "Nov");//remember to #include <string.h> to use
strcpy
dob.year = 2015;
我们可以这样写printDate
:
void printDate(Date d) {
printf("%s %d, %d \n", d.month, d.day, d.year);
}
电话
printDate(dob);
将打印以下内容:
Nov 14, 2015
假设我们想要存储关于学生的信息。对于每个学生,我们希望存储他们的姓名、年龄和性别(男性或女性)。假设一个名称不超过 30 个字符,我们可以使用下面的声明:
typedef struct {
char name[31];
int age;
char gender;
} Student;
我们现在可以声明类型为Student
的变量,如下所示:
Student stud1, stud2;
每个stud1
和stud2
都有自己的字段——name
、age
和gender
。我们可以参考以下这些字段:
stud1.name stud1.age stud1.gender
stud2.name stud2.age stud2.gender
像往常一样,我们可以给这些字段赋值,或者将值读入这些字段。而且,如果我们愿意,我们可以用一条语句将stud1
的所有字段分配给stud2
:
stud2 = stud1;
结构的排列
假设我们想要存储 100 名学生的数据。我们将需要一个大小为 100 的数组,数组的每个元素将保存一个学生的数据。因此,每个元素都必须是一个结构——我们需要一个“结构阵列”
我们可以用下面的语句来声明数组,类似于我们说“int pupil[100]
”来声明一个大小为 100 的整数数组:
Student pupil[100];
这为pupil[0]
、pupil[1]
、pupil[2
、…
,直到pupil[99]
分配存储。每个元素pupil[i]
由三个字段组成,可参考如下:
pupil[i].name pupil[i].age pupil[i].gender
首先,我们需要在数组中存储一些数据。假设我们有以下格式的数据(姓名、年龄、性别):
"Jones, John" 24 M
"Mohammed, Lisa" 33 F
"Singh, Sandy" 29 F
"Layne, Dennis" 49 M
"END"
假设数据存储在文件input.txt
中,并且in
声明如下:
FILE * in = fopen("input.txt", "r");
如果str
是一个字符数组,假设我们可以调用这个函数
getString(in, str)
将下一个带引号的数据字符串存储在str
中,不带引号。还假设readChar(in)
将读取数据并返回下一个非空白字符。
练习:编写函数getString
和readChar
。
我们可以用下面的代码将数据读入数组pupil
:
int n = 0;
char temp[31];
getString(in, temp);
while (strcmp(temp, "END") != 0) {
strcpy(pupil[n].name, temp);
fscanf(in, "%d", &pupil[n].age);
pupil[n].gender = readChar(in);
n++;
getString(in, temp);
}
最后,n
包含存储的学生人数,pupil[0]
到pupil[n-1]
包含这些学生的数据。
为了确保我们不会试图存储超过我们在数组中的空间的数据,我们应该检查n
是否在数组的边界内。假设MaxItems
的值为100
,这可以通过将while
条件改为如下来实现:
while (n < MaxItems && strcmp(temp, "END") != 0)
或者在语句n++
后插入以下内容;在循环内部:
if (n == MaxItems) break;
10.4 搜索结构数组
有了存储在数组中的数据,我们可以用各种方式操纵它。例如,我们可以编写一个函数来搜索一个给定的名字。假设数据没有按特定顺序存储,我们可以使用如下顺序搜索:
int search(char key[], Student list[], int n) {
//search for key in list[0] to list[n-1]
//if found, return the location; if not found, return -1
for (int h = 0; h < n; h++)
if (strcmp(key, list[h].name) == 0) return h;
return -1;
} //end search
给定前面的数据,调用
search("Singh, Sandy", pupil, 4)
将返回2
,下面的调用将返回-1
:
search("Layne, Sandy", pupil, 4)
10.5 对结构数组进行排序
假设我们需要按姓名字母顺序排列的学生名单。需要对数组pupil
进行排序。下面的函数使用插入排序来完成这项工作。这个过程与对一个int
数组进行排序是一样的,除了name
字段用于控制排序。
void sort(Student list[], int n) {
//sort list[0] to list[n-1] by name using an insertion sort
Student temp;
int k;
for (int h = 1; h < n; h++) {
Student temp = list[h];
k = h - 1;
while (k >= 0 && strcmp(temp.name, list[k].name) < 0) {
list[k + 1] = list[k];
k = k - 1;
}
}
list[k + 1] = temp;
} //end sort
请遵守以下声明:
list[k + 1] = list[k];
这将把list[k]
的所有字段分配给list[k+1]
。
如果我们想按年龄对学生进行排序,我们需要改变的只是while
条件。为了按升序排序,我们这样写:
while (k >= 0 && temp.age < list[k].age)
//move smaller numbers to the left
要按降序排序,我们这样写:
while (k >= 0 && temp.age > list[k].age)
//move bigger numbers to the left
我们甚至可以根据性别字段将列表分为男生和女生。因为按字母顺序 F 在 M 之前,我们可以把女性放在第一位,写为:
while (k >= 0 && temp.gender < list[k].gender)
//move Fs to the left
我们可以把男性放在第一位:
while (k >= 0 && temp.gender > list[k].gender)
//move Ms to the
left
10.6 读取、搜索和排序结构
我们通过编写程序 P10.1 来说明前面讨论的思想。该程序执行以下操作:
- 从文件
input.txt
中读取学生的数据,并将它们存储在一个结构数组中。 - 按照数组中存储的顺序打印数据。
- 测试通过读取几个名字并在数组中查找它们来进行搜索。
- 按照
name
的字母顺序对数据进行排序。 - 打印排序后的数据。
该程序还说明了如何编写函数getString
和readChar
。getString
让我们读取包含在任何“分隔符”字符中的字符串。例如,我们可以指定一个字符串为$John Smith$
或"John Smith."
,这是一种非常灵活的指定字符串的方式。每个字符串都可以指定自己的分隔符,对于下一个字符串,分隔符可能会有所不同。它对于指定可能包含特殊字符(如双引号)的字符串特别有用,而不必使用转义序列(如\"
)。
Program P10.1
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#define MaxStudents 100
#define MaxNameLength 30
#define MaxNameBuffer MaxNameLength+1
typedef struct {
char name[MaxNameBuffer];
int age;
char gender;
} Student;
int main() {
Student pupil[MaxStudents];
char aName[MaxNameBuffer];
void getString(FILE *, char[]);
int getData(FILE *, Student[]);
int search(char[], Student[], int);
void sort(Student[], int);
void printStudent(Student);
void getString(FILE *, char[]);
FILE * in = fopen("input.txt", "r");
if (in == NULL) {
printf("Error opening input file.\n");
exit(1);
}
int numStudents = getData(in, pupil);
if (numStudents == 0) {
printf("No data supplied for students");
exit(1);
}
printf("\n");
for (int h = 0; h < numStudents; h++) printStudent(pupil[h]);
printf("\n");
getString(in, aName);
while (strcmp(aName, "END") != 0) {
int ans = search(aName, pupil, numStudents);
if (ans == -1) printf("%s not found\n", aName);
else printf("%s found at location %d\n", aName, ans);
getString(in, aName);
}
sort(pupil, numStudents);
printf("\n");
for (int h = 0; h < numStudents; h++) printStudent(pupil[h]);
} //end main
void printStudent(Student t) {
printf("Name: %s Age: %d Gender: %c\n", t.name, t.age, t.gender);
} //end printStudent
int getData(FILE *in, Student list[]) {
char temp[MaxNameBuffer];
void getString(FILE *, char[]);
char readChar(FILE *);
int n = 0;
getString(in, temp);
while (n < MaxStudents && strcmp(temp, "END") != 0) {
strcpy(list[n].name, temp);
fscanf(in, "%d", &list[n].age);
list[n].gender = readChar(in);
n++;
getString(in, temp);
}
return n;
} //end getData
int search(char key[], Student list[], int n) {
//search for key in list[0] to list[n-1]
//if found, return the location; if not found, return -1
for (int h = 0; h < n; h++)
if (strcmp(key, list[h].name) == 0) return h;
return -1;
} //end search
void sort(Student list[], int n) {
//sort list[0] to list[n-1] by name using an insertion sort
Student temp;
int k;
for (int h = 1; h < n; h++) {
temp = list[h];
k = h - 1;
while (k >= 0 && strcmp(temp.name, list[k].name) < 0) {
list[k + 1] = list[k];
k = k - 1;
}
list[k + 1] = temp;
} //end for
} //end sort
void getString(FILE * in, char str[]) {
// stores, in str, the next string within delimiters
// the first non-whitespace character is the delimiter
// the string is read from the file 'in'
char ch, delim;
int n = 0;
str[0] = '\0';
// read over white space
while (isspace(ch = getc(in))) ; //empty while body
if (ch == EOF) return;
delim = ch;
while (((ch = getc(in)) != delim) && (ch != EOF))
str[n++] = ch;
str[n] = '\0';
} // end getString
char readChar(FILE * in) {
char ch;
while (isspace(ch = getc(in))) ; //empty while body
return ch;
} //end readChar
假设文件input.txt
包含以下数据:
"Jones, John" 24 M
"Mohammed, Lisa" 33 F
"Singh, Sandy" 29 F
"Layne, Dennis" 49 M
"Singh, Cindy" 16 F
"Ali, Imran" 39 M
"Kelly, Trudy" 30 F
"Cox, Kerry" 25 M
"END"
"Kelly, Trudy"
"Layne, Dennis"
"Layne, Cindy"
"END"
该程序打印以下内容:
Name: Jones, John Age: 24 Gender: M
Name: Mohammed, Lisa Age: 33 Gender: F
Name: Singh, Sandy Age: 29 Gender: F
Name: Layne, Dennis Age: 49 Gender: M
Name: Singh, Cindy Age: 16 Gender: F
Name: Ali, Imran Age: 39 Gender: M
Name: Kelly, Trudy Age: 30 Gender: F
Name: Cox, Kerry Age: 25 Gender: M
Kelly, Trudy found at location 6
Layne, Dennis found at location 3
Layne, Cindy not found
Name: Ali, Imran Age: 39 Gender: M
Name: Cox, Kerry Age: 25 Gender: M
Name: Jones, John Age: 24 Gender: M
Name: Kelly, Trudy Age: 30 Gender: F
Name: Layne, Dennis Age: 49 Gender: M
Name: Mohammed, Lisa Age: 33 Gender: F
Name: Singh, Cindy Age: 16 Gender: F
Name: Singh, Sandy Age: 29 Gender:
F
10.7 嵌套结构
c 允许我们使用一个结构作为另一个结构定义的一部分——结构中的结构,称为嵌套结构。考虑一下Student
结构。假设我们想存储学生的出生日期,而不是年龄。这可能是一个更好的选择,因为学生的出生日期是固定的,而他的年龄是变化的,并且该字段必须每年更新。
我们可以使用下面的声明:
typedef struct {
char name[31];
Date dob;
char gender;
} Student;
如果mary
是一个Student
类型的变量,那么mary.dob
指的是她的出生日期。但是mary.dob
本身就是一个Date
结构。如果需要,我们可以用mary.dob.day
、mary.dob.month
、mary.dob.year
来指代它的字段。
如果我们想以更灵活的方式存储姓名,例如,名、中间名和姓,我们可以使用这样的结构:
typedef struct {
char first[21];
char middle;
char last[21];
} Name;
现在,Student
结构变成如下,它包含两个结构,Name
和Date
:
typedef struct {
Name name; //assumes Name has already been declared
Date dob; //assumes Date has already been declared
char gender;
} Student;
如果st
是类型Student
的变量,
st.name
是指Name
类型的结构;
st.name.first
指学生的名字;和
st.name.last[0]
指她姓氏的第一个字母。
现在,如果我们想按姓氏对数组pupil
进行排序,函数sort
中的while
条件就变成这样:
while (k >= 0 && strcmp(temp.name.last, pupil[k].name.last) < 0)
一个结构可以嵌套到你想要的深度。点(.
)运算符从左到右关联。如果a
、b
和c
是结构,则构造
a.b.c.
d
被解释为
((a.b).c).d
10.8 使用分数
考虑处理分数的问题,其中分数由两个整数值表示:一个表示分子,另一个表示分母。例如,5/9
由两个数字5
和9
表示。
我们将使用以下结构来表示分数:
typedef struct {
int num;
int den;
} Fraction;
如果f
是Fraction
类型的变量,我们可以用这个在f
中存储 5/9:
f.num = 5;
f.den = 9;
这可以如下图所示:
我们还可以读取代表分数的两个值,并使用如下语句将它们存储在f
中:
scanf("%d %d", &f.num, &f.den);
我们可以写一个函数printFraction
,来打印一个分数。它显示在下面的程序中。
#include <stdio.h>
typedef struct {
int num;
int den;
} Fraction;
int main() {
void printFraction(Fraction);
Fraction f;
f.num = 5;
f.den = 9;
printFraction(f);
}
void printFraction(Fraction f) {
printf("%d/%d", f.num, f.den);
}
运行时,该程序将打印
5/9
操纵分数
我们可以编写函数对分数进行各种运算。例如,由于
我们可以编写一个函数将两个分数相加如下:
Fraction addFraction(Fraction a, Fraction b) {
Fraction c;
c.num = a.num * b.den + a.den * b.num;
c.den = a.den * b.den;
return c;
} //end addFraction
类似地,我们可以编写函数来加减乘除分数。
Fraction subFraction(Fraction a, Fraction b) {
Fraction c;
c.num = a.num * b.den - a.den * b.num;
c.den = a.den * b.den;
return c;
} //end subFraction
Fraction mulFraction(Fraction a, Fraction b) {
Fraction c;
c.num = a.num * b.num;
c.den = a.den * b.den;
return c;
} //end mulFraction
Fraction divFraction(Fraction a, Fraction b) {
Fraction c;
c.num = a.num * b.den;
c.den = a.den * b.num;
return c;
} //end divFraction
为了说明它们的用途,假设我们想找到
{ 的2537+58
我们可以使用以下语句来实现这一点:
Fraction a, b, c, sum, ans;
a.num = 2; a.den = 5;
b.num = 3; b.den = 7;
c.num = 5; c.den = 8;
sum = addFraction(b, c);
ans = mulFraction(a, sum);
printFraction(ans);
严格地说,变量sum
和ans
不是必需的,但是我们用它们来简化解释。因为函数的参数可以是一个表达式,所以我们可以得到相同的结果:
printFraction(mulFraction(a, addFraction(b, c)));
运行时,此代码将打印以下内容,正确答案是:
118/280
然而,如果你愿意,你可以写一个函数把一个分数化为它的最低项。这可以通过找到分子和分母的最大公因数(HCF)来实现。然后将分子和分母除以它们的 HCF。例如,118 和 280 的 HCF 是 2,因此118/280
减少为59/140
。编写这个函数作为一个练习。
10.9 投票问题
这个例子将用来说明关于函数参数传递的几个要点。它进一步强调了数组参数和简单变量参数之间的区别。我们将展示一个函数如何通过使用一个结构向一个调用函数返回多个值。为此,我们将编写一个程序来解决我们在 8.15 节中遇到的投票问题。又来了:
- 问题:在一次选举中,有七名候选人。每个选民都被允许为他们选择的候选人投一票。投票记录为从 1 到 7 的数字。投票人数事先未知,但以
0
票终止投票。任何不是从 1 到 7 的数字的投票都是无效的。 - 文件
votes.txt
包含候选人的姓名。第一个名字被视为候选人 1,第二个被视为候选人 2,依此类推。名字后面是投票。写一个程序来读取数据并评估选举的结果。打印所有输出到文件,results.txt
。 - 您的输出应该指定总投票数、有效投票数和无效投票数。接下来是每位候选人和选举获胜者获得的票数。
假设文件votes.txt
包含以下数据:
Victor Taylor
Denise Duncan
Kamal Ramdhan
Michael Ali
Anisa Sawh
Carol Khan
Gary Olliverie
3 1 2 5 4 3 5 3 5 3 2 8 1 6 7 7 3 5
6 9 3 4 7 1 2 4 5 5 1 4 0
您的程序应该向results.txt
发送以下输出:
Invalid vote: 8
Invalid vote: 9
Number of voters: 30
Number of valid votes: 28
Number of spoilt votes: 2
Candidate Score
Victor Taylor 4
Denise Duncan 3
Kamal Ramdhan 6
Michael Ali 4
Anisa Sawh 6
Carol Khan 2
Gary Olliverie 3
The winner(s):
Kamal Ramdhan
Anisa
Sawh
我们现在解释如何使用 C 结构解决这个问题。考虑这些声明:
typedef struct {
char name[31];
int numVotes;
} PersonData;
PersonData candidate[8];
这里,candidate
是一个结构体数组。我们将使用candidate[1]
到candidate[7]
来表示七个候选人;我们不会用candidate[0]
。这将使我们更自然地处理选票。投个票(v
,说吧),candidate[v]
会更新。如果我们使用candidate[0]
,我们会有一个尴尬的情况,投票v
,candidate[v-1]
必须被更新。
元素candidate[h]
不仅仅是单个数据项,而是由两个字段组成的结构。这些字段可参考如下:
candidate[h].name and candidate[h].numVotes
为了使程序灵活,我们将定义以下符号常量:
#define MaxCandidates 7
#define MaxNameLength 30
#define MaxNameBuffer MaxNameLength+1
我们还将前面的声明更改为以下内容:
typedef struct {
char name[MaxNameBuffer];
int numVotes;
} PersonData;
PersonData candidate[MaxCandidates+1];
该解决方案基于以下大纲:
initialize
process the votes
print the
results
函数initialize
将从文件in
中读取名字,并将投票计数设置为0
。该文件作为参数传递给函数。我们将分两部分(名和姓)读取候选人的姓名,然后将它们连接在一起,创建一个单一的姓名,并存储在person[h].name
中。将为max
人员读取数据。下面是函数:
void initialize(PersonData person[], int max, FILE *in) {
char lastName[MaxNameBuffer];
for (int h = 1; h <= max; h++) {
fscanf(in, "%s %s", person[h].name, lastName);
strcat(person[h].name, " ");
strcat(person[h].name, lastName);
person[h].numVotes = 0;
}
} //end initialize
处理投票将基于以下大纲:
get a vote
while the vote is not 0
if the vote is valid
add 1 to validVotes
add 1 to the score of the appropriate candidate
else
print invalid vote
add 1 to spoiltVotes
endif
get a vote
endwhile
在处理完所有的投票后,这个函数需要返回有效投票和无效投票的数量。但是一个函数怎么能返回多个值呢?如果值存储在结构中,并且该结构作为函数的“值”返回,则可以。
我们将使用以下声明:
typedef struct {
int valid, spoilt;
} VoteCount;
我们将把processVotes
写成:
VoteCount processVotes(PersonData person[], int max, FILE *in, FILE *out) {
VoteCount temp;
temp.valid = temp.spoilt = 0;
int v;
fscanf(in, "%d", &v);
while (v != 0) {
if (v < 1 || v > max) {
fprintf(out, "Invalid vote: %d\n", v);
++temp.spoilt;
}
else {
++person[v].numVotes;
++temp.valid;
}
fscanf(in, "%d", &v);
} //end while
return temp;
} //end processVotes
接下来,我们编写main
,前面是编译器指令和结构声明。
#include <stdio.h>
#include <string.h>
#define MaxCandidates 7
#define MaxNameLength 30
#define MaxNameBuffer MaxNameLength+1
typedef struct {
char name[MaxNameBuffer];
int numVotes;
} PersonData;
PersonData candidate[MaxCandidates];
typedef struct {
int valid, spoilt;
} VoteCount;
int main() {
void initialize(PersonData[], int, FILE *);
VoteCount processVotes(PersonData[], int, FILE *, FILE *);
void printResults(PersonData[], int, VoteCount, FILE *);
PersonData candidate[MaxCandidates+1];
VoteCount count;
FILE *in = fopen("votes.txt", "r");
FILE *out = fopen("results.txt", "w");
initialize(candidate, MaxCandidates, in);
count = processVotes(candidate, MaxCandidates, in, out);
printResults(candidate, MaxCandidates, count, out);
fclose(in);
fclose(out);
} //end main
PersonData
和VoteCount
的声明在main
之前。这样做是为了让其他函数可以引用它们,而不必重复整个声明。如果它们是在main
中声明的,那么PersonData
和VoteCount
的名字只会在main
中被知道,其他函数将无法访问它们。
现在我们知道了如何读取和处理投票,剩下的只是确定获胜者并打印结果。我们将把这个任务委托给函数printResults
。
使用样本数据,数组candidate
将在所有投票被统计后包含如下所示的值(记住,我们没有使用candidate[0]
)。
要找到获胜者,我们必须首先找到数组中的最大值。为此,我们将如下调用函数getLargest
:
int win = getLargest(candidate, 1, MaxCandidates);
这将把win
设置为从candidate[1]
到candidate[7]
的numVotes
字段中最大值的下标(因为MaxCandidates
是7
):
在我们的例子中,win
将被设置为3
,因为最大值6
位于位置3
。(6
也在5
位置,但是我们只需要最大值,我们可以从任一位置得到。)
这里是getLargest
:
int getLargest(PersonData person[], int lo, int hi) {
//returns the index of the highest vote from person[lo] to person[hi]
int big = lo;
for (int h = lo + 1; h <= hi; h++)
if (person[h].numVotes > person[big].numVotes) big = h;
return big;
} //end getLargest
现在我们知道最大值在candidate[win].numVotes
中,我们可以“遍历”数组,寻找具有该值的候选值。这样,我们将找到所有得票最高的候选人(如果不止一个),并宣布他们为获胜者。
printResults
的概要如下:
printResults
print the number of voters, valid votes and spoilt votes
print the score of each candidate
determine and print the winner(s)
详细信息在函数printResults
中给出:
void printResults(PersonData person[], int max, VoteCount c, FILE*out) {
int getLargest(PersonData[], int, int);
fprintf(out, "\nNumber of voters: %d\n", c.valid + c.spoilt);
fprintf(out, "Number of valid votes: %d\n", c.valid);
fprintf(out, "Number of spoilt votes: %d\n", c.spoilt);
fprintf(out, "\nCandidate Score\n\n");
for (int h = 1; h <= max; h++)
fprintf(out, "%-15s %3d\n", person[h].name,
person[h].numVotes);
fprintf(out, "\nThe winner(s)\n");
int win = getLargest(person, 1, max);
int winningVote = person[win].numVotes;
for (int h = 1; h <= max; h++)
if (person[h].numVotes == winningVote) fprintf(out, "%s\n",
person[h].name);
} //end printResults
把所有的片段放在一起,我们得到了程序 P10.2,解决投票问题的程序。
Program P10.2
#include <stdio.h>
#include <string.h>
#define MaxCandidates 7
#define MaxNameLength 30
#define MaxNameBuffer MaxNameLength+1
typedef struct {
char name[MaxNameBuffer];
int numVotes;
} PersonData;
PersonData candidate[MaxCandidates];
typedef struct {
int valid, spoilt;
} VoteCount;
int main() {
void initialize(PersonData[], int, FILE *);
VoteCount processVotes(PersonData[], int, FILE *, FILE *);
void printResults(PersonData[], int, VoteCount, FILE *);
PersonData candidate[MaxCandidates+1];
VoteCount count;
FILE *in = fopen("votes.txt", "r");
FILE *out = fopen("results.txt", "w");
initialize(candidate, MaxCandidates, in);
count = processVotes(candidate, MaxCandidates, in, out);
printResults(candidate, MaxCandidates, count, out);
fclose(in);
fclose(out);
} //end main
void initialize(PersonData person[], int max, FILE *in) {
char lastName[MaxNameBuffer];
for (int h = 1; h <= max; h++) {
fscanf(in, "%s %s", person[h].name, lastName);
strcat(person[h].name, " ");
strcat(person[h].name, lastName);
person[h].numVotes = 0;
}
} //end initialize
VoteCount processVotes(PersonData person[], int max, FILE *in, FILE *out) {
VoteCount temp;
temp.valid = temp.spoilt = 0;
int v;
fscanf(in, "%d", &v);
while (v != 0) {
if (v < 1 || v > max) {
fprintf(out, "Invalid vote: %d\n", v);
++temp.spoilt;
}
else {
++person[v].numVotes;
++temp.valid;
}
fscanf(in, "%d", &v);
} //end while
return temp;
} //end processVotes
int getLargest(PersonData person[], int lo, int hi) {
//returns the index of the highest vote from person[lo] to person[hi]
int big = lo;
for (int h = lo + 1; h <= hi; h++)
if (person[h].numVotes > person[big].numVotes) big = h;
return big;
} //end getLargest
void printResults(PersonData person[], int max, VoteCount c, FILE *out) {
int getLargest(PersonData[], int, int);
fprintf(out, "\nNumber of voters: %d\n", c.valid + c.spoilt);
fprintf(out, "Number of valid votes: %d\n", c.valid);
fprintf(out, "Number of spoilt votes: %d\n", c.spoilt);
fprintf(out, "\nCandidate Score\n\n");
for (int h = 1; h <= max; h++)
fprintf(out, "%-15s %3d\n", person[h].name, person[h].numVotes);
fprintf(out, "\nThe winner(s)\n");
int win = getLargest(person, 1, max);
int winningVote = person[win].numVotes;
for (int h = 1; h <= max; h++)
if (person[h].numVotes == winningVote)
fprintf(out, "%s\n", person[h].name);
} //end
printResults
假设需要通过numVotes
按降序打印候选人的姓名。为此,必须使用numVotes
字段控制排序,以降序对结构数组candidate
进行排序。这可以通过以下函数调用来完成:
sortByVote(candidate, 1, MaxCandidates);
sortByVote
使用插入排序,并使用形参person
(任何名字都可以)编写,如下所示:
void sortByVote(PersonData person[], int lo, int hi) {
//sort person[lo..hi] in descending order by numVotes
PersonData insertItem;
// process person[lo+1] to person[hi]
for (int h = lo + 1; h <= hi; h++) {
// insert person h in its proper position
insertItem = person[h];
int k = h -1;
while (k >= lo && insertItem.numVotes > person[k].numVotes) {
person[k + 1] = person[k];
--k;
}
person[k + 1] = insertItem;
}
} //end sortByVote
请注意,函数的结构与我们对一个简单的整数数组进行排序时非常相似。主要的区别是在while
条件中,我们必须指定哪个字段用于确定排序顺序。(在这个例子中,我们也使用了>,而不是person[h]
,我们将其复制到临时结构insertItem
。这释放了person[h]
,以便在必要时person[h-1]
可以移动到位置h
。要将数组元素向右移动,我们使用下面的简单赋值:
person[k + 1] = person[k];
这将移动整个结构(在本例中是两个字段)。
如果我们需要按字母顺序对候选人进行排序,我们可以使用函数sortByName
:
void sortByName(PersonData person[], int lo, int hi) {
//sort person[lo..hi] in alphabetical order by name
PersonData insertItem;
// process person[lo+1] to person[hi]
for (int h = lo + 1; h <= hi; h++) {
// insert person j in its proper position
insertItem = person[h];
int k = h -1;
while (k > 0 && strcmp(insertItem.name, person[k].name) < 0) {
person[k + 1] = person[k];
--k;
}
person[k + 1] = insertItem;
}
} //end sortByName
函数sortByName
与sortByVote
相同,除了while
条件,该条件指定在比较中使用哪个字段,并使用<按升序排序。注意使用标准字符串函数strcmp
来比较两个名字。如果strcmp(s1, s2)
为负,则意味着按照字母顺序,字符串s1
在字符串s2
之前。
作为一个练习,重写解决投票问题的程序,使它按照投票的降序和字母顺序打印结果。
10.10 将结构传递给函数
在投票问题中,我们看到了将结构数组candidate
传递给各种函数的例子。我们现在讨论将结构传递给函数时出现的一些其他问题。
考虑具有以下字段的“书籍类型”的结构:
typedef struct {
char author[31];
char title[51];
char binding; //paperback, hardcover, spiral, etc.
double price;
int quantity; //quantity in stock
} Book;
Book text;
这声明了一个名为Book
的新类型,而text
被声明为Book
类型的变量。
我们可以以通常的方式将单个字段传递给函数;对于简单变量,传递它的值,但是对于数组变量,传递它的地址。因此:
fun1(text.quantity); // value of text.quantity is passed
fun2(text.binding); // value of text.binding is passed
fun3(text.price); // value of text.price is passed
但是,
fun4(text.title); // address of array text.title is passed
我们甚至可以通过标题的第一个字母,如下:
fun5(text.title[0]); // value of first letter of title is passed
为了传递整个结构,我们使用这个:
fun6(text);
当然,这些函数的头必须用适当的参数类型编写。
在最后一个例子中,text
的字段被复制到一个临时位置(称为运行时堆),副本被传递给fun6
;也就是说,该结构是“按值”传递的如果一个结构很复杂或者包含数组,复制操作可能会很耗时。此外,当函数返回时,必须从堆中移除结构元素的值;这增加了开销——执行函数调用所需的额外处理。
为了避免这种开销,可以传递该结构的地址。这可以通过下面的语句来实现:
fun6(&text);
然而,进一步的讨论涉及到指针的更深层次的知识,这超出了本书的范围。
EXERCISES 10Write a program to read names and phone numbers into a structure array. Request a name and print the person’s phone number. Use binary search to look up the name. Write a function that, given two date structures, d1
and d2
, returns -1
if d1
comes before d2
, 0
if d1
is the same as d2
, and 1
if d1
comes after d2
. Write a function that, given two date structures, d1
and d2
, returns the number of days that d2
is ahead of d1
. If d2
comes before d1
, return a negative value. A time in 24-hour clock format is represented by two numbers; for example, 16 45
means the time 16:45: that is, 4:45 p.m. Using a structure to represent a time, write a function that, given two time structures, t1
and t2
, returns the number of minutes from t1
to t2
. For example, if the two given times are 16 45
and 23 25
, your function should return 400
. Modify the function so that it works as follows: if t2
is less than t1
, take it to mean a time for the next day. For example, given the times 20:30
and 6:15
, take this to mean 8.30 p.m. to 6.15 a.m. of the next day. Your function should return 585
. A length, specified in meters and centimeters, is represented by two integers. For example, the length 3m 75cm is represented by 3 75
. Using a structure to represent a length, write functions to compare, add, and subtract two lengths. A file contains the names and distances jumped by athletes in a long-jump competition. Using a structure to hold a name and distance (which is itself a structure as in Exercise 5), write a program to read the data and print a list of names and distance jumped in order of merit (best jumper first). A data file contains registration information for six courses – CS20A, CS21A, CS29A, CS30A, CS35A, and CS36A. Each line of data consists of a seven-digit student registration number followed by six (ordered) values, each of which is 0
or 1
. A value of 1
indicates that the student is registered for the corresponding course; 0
means the student is not. Thus, 1 0 1 0 1 1
means that the student is registered for CS20A, CS29A, CS35A, and CS36A, but not for CS21A and CS30A. You may assume that there are no more than 100 students and a registration number 0
ends the data. Write a program to read the data and produce a class list for each course. Each list consists of the registration numbers of those students taking the course. At a school’s bazaar, activities were divided into stalls. At the close of the bazaar, the manager of each stall submitted information to the principal consisting of the name of the stall, the income earned, and its expenses. Here are some sample data: Games 2300.00 1000.00
Sweets 900.00 1000.00
Create a structure to hold a stall’s data Write a program to read the data and print a report consisting of the stall name and net income (income – expenses), in order of decreasing net income (that is, with the most profitable stall first and the least profitable stall last). In addition, print the number of stalls, the total profit or loss of the bazaar, and the stall(s) that made the most profit. Assume that a line containing xxxxxx
only ends the data.