02 数据与简单计算程序
编程的本质在于实现计算。在这一过程中,程序需要处理各种数据,因此编程同时也是对数据处理的描述,必然涉及对数据本身的定义,以及从数据出发的计算过程的表达。
在初等数学中,我们经常处理类似下面这样的表达式:
\[ -\dfrac{3.24 \times 5+ \sin 2.3}{4} \times 6.24 \]
它描述了基于给定数值,通过三角函数与算术运算求解结果的过程。在 C 语言中,我们可以编写与之对应的计算语句片段:
-(3.24*5+sin(2.3))/4*6.24这段描述在 C 语言中称为表达式。与数学表达式类似,它能描述计算关系,但还有一个独特功能:可以指挥计算机自动执行相应计算并得出具体结果。
编写 C 程序的第一步,就是学会如何写出所需的表达式。要理解并正确书写表达式,必须掌握 C 语言对基本数据和表达式书写方式的规定:表达式可以包含哪些成分?各个基本元素具有什么含义?一个表达式描述了怎样的计算过程?计算的结果是什么?接下来,我们将围绕这些问题展开说明。
本章将首先介绍 C 语言中名字(标识符)的表示方法,接着讲解基本数据的描述方式,以及如何基于这些基本数据元素来表达计算过程、写出正确的表达式。读者将初步接触编程领域的许多重要概念,并了解它们在简单程序中的作用与意义。
2.1 基本字符、标识符和关键字
一个 C 程序是由 C 语言基本字符按照特定形式组成的序列。C 语言基本字符包括:
- 数字:0, 1, 2, 3, 4, 5, 6, 7, 8, 9
- 大小写拉丁字母:az、AZ
- 其他可打印字符(如标点符号、运算符号、括号等)
- 一些特殊字符,如空格符、换行符和制表符等
空格、换行和制表符等统称为空白字符,在程序中主要起分隔作用。
C 语言规定,在程序中的许多地方增加或减少空白字符,不会影响程序的意义。因此,人们常利用这一特性,通过添加空白字符将程序排列成良好的格式,从而增强可读性。应在适当的位置换行,并添加空格或制表符,使程序的呈现形式更好地反映其结构和计算过程。举例来说,第 1 章中的简单 C 程序也可以写成:
#include <stdio.h>
int main(){printf("Good morning!\n");return 0;}但这种写法明显不如分行的版本清晰。对于复杂程序而言,不良的格式会严重影响代码的阅读与理解,甚至可能引发错误。因此,从编写小程序开始,就应当养成良好的代码习惯。本书后续章节会多次探讨各类程序结构的推荐写法,其中的代码示例也体现了这些实践。
正如英文写作中字母和符号先构成单词,再由单词组成句子和段落,C 程序也由字符构成的“单词”组成,这些成分包括名称(如 main、printf)、各类数值(如 125、3.14),以及运算符与其他特殊符号。
标识符(名字)
在程序设计过程中,常常需要先定义某些程序实体,以便在后续代码中使用。为了在定义处和引用处之间建立明确的关联,表明它们指向的是同一个实体,最基本的方式就是为这些程序实体命名——通过名称来链接定义与使用、以及同一实体的不同使用场合。在 C 语言中,这类名称被称作“标识符”,并且语言严格规定了标识符的书写形式。
一个合法的 C 语言标识符是由字母和数字组成的连续字符序列,且首字符必须是字母。序列中不允许出现空格等空白字符。为了使用上的便利,C 语言特别将下划线 _ 也视作字母字符,这意味着下划线可以出现在标识符的任何位置,尤其允许作为标识符的开头。以下是一些标识符的示例:
abcd Beijing C_Programming xt386ex
A_great_machine Small_talk_80
FORTRAN_90 _f2048sin a3b06
通常,以下划线开头的标识符被保留给系统使用。在编写一般程序时,应避免定义此类标识符,以防与系统内部名称冲突,导致程序出错。
此外,若一个字符序列中出现非字母、非数字、非下划线的字符,那么它就不是合法的标识符(尽管其中可能包含部分合法的标识符,例如在 x3+5+y 中,x3 和 y 是标识符,而 +5+ 不是)。另外,不以字母开头的字符序列也绝不构成标识符。
以下是一些非标识符的示例:
3set a[32] $$$ sin(2+5)::ab4== 23x5 +=
在标识符是否相同的问题上,C 语言是区分大小写的。它将同一字母的大写和小写形式视为不同的字符。因此,a 和 A 是不同的字符,也是两个不同的单字符标识符;而 name、Name、NAME、naMe 和 nAME 则是五个互不相同的标识符。
C 语言的合法标识符中包含一个特殊的集合,称为“关键字”。这些关键字具有预定义的特殊含义,不能用于命名其他程序实体,也就是不能作为普通标识符使用。目前我们暂不深入讨论关键字的具体内容,随着后续学习的展开,你会逐渐接触并熟悉它们。所有关键字的完整列表可参见附录 B。
除了不能使用关键字之外,我们在编程时可以自由选择任何符合 C 语法规则的标识符来命名程序中的实体。不过,命名并非无关紧要的细节。长期的编程实践表明,为程序对象选择恰当的名称,能极大地提高代码的可读性和可维护性,因此通常建议使用具有一定语义、能反映对象用途的标识符。
值得一提的是,命名问题并非 C 语言独有。几乎所有的编程语言都会对标识符的命名做出规定。实际上,在整个计算机领域中,命名都是一项基础且重要的活动——无论是文件与目录、应用程序与系统组件,还是图形界面中的图标与按钮,乃至网络中的每一台计算机,都需要合理的命名。因此,“使用有意义的名字”这一原则,在计算机领域中具有广泛的适用性与实践价值。
2.2 数据、类型和简单程序
C 语言将程序处理的基本数据对象划分为不同集合。同一集合内的数据具有相同性质:它们采用统一的书写形式,在实现时使用相同的编码方式(即按相同规则转换为二进制,并占用相同的内存位数),并且能够进行相同的操作。这样的一组数据集合,在 C 语言中称为一个类型。
计算机硬件处理的数据同样区分类型,如字符、整数、浮点数等,CPU 会为不同类型提供不同的指令——例如整数和浮点数就各有其专用的运算指令。高级语言中的类型划分与此一脉相承。
然而,类型不仅是实现层面的对应,它更是计算机科学中的核心概念之一。在学习和使用程序设计语言的过程中,我们会不断与“类型”打交道,请读者特别留意这个概念。
在 C 语言中,基本类型包括字符类型、整数类型、实数类型等。程序中编写和处理的每一个基本数据,都属于某个基本类型。对于具体的 C 语言系统,各基本类型都有确定的编码方式和表示范围。例如,整数类型只包含有限个整数值,它是数学中整数集的一个子集,有可表示的最小值和最大值,超出此范围的整数在该类型中无法表示。
我们很快会在编程中体会到这些限制带来的影响。
C 语言规定了基本类型的名称,这些类型名由一个或几个关键字构成,形式上与之前提到的“标识符”有所不同。本节先介绍几个最常用的类型,它们已足够支持前几章的编程需要;后续章节将逐一讲解 C 语言的所有基本类型。这样安排是为了让讨论尽快进入核心——程序与程序设计。
最后还需说明“字面量”的概念,它指的是直接写在程序中的数据。C 语言为各种基本类型规定了字面量的书写形式。例如,程序中直接写出的整数就是一个“整型字面量”。为简洁起见,人们也常把“整型字面量”简称为“整数”,其他类型也类似。后文在不引起混淆时,会沿用这种简称,仅在需要特别明确时采用更严谨的说法。
2.2.1 几个常用类型
本节介绍几个最常用的类型。
整数类型和整数
为满足不同需求,C 语言提供了多种整数类型,其中最常用的是一般整数类型(简称整型)和长整数类型(简称长整型)。整型的类型名是 int,长整型的类型名是 long int,可简写为 long。二者均为 C 语言的关键字。
不同整数类型的区别主要在于它们可能占用不同的二进制位数,因此具有不同的表示范围。每种整数类型都是数学整数的一个连续子集。int 和 long 类型的取值范围均是以 0 为中心的某个连续整数区间,具体范围将在 2.5 节说明。
整型(即 int 类型)的字面量有多种书写方式,最常用的是十进制写法:即一个连续的数字序列,中间不能包含空格或其他字符。C 语言规定,十进制整数的首位不能是 0,除非该数本身就是 0。以下是一些整型字面量的示例:
123 30425278 0 906
长整数是另一种独立的整数类型。在 C 语言中,长整型字面量有专门的书写格式:在表示数值的数字序列末尾,必须加上字母 l 或 L 作为后缀。由于小写字母 l 容易与数字 1 混淆,通常建议使用大写 L。以下是一些长整型字面量的例子:
123L 304125278L 110L 906L
在 C 语言中,整型字面量前可以添加正号 + 或负号 -,添加负号即表示一个负整数。此外,C 语言还支持其他进制的书写方式,具体规则将在 2.5 节介绍。
实数类型和实数
C 语言提供三种实数类型:单精度浮点类型(float)、双精度类型(double)和长双精度类型(long double)。它们的字面量可分别简称为浮点数、双精度数和长双精度数。所有整数类型与实数类型合称为算术类型。
每个实数类型只能表示数学实数中一个很小的子集,不仅范围有限,精度(有效数字位数)也有限。详细说明参见 2.5 节。
最常用的实数类型是双精度类型 double。双精度数的基本形式是一个数字序列,但必须满足以下条件之一:
- 包含小数点 .(可位于第一位或最后一位);
- 在数字序列后带有指数部分,即以 e 或 E 开头、可带正负号的数字序列,表示以 10 为底的指数(科学记数法);
- 同时包含小数点与指数部分。
下面是一些双精度数的例子:
3.2 3.2E-3 2.45e17 0.038 105.4E-10 304.24E8
下表展示了部分双精度字面量与实际表示的实数值的对应关系:
| 双精度数 | 所表示的实数值 |
|---|---|
| 2E-3 | 0.002 |
| 105.4E-10 | 0.000 000 010 54 |
| 2.45e17 | 245 000 000 000 000 000.0 |
| 304.24E8 | 30 424 000 000.0 |
浮点数(float)的写法与双精度数类似,但必须在末尾加上后缀 f 或 F。长双精度数则需加上后缀 l 或 L。例如:
13.2F 1.7853E-2F 24.68700f .32F 0.337f
12.869L 3.417E34L .05L 5.E88L 1.L
若表示负数,只需在字面量前添加负号即可。
字符类型、字符和字符串
字符类型的数据主要用于程序的输入输出,也广泛应用于文字处理等需要操作字符数据的场景。由于大多数程序都需要与用户交互,接收指令或数据并显示结果,因此字符类型在程序中使用十分频繁。
C 语言中最常用的字符类型名为 char。该类型的取值范围包括计算机所用编码字符集中的所有字符。目前微机和工作站大多采用 ASCII 字符集,其中标准 ASCII 包含 128 个字符(包括大小写字母、数字、标点及控制字符),扩展 ASCII 则包含 256 个字符。在程序运行时,字符数据以其编码形式存储,通常每个字符占用一个字节。
字符字面量由一对单引号括起的单个字符表示,如 '1'、'a'、'D'。部分特殊字符无法直接书写,C 语言为此规定了转义序列,例如换行符写作 '\n'。其他特殊字符的表示方法将在后续介绍。
C 语言还有一个重要特性:字符被视为一种特殊的短整数,允许在程序中直接使用字符的编码值参与算术运算,相关用法将在后文讨论。
字符串是 C 程序中另一类可直接书写的数据,其形式是由双引号括起的一串字符。示例如下:
"CHINA" "Beijing" "Daxue" "Welcome\n"
字符串中的特殊字符也使用转义序列表示,例如第四个字符串末尾的 \n 表示一个换行符。字符串主要用于输入输出。需注意,字符串中的空格是有效字符,例如 "Good morning!\n" 中的空格是该字符串的一部分。
2.2.2 函数 printf 和简单文本输出程序
了解了上面的概念和规定,就可以开始编写最简单的程序了。C 语言里最简单的一类程序是文本输出程序,第 1 章里输出 "Good morning!" 的程序就属于这一类。输出是程序送给外部的信息,可能送到计算机屏幕显示出来供人阅读,也可能送到计算机连接的其他设备。程序的输出很重要,如果没有输出,我们就看不到程序运行的效果。
要想让 C 程序产生输出,需要使用 C 语言的标准函数库(或称标准库)。每个 C 语言系统都提供了标准库,其中有许多常用函数,包括一个名字为 printf 的输出函数(函数名也是标识符)。printf 执行时能输出一些信息,这些信息一般会送到显示器,显示在计算机的屏幕上或特殊窗口中。这个函数的最简单使用形式是:
printf(字符串);也就是说,先写函数名 printf,后面在一对圆括号里写一个符合 C 语言要求的字符串,最后是一个分号。我们称这种形式的程序片段为一个语句,分号是语句的一部分,不能缺少。上面这样的语句可以看作是一个输出语句,表示要求执行标准库函数 printf。写在圆括号里面的描述称为函数的实际参数,简称实参。
在程序运行时,上述语句的作用就是输出括号里的字符串的内容,无论其中包含什么都照原样输出,遇到特殊字符的转义描述也输出与之对应的字符。第 1 章程序示例里写了语句 printf("Good morning!\n");,该语句的执行将输出:
Good morning!
这段输出包含 13 个字符(包括一个空格)和一个换行符。如果执行下面语句:
printf("Welcome\nto\nBeijing!\n");程序会输出三行字符:
Welcome
to
Beijing!
注意,上面语句的字符串里包含三个换行符,它们都被输出,产生了换行的效果。
输出文本的简单程序
要想在程序里用 printf,就要在程序最前面写上下面一行:
#include <stdio.h>这行的作用是通知编译器,本程序要使用标准库里的输入输出函数,要求编译器正确处理这些函数。这种命令行的细节意义将在后面章节介绍。
例 2.1 简单的字符串输出程序。下面的完整 C 程序能产生上面三行输出:
#include <stdio.h>
int main() {
printf("Welcome\nto\nBeijing!\n");
return 0;
}第一行的意思如上所述,随后的空行只是为了清晰,再后 4 行是程序的主要部分:第一行包括关键字 int 和标识符 main,随后是一对圆括号,后面一对花括号里写程序要做的操作。本程序的工作就是执行一个 printf 语句输出三行文字。最后的语句 return 0; 通知程序的执行环境(操作系统)本程序正常结束,其中 return 也是关键字。
如前所说,要运行这个程序,就需要先编辑好这几行代码,将其存入一个文件。然后调用 C 语言的编译器和连接器,生成相应的可执行程序,然后运行这个程序就可以看到输出了。运行一次可以看到该程序输出一次上述的三行文字。
前面的示例程序都有如下形式的外围结构:
int main() {
// 程序语句
return 0;
}这是任何完整的 C 程序里都需要包含的一部分框架,下面的程序实例也都是这样。读者自己写程序时也应该这样做。这一结构的具体意义在下一章讨论。
虽然上面给出的是一个特殊程序,但它实际上代表了一大类程序。只要把这个程序里 printf 函数的实际参数换成另一个字符串,得到的程序就能产生另一段文字。读者可以仿照上面程序,写出能输出各种文本的程序来。学习写程序与学习其他许多东西一样,需要在学习中认真练习。最初的练习是简单模仿,需要参考教科书示例和书中讲解,自己写一些与书上例子类似的东西。随着写出越来越多的、一个比一个复杂的程序,我们对程序本身和所用语言的理解,对如何思考程序问题的认识都将越来越清晰和深刻。
例 2.2 输出长字符串。下面程序输出一个很长的字符串:
#include <stdio.h>
int main() {
printf("this is the poem of the air,\n"
"slowly in silent syllables recorded.\n");
return 0;
}C 语言规定不能在字符串的中间换行,否则编译会出错。例如不能写:
printf("A simple, meaningless, not interesting,
but very long string.");这里的字符串跨过两行,是不合法的。要写很长的字符串,可以将其分开写成几个字符串,字符串之间只有空白字符(空格、换行和制表符),上面例子就是这样。编译器将自动把这样连着写的几个字符串拼为一个长字符串。下面语句也合法:
printf("A simple, meaningless, not interesting"
"but very long string.");虽然看起来括号里有两个字符串,实际送给 printf 的是拼接而成的一个字符串。
C 语言里的每个函数都有规定的使用方式,正确使用才能得到正确结果。如果使用不符合规定,写出的程序可能在编译时报错;也可能通过了编译但产生的程序不能得到正确的运行结果。下面我们会看到更多标准库函数,还要学习如何自定义有用的函数。
输出函数 printf 的格式描述
前面介绍了 printf 的最简单使用形式。实际上 printf 的功能非常强大,可以产生各种复杂形式的输出。其一般使用形式是:
printf(格式描述串, 其他实参1, ..., 其他实参n);这里的其他实参可以有多个。如果格式描述串里不出现特殊字符 %,该串将直接输出,得到前面介绍的效果;如果串里出现了 %,那么就需要其他参数了。例如语句:
printf("%d + %d = %d\n", 2, 3, 5);执行时将产生一行输出:
2 + 3 = 5
对比语句里的参数和实际输出,可以看到格式串 "%d + %d = %d\n" 提供了输出的基本框架,而其中三个 %d 被后面的参数逐个取代。下面是另一个例子:
printf("len: %f, width: %f, area: %f\n", 2.2, 3.5, 7.7);这个语句的执行将输出:
len: 2.200000, width: 3.500000, area: 7.700000
其中的 2.200000 是系统对双精度数 2.2 计算的结果。
以百分号(%)开头的序列在格式描述串里有特殊意义,这种序列称为转换描述。在一个 printf 调用中,格式描述串里转换描述的个数要与其他实参的个数匹配(简单情况是个数相同,详细情况见第 8 章),说明与之对应的参数的转换和输出方式。下表列出了最常用的几个转换描述,它们指定的转换方式,以及与之对应的其他实参应有的类型:
| 转换描述 | 实现的输出转换 | 对应实际参数的类型 |
|---|---|---|
%d |
按整数形式转换并输出 | int 类型 |
%ld |
按长整数形式转换并输出 | long 类型 |
%f |
按带小数点形式转换并输出 | double 类型 |
%e |
按科学表示形式转换并输出 | double 类型 |
%g |
按带小数点形式或科学表示形式转换并输出 | double 类型 |
%Lf |
按带小数点数形式转换并输出 | long double 类型 |
%c |
输出一个字符 | 对应参数应表示字符的编码 |
%s |
输出一个字符串 | 字符串 |
转换描述 %g 是一种方便形式,它可以根据被输出数值的情况自动选用带小数点的形式或带指数的形式。如果用小数形式输出时小数点后面需要太多的前导 0(被输出的数太小),或者小数点前面需要太多的 0(数太大),就会自动用科学形式输出。对长双精度数,与 %e 和 %g 对应的转换描述分别是 %Le 和 %Lg。
一个格式串通常包含一些普通字符和几个转换描述。串中普通字符将被逐个输出,形成实际输出的框架,对串里写着转换描述的地方,实际输出的是对应实参经过转换得到的结果。实际参数和转换描述之间按位置一一对应。
这里要特别强调在格式串中转换描述与其对应实参在类型上的一致性。如果实参类型与转换描述的要求不一致,就不能保证得到正确输出,甚至可能引起严重的程序运行错误。格式串里转换描述的个数和其他参数的个数必须一致,否则也是严重错误,引起的后果无法预料(这一说法表示可能产生严重后果)。初学者常遇到这类情况:程序的输出结果不对,检查其中计算过程却怎么也找不出错误,最后发现是输出语句里的格式转换描述与对应参数不匹配。这种情况值得特别注意。
在使用 printf 函数时,作为实参的不但可以是上面实例中那样的整数或浮点数,还可以是下面介绍的表达式。如果实际参数是表达式,printf 将先求出表达式的值(计算结果),而后再根据格式串的要求转换并输出它们。
2.3 运算符、表达式和计算
为计算机写程序就是描述计算。C 语言描述计算的基本结构是表达式,它由被计算对象(如文字量,后面将介绍其他计算对象)和表示运算的特殊符号(运算符)按照一定规则构造而成。C 语言的运算符大都由一个或两个特殊字符表示(有个别例外)。本节讨论算术运算符的形式和意义,介绍如何用它们构造算术表达式。还要介绍一些与运算符、表达式和计算有关的重要问题。理解了这些问题,才能正确写出所需的表达式。
2.3.1 算术运算符和算术表达式
算术运算符共有 5 个:
| 运算符 | 使用形式 | 意义 |
|---|---|---|
+ |
一元和二元运算符 | 一元表示正号,二元表示加法运算 |
- |
一元和二元运算符 | 一元表示负号,二元表示减法运算 |
* |
二元运算符 | 乘法运算 |
/ |
二元运算符 | 除法运算 |
% |
二元运算符 | 取模运算(求余数) |
一元算术运算符的运算对象写在运算符后面,二元运算符的两个运算对象写在运算符两边。+ 和 - 同时作为一元和二元运算符使用,其他是二元运算符。对表达式里具体的 + 或 -,根据其上下文可以确定是作为哪种运算符使用的。取模就是求余数,例如 17 取模 5 的结果是 2。取模运算符只能用于各种整型,其余运算符可用于所有算术类型。
算术表达式由计算对象(如数值文字量)、算术运算符及圆括号构成,其基本形式与数学上的算术表达式类似。下面是两个算术表达式:
-(28+32)+(16*7-4)
25*(3-6)+234前面说过,C 语言里的数据都有特定的类型。考虑算术表达式时,这一问题就有清晰的表现:我们不但要关心运算的结果,还要关心结果的类型。对同一类型(无论哪个整数或浮点数类型)的一个或两个对象做算术运算,结果还是该类型的值。例如,3+5 的结果是 int 类型的 8,3L+5L 的结果是 long 类型的 8,而 3.2+2.88 的结果是双精度值。这一规定也适用于非简单数值的运算对象。对 3*5+6/3,首先可确定加法的两个运算对象都是 int 类型,进而可知整个表达式的结果类型。在写表达式时,为清晰起见,可以在运算对象和运算符之间适当地加一个空格,这样做不影响程序的意义。
例 2.3 计算圆球体积。写一个程序计算半径为 6.5 厘米的圆球的体积。
根据前面对简单 C 程序的基本形式的说明和解释,表达式的写法,以及 printf 的使用形式,很容易写出下面简单程序:
#include <stdio.h>
int main() {
printf("V = %f cm^3\n", (3.1416*6.5*6.5*6.5)*4.0/3.0);
return 0;
}格式串里写 %f 表示要转换输出的是一个双精度数值,格式串后实参表达式里都是双精度数,计算结果也是双精度数。这个程序运行时将输出:
V = 1150.349200 cm^3
这里又看到了格式描述串的作用:它给出了输出的框架,计算结果嵌入其中。
上面程序示例表示了一类最简单的计算程序的模式,把作为 printf 参数的表达式换成其他表达式,就可以完成各种算术表达式的计算。我们还可以根据需要写出特定的格式串,得到特定的输出形式。写程序时要特别注意表达式的结果类型(如,是 int 还是 double),格式串中的转换描述必须与之对应,否则就是错误。例如,下面程序里就有一个错误,我们无法确定这一程序能给出什么结果,甚至不知道它是否会导致系统崩溃。请设法找出这个程序里的错误,但不要在计算机上试验这个错误程序:
#include <stdio.h>
int main() {
printf("Factorial of %d is %f\n", 7, 1*2*3*4*5*6*7);
return 0;
}2.3.2 表达式的求值
表达式的意义就是由它求出的值,对它的计算过程也称为表达式求值。对于算术表达式,总是先求出运算对象的值,然后再对它们应用相应的运算符。如果运算对象不是简单对象而是子表达式,那就先求出子表达式的值作为实际运算对象。对复杂的算术表达式,这一过程需要一层层进行,直到求出整个表达式的值。
表达式可能很复杂,其中可能有多个运算符,它所确定的计算过程是什么呢?或者说,表达式里的运算符将按什么顺序计算呢?C 语言对表达式求值的规定包括几个方面:运算符的优先级,运算符的结合方式,运算对象的求值顺序,以及括号的意义。下面分别介绍这几个问题,有关运算对象的求值顺序问题在本章最后介绍。
优先级
我们在小学学算术时就知道先乘除后加减,也就是说,乘除的优先级较高,应该先算。C 语言有很多运算符,每个运算符有一个优先级。当不同运算符在表达式里相邻出现时,高优先级的运算符应比低优先级的运算符先算。算术运算符分为三个不同优先级:
- 最高:一元
+、一元- - 次之:
*、/、% - 最低:二元
+、二元-
这样,下面表达式里的加法将最后做,与数学中的情况一样:
5/3 + 24/6*2结合方式
仅靠优先级无法确定上例中子表达式 24/6*2 的计算方式,因为其中相邻的乘和除运算符优先级相同。结合方式负责解决这类问题,它规定相同优先级的运算符相邻出现时的计算方式。C 语言中一元算术运算符自右向左结合;二元算术运算符自左向右结合,优先级相同时左边运算符先算。这样,上例中先算的是 24/6,而后再用计算结果乘另一运算对象 2。此外,-+-8 计算出的还是 8。这一规定也符合数学习惯。
括号
括号使人可以明确地描述表达式中的计算顺序。如果用括号括起表达式中的一部分,括号里面的表达式将先行计算,得到的结果再参与括号外面的其他计算。例如,在下面表达式里,各个步骤的计算顺序都完全确定了:
-(((2+6)*4)/(3+5))括号是供人使用控制计算过程的一种手段。如果直接写出的表达式产生的计算顺序不符合需要,我们可以通过加括号的方式,强制性地要求按特定的顺序计算。
理解了这些规定,就知道如何写出正确的表达式了,进而能写出许多基于算术表达式计算的简单程序。这类程序的框架是:程序里只用一个 printf 语句产生输出,要计算的表达式写在相应的实参位置,这些参数之前是描述输出格式所需的格式串。要特别注意格式串中转换描述和对应参数之间的类型匹配问题。
最后还要指出:如果表达式很复杂,即使括号并不必要,加入一些括号也可能对读程序的人有益。此外,如果一个表达式很长,一行无法写完时可以换行。多行书写的表达式应采取某种对齐方式,以利于人的理解,出现错误也容易发现和改正。
2.3.3 计算和类型
在程序里,参与计算的数据都有特定的类型,因此计算过程中自然地会出现许多与类型有关的问题。下面介绍其中的一些重要问题。
类型对计算结果的影响
计算在具体的类型中进行。对两个 int 类型的数据,运算得到的是 int 类型的结果。对长整数类型、各种实数类型,情况也一样。这一情况将带来许多后果。
首先,int 类型(及 long 类型)的除法是整除,计算得到的商是整数,余数将被丢掉。因此 3/2 和 3.0/2.0 得到的结果不同,前者得到整数 1 而后者得到双精度的 1.5。这种情况有时会带来很迷惑人的结果。例如,两个表达式
1/3*3 和 1*3/3算出不同结果:前一个表达式算出的值是 0 而不是 1。所有整数类型都有这个问题,写程序时必须注意。由于类似原因,下面程序也不能正确算出 68 和 39 的平均值:
#include <stdio.h>
int main() {
printf("Average of %d and %d is %d\n", 68, 39, (68 + 39) / 2);
return 0;
}算术计算中还有一个共性问题:每个类型都有确定的取值范围,超出该范围的值无法在这个类型中表示;另一方面,两个同类型对象的计算结果仍是这个类型的值,但计算的结果完全可能超出该类型的表示范围。如果一个表达式的计算结果超出了类型的表示范围,能得到什么就没有任何保证了。程序运行中出现的这种情况称为“溢出”。
如果在 C 程序运行中发生溢出,程序不报告错误并继续运行下去。C 语言对出现溢出时表达式的值也没有任何规定。但无论如何,发生溢出后得到的结果已不可能是我们希望的东西了,随后的计算也没价值了。举例说,假设在某个 C 系统里的 int 类型用 16 位二进制表示,其表示范围是 -32 768~32 767,那么下面表达式就是不正确的:
32766 + 5因为计算结果超出了 int 类型的最大值 32 767。如果写程序时认识到可能出现溢出,那就应该考虑换一个表示范围更大的类型,对上面例子,可以考虑写:
32766L + 5L各种实数类型的计算中也可能发生溢出,这时可能出现两种不同情况:计算结果可能绝对值过大而无法在类型里表示,这种情况称为“上溢”;也可能结果的绝对值过小(但又不是 0)而无法表示,这种情况称为“下溢”。出现下溢时通常把结果归为 0,这种情况也可能严重影响随后的计算(请考虑用这一结果乘以一个很大的数)。
混合类型计算和类型转换
如果某个二元运算符的两个运算对象类型不同,就出现了混合类型计算。例如:
3.27 + 201这里一个运算对象是 double 而另一个是 int。混合类型计算带来一些新问题。
C 语言里有 int 类型的加法,有 long 类型的加法,也有 double 类型的加法等,所有这些运算都用 + 表示。如果表达式里出现了 +,编译器将根据运算对象的类型确定如何完成计算。例如,很容易确定下面表达式里应该用哪种加法:
3 + 5应当用int类型的加法运算3.0 + 5.0应当用double类型的加法运算
但 C 语言没有混合类型的算术运算。编译器对混合类型计算的处理方式是转换一个(或两个)运算对象的值,得到相同类型的值之后再实际计算。由混合类型计算引起的类型转换称为自动类型转换。所谓“自动”,就是说这种转换不需要在程序里明确写出来。
自动类型转换的基本原则是把表示范围小的类型的值转换为表示范围大的类型的值。前面介绍的几个常用算术类型从小到大的排列顺序是:
int → long → float → double → long double
如果两个运算对象类型不同,系统就把位于左边的类型(较小类型)的值转换为另一运算对象的类型(较大类型)的值,然后用这个新值参与计算。例如,对表达式:
32767 + 2L由于第二个运算对象是长整数,所以整数 32767 将被转换,产生出对应的 long 类型值,然后用这个新值参与计算,最后得到的是 long 类型的 32769。
看另一个混合计算表达式的例子:
2L + 3*4.5计算这个表达式时,先由 int 类型的 3 转换得到 double 类型的 3.0,然后用 3.0 与 4.5 计算,得到 double 类型的结果值。下一步,由 long 类型的 2L 转换得到 double 类型的值后参与计算,最后得到所需结果。图 2-1 形象地描述了这一计算过程,其中虚线表示的是自动加入的转换操作。请注意,自动插入的隐含数值转换动作,总是由具有某类型的原值出发产生一个所需类型的新值,并不改变原值。
2L 3 4.5
| | |
| | |
| v |
| 3.0 ------+
| |
| v
| 13.5
| |
v |
2.0 -------+
|
v
15.5
图 2-1 表达式 2L+3*4.5 的计算过程
下面程序计算这个表达式:
#include <stdio.h>
int main() {
printf("%f", 2L + 3*4.5);
return 0;
}在写这里的格式描述串时,必须仔细弄清楚表达式的结果类型(这里是 double)。
从上面例子可以看出,无论是写程序还是读程序,都要特别注意表达式中计算对象的类型和计算结果的类型,看清楚在哪些地方将发生类型转换,是从什么类型的值转换到什么类型的值。只有这样,才能清楚理解表达式所描述的计算过程。
显式要求类型转换
如果表达式自然形成的计算过程不符合需要,可以加入括号强制要求某种计算顺序。与此类似,如果自动类型转换不满足需要,也可以显式要求做特定的类型转换。显式要求的类型转换称为强制类型转换,其形式是在被转换表达式前写一对括号,括号里写要求转换到的类型名(注意,一个类型名可能包含多个关键字,如 long int)。例如:
(int)(3.6*15.8) + 4这里要求把 3.6*15.8 的计算结果(double 值)转换为 int 值,而后再用这个 int 值参与随后的加法运算。从实数类型到整型的转换方式是直接丢掉小数部分。
还有一些与类型转换有关的问题:
在类型转换中可能丢失信息。上面示例中的显式类型转换显然会丢失信息:从双精度值转换为整数值时,原数的小数部分丢掉了。算术运算的自动转换有时也可能丢失信息,一个典型例子是把
long类型的数据转换到float类型。通常float类型的精度位数比long类型少,因此可能丢失long类型值的低位信息。如果被转换的值无法在转换结果类型里表示,转换结果没有定义。这种情况在算术运算引起的自动转换里不会出现,写强制类型转换时必须注意。
C 语言规定数值类型之间都可以互相转换。允许的其他转换在后面章节讨论。
C 语言把显式类型转换看作一元运算符,具有与其他一元运算符同样的优先级和结合方式。如果在上面的表达式里不写括号,其意义就会不同。请读者自己分析。
类型转换是值的转换,从一个值出发产生出另一个不同类型的值。类型转换并不改变原来的值,而是产生一个新值。这一点也非常重要。
2.4 数学函数和简单计算程序
C 标准库提供了许多有用的函数,其中有一组数学函数,用于计算各种常用数学函数的函数值。了解了这些函数,将大大扩展我们写程序的能力。
2.4.1 函数、函数调用
我们在前面已经见过函数,编程中一直使用的 printf 就是一个函数。标准库里的每个函数实现一项具体计算,printf 完成数据的形式转换和输出。要正确使用一个函数,我们需要(且只需要)知道它的名字(函数名)、使用方式及功能(能完成什么工作)。我们完全不必关心这些函数的功能是怎样实现的,谁实现的,用什么技术实现的,等等。C 语言提供标准库是为编程方便,使人们可以方便地使用这些函数。
举例说,标准库里有一个名为 sin 的函数,它从一个双精度数出发,把这个数值看成弧度,算出它的正弦函数值,得到的结果也是个双精度数。假如我们要计算弧度 2.4 的正弦函数值的两倍,下面的表达式就能算出这个结果:
2.0 * sin(2.4)乘号后的 sin(2.4) 表示要求使用函数 sin,送给它去计算的数是 2.4。表达式还要求对函数的结果做一个乘法以得到最后结果,正好描述了我们所希望的计算。
送给函数的计算对象(一般而言是个表达式,数是表达式的特殊情况)是函数的实际参数,实际参数是使用具体的函数做一次具体计算的出发点。函数计算得到的值称为函数的结果或者返回值。例如,在下面表达式里两次使用了 sin 函数,要求它从两个不同实际参数出发进行计算,表达式的最终结果是这两次函数计算的返回值的乘积:
sin(2.4) * sin(3.98)使用函数的专门术语是函数调用,因此我们说“这个表达式里两次调用了 sin”。
在表达式里使用函数的形式是(根据函数对实参的不同要求情况):
函数名(实际参数)
函数名(实际参数1, 实际参数2)
...函数名后写一对括号,其中根据函数的要求写一个或几个实参表达式,多个实参之间用逗号分隔。通常函数都明确规定了所需实参的个数,如 sin 要求一个双精度的实参。
假设我们要计算两个边长分别为 3.5 米和 4.72 米,两边夹角为 37 度的三角形的面积。根据数学知识,下面表达式完成这一计算:
3.5 * 4.72 * sin(37.0/180.0*3.1416) / 2.0函数的实参可以是任意复杂的表达式,其中还可以包含函数调用,例如 sin(sqrt(3.5))。显然,对一个函数调用,相应函数规定的计算过程只能在算出其实参表达式值之后才能开始。前面使用 printf 的程序示例也多次用到比较复杂的实参表达式。
2.4.2 数学函数及其使用
标准库的数学函数对实参的个数和类型,以及返回值类型都有明确规定。它们大都要求一个 double 实参且返回 double 值,sin 就是这样。我们把 sin 的类型特征表述为:
double sin(double)这种形式说明函数名是 sin,它要求一个双精度参数(用写在括号里的 double 表示),返回双精度值(用写在函数名前的 double 表示)。这种表述方式既简洁又准确,后面会看到这种表述方式在 C 程序里的用途。
标准库的数学函数主要包括:
- 三角函数:
sin,cos,tan - 反三角函数:
asin,acos,atan - 双曲函数:
sinh,cosh,tanh - 指数和对数函数:
- 以 e 为底的指数函数:
exp - 自然对数函数:
log - 以 10 为底的对数函数:
log10
- 以 e 为底的指数函数:
- 其他常用函数:
| 函数 | 功能 | 类型特征 |
|---|---|---|
sqrt |
平方根 | double sqrt(double) |
fabs |
绝对值 | double fabs(double) |
pow |
乘幂,第一个参数作为底,第二个是指数 | double pow(double, double) |
fmod |
实数的余数,两个参数分别是被除数和除数 | double fmod(double, double) |
上面表里没给出类型特征的函数都要求一个参数,参数与返回值的类型都是 double。最后两个函数要求两个 double 参数并返回 double 值。pow 是乘幂函数,它的第一个参数是底,第二个参数是乘幂的指数。表达式:
pow(2.5, 3.4)计算 \(2.5^{3.4}\)。当底为负数时,pow 要求作为指数的参数必须是整数。fmod 求出实数除法的余数(的近似值),它的两个参数分别是被除数和除数。表达式:
fmod(235.74, 3.14159265)求出 235.74 除以 3.14159265 的余数。余数的符号总与被除数相同。
如果要在程序里使用标准库的数学函数,需要在程序最前面另写下面一行:
#include <math.h>例 2.4 已知三角形两边长和夹角求面积。写程序求两邻边长度分别为 3.5 米和 4.72 米,两边夹角为 37 度的三角形的面积。
根据前面的经验及函数调用的写法,这个程序可以写为:
#include <stdio.h>
#include <math.h>
int main() {
printf("Area of the triangle: %f m^2\n",
3.5*4.72*sin(37.0/180*3.1416)/2);
return 0;
}本程序代表本章的又一个程序模式。如果需要完成一个计算,其中不仅需要做算术,还要用一些基本函数,就可以参考这一示例写程序:在 printf 的调用里写好需要计算的表达式,并根据需要给出格式串。请注意,标准库的所有函数(包括数学函数)都用完全小写字母拼写的名字,写函数名时必须注意(C 语言区分字母的大小写)。
数学函数大大提高了我们写程序的能力,所有能通过标准库的基本数学函数和算术运算描述的计算过程,现在我们都能写出相应的程序了。换句话说,我们现在至少已经有了相当于普通科学计算器的编程能力。由于程序里可以写任意长的、具有任意层次嵌套结构的表达式,我们已经能解决许多实际计算问题了。本章习题中的程序都可以用至今为止我们提出的几个程序模式写出来。
2.4.3 函数调用中的类型转换
函数对参数和返回值都有明确的类型规定。当实参表达式的类型与函数的要求不符时,或者函数返回值不符合调用位置的上下文需要时,就出现了类型问题。C 语言规定,如果出现前一种情况,它先把实参求出的值自动转换为函数要求的类型的值,然后再送给函数;后一情况也会根据上下文的要求做转换。下面表达式计算中将出现两次自动类型转换,两次调用 sin 时,都将先把整型参数值转换到 double 值后才送给 sin:
sin(2) * sin(4)假设函数 f 的类型特征为 int f(int),下面表达式里调用 f 时,实参值将从 double 类型(3.56*2.7 的结果类型)转换到 int 类型(f 要求的实参类型),然后送给函数:
4.0 * f(3.56*2.7)而得到 f 的值后,上下文要求用它和 double 值做乘法,做乘法之前,又将先对 f 的结果做 int 到 double 的数值转换。
例 2.5 简单级数计算。计算 \(\sum_{n=1}^{10} sin \dfrac{1}{n}\) 的值。
看到这个问题,初学者很可能立即写出下面的程序:
#include <stdio.h>
#include <math.h>
int main() {
printf("%f\n", sin(1) + sin(1/2) + sin(1/3) + sin(1/4) +
sin(1/5) + sin(1/6) + sin(1/7) + sin(1/8) +
sin(1/9) + sin(1/10));
return 0;
}此后发现这个程序可以正常通过编译,但执行这个程序却发现不能得到正确结果。也就是说,这个程序里有语义错误。为什么呢?如果读者在仔细检查这个程序后还没有发现问题,那么请认真复习一下上面有关数据类型的讨论。
例 2.6 已知三角形三边长求面积。已知三角形边长分别为 3、5、7 厘米,求面积。
我们首先找出已知三边长求三角形面积的公式:\(S=\sqrt{s(s-a)(s-b)(s-c)}\),公式中的 \(s=\dfrac{1}{2}(a+b+c)\)。根据公式可以写出下面的程序:
#include <stdio.h>
#include <math.h>
int main() {
printf("%f\n", sqrt((3+5+7)/2.0*((3+5+7)/2.0-3)*
((3+5+7)/2.0-5)*((3+5+7)/2.0-7)));
return 0;
}由于自动类型转换,这个程序能得到正确的结果。请读者自己分析,在这个程序的执行过程中,哪些地方将会发生类型转换,各是从什么类型转换到什么类型。
语言细节和问题
为避免对 C 语言细节的介绍过长而干扰有关编程技术、思想和方法的讨论,我们将根据需要把一些语言细节的介绍集中放到各章最后。下面是一些与本章所涉及的 C 语言结构有关的细节,供读者参考。
C 语言的字符集
C 语言的基本字符包括:
数字字符:0, 1, 2, 3, 4, 5, 6, 7, 8, 9。
大小写拉丁字母:a~z, A~Z。
其他一些可打印(可以显示)的字符(如标点符号、运算符号、括号等),包括:
~ ! @ # % ^ & * ( ) _ - + = { } [ ] | \ : ; " ' < > , . ? /不必死记这些字符,随着学习进展,读者将很容易记住它们的意义和作用。前面程序用过的字符包括标识符里用的
"_"、算术运算符(共 5 个)、小数点、圆括号和花括号、逗号和分号、表示字符和字符串的单引号和双引号、作用特殊的"%"。其中"%"在表达式里表示取模运算,在printf的格式描述串里表示转换描述。空格符、换行符、制表符。
只能用在字符文字量或字符串文字量里的其他字符。
基本数据类型的一些问题
整数类型
整数与长整数(int 和 long 文字量)还可以用八进制或十六进制形式书写。
八进制形式的整数是数字 0 开始的连续数字序列,其中只允许 0~7 八个数字,其长整数也是在序列最后加 l 或 L。下面是用八进制形式写出的一些整数和长整数:
0236 0527 06254 0531 0765432L
十六进制形式的整数和长整数是由 0x 或 0X 开头的数字序列(长整数也要带后缀字母 l 或 L)。由于数字只有 10 个,而十六进制形式中需要 16 个数字,C 语言采用计算机领域通行的方式,用字母 a~f 或 A~F 表示其余的 6 个十六进制数字,其对应关系是:
| 字母 | 表示的数字 |
|---|---|
| a 或 A | 10 |
| b 或 B | 11 |
| c 或 C | 12 |
| d 或 D | 13 |
| e 或 E | 14 |
| f 或 F | 15 |
下面是用十六进制形式写出的一些整数和长整数:
0x2073 0xA3B5 0XABCD 0XFFFF 0XF0F00000L
请注意:八进制、十进制和十六进制形式只是整数的不同书写形式。无论采用哪种进制的写法,写出的都是 int 或 long 类型的整数。例如,255、0377 和 0xFF 都表示 int 整数,实际上它们表示同一个数(二百五十五)。C 语言为整数提供多种写法,只是为了编程方便,使人们可以根据需要选择适用的书写方式。在写某些复杂程序时,有时用八进制和十六进制形式更方便,后面会看到这方面的例子。
C 语言标准没有规定各种整数类型的表示范围,即没规定各整数类型的二进制编码长度。对 int 和 long,C 语言只规定了 long 类型的表示范围不小于 int 的表示范围,但允许它们的表示范围相同。具体的 C 语言系统都明确规定了 int 和 long 的表示方式和范围。在微型机上的一些早期 C 系统(如 Turbo C)里用 16 位二进制表示的整数类型(一个 int 占 2 个字节)和 32 位表示的长整数类型(一个 long 占 4 个字节),在这种情况下,int 类型的表示范围就是 \(-2^{15} \sim 2^{15}-1\),也就是 -32768~32767;long 类型的表示范围是 \(-2^{31} \sim 2^{31}-1\),也就是 -2147483648~2147483647。在新些的微机 C 语言系统(如 VC)里,int 和 long int 都用 32 位的二进制数表示。
实数类型
实数的计算机内部表示也由具体系统规定,目前多数 C 系统采用通行的标准(IEEE 754 国际标准,IEEE 即电子电器工程师协会,是一个著名的国际性技术组织):
float类型的数用 4 个字节 32 位二进制表示,表示的数大约有 7 位十进制有效数字,数值表示范围约为 \(\pm(3.4 × 10^{-38} \sim 3.4 × 10^{38})\);double类型的数用 8 个字节 64 位二进制表示,这种数大约有 16 位十进制有效数字,数值表示范围约为 \(\pm(1.7 × 10^{-308} \sim 1.7 × 10^{308})\);long double类型的数可能用 10 个字节 80 位二进制表示(IEEE 754 没有明确规定),大约有 19 位十进制有效数字,其数值的表示范围约为:\(\pm(1.2 × 10^{-4932} \sim 1.2 × 10^{4932})\)。在有些系统里,long double采用与double同样的表示形式。
显然,每个实数类型能表示的数只是数学中的实数的一个小子集合,不仅表示的范围有限,表示的精度(数的有效数字位数)也有限,请读者注意这些情况。
使用某具体计算机上的某个 C 语言系统编程时,要做的一件事就是弄清系统里各种整数类型的表示范围。有关情况可以从系统使用手册中查到,也可以查看介绍该系统的书籍或系统的联机帮助。此外,还可以查看这个 C 系统中名字为 limit.h 的文件。这是每个 C 系统都有的一个标准文件,其中列出了与整数有关的所有具体规定。
浮点数也有类似情况。例如,在一些 C 语言系统里 long double 与 double 的表示方式一样。有关具体 C 语言系统中浮点数表示的情况,也应该查阅系统手册,还可以查阅名为 float.h 的标准库文件。
字符类型
下面是几个最常用的特殊字符的写法:
| 特殊字符 | 写法 |
|---|---|
| 换行字符 | \n |
| 双引号字符 | \" |
| 单引号字符 | \' |
| 反斜线字符 | \\ |
形式上都是一个反斜线字符(\)后跟另一字符。这样表示的字符可用于字符文字量(如 \')或字符串文字量(如 "He said: \"Fine!\"" 表示字符序列 He said: "Fine!")。反斜线字符的作用就是表明其后的字符不取原意。这样连续两个字符(或更多几个字符)称为一个转义序列,可用于表示各种无法直接写出的字符。起特殊作用的反斜线字符称为转义字符。还有一些特殊字符也有特别规定的写法,附录 B 列出了所有特殊字符的写法,还说明了如何用八进制和十六进制编码的形式写各种字符。
需要强调下面两个问题:
数字字符和数不同。例如
1和'1',前者是一个int类型的文字量,在计算机里用int类型的编码方式表示,占据int类型数据所需的单元。在常见的微机 C 系统里可能占了 2 个字节或 4 个字节,其中保存 1 的二进制编码。而'1'是char类型的数据,通常占一个字节,其中保存字符 1 的编码(1 的 ASCII 编码是 49)。字符或字符串类型的数据也不是标识符。例如,
sin和"sin"是两种完全不同的东西。后者表示需要程序处理的一个字符串数据;前者是程序描述中使用的一个名字,标准库的正弦函数用的是这个名字。显然两者根本不在同一个层次上。
数据形式的转换和输出
程序运行中处理的数据来自外部(除了直接写在程序代码里的文字量),还要把计算结果送出来,例如送到显示器给人看。信息在外部世界的表现形式丰富多彩,而送到计算机里给程序处理时统一为二进制表示。这样,在程序输入或输出信息时,就必须做数据形式的转换。数据的外部表示形式指源程序中写数据时(或为运行中的程序提供数据时,或者程序产生的输出)所采用的形式。内部表示形式指程序操作的数据的二进制编码形式,也是计算机内部存储和处理数据时所用的形式。这是两种不同形式。例如:
| 数据类型 | 外部表示形式 | 内部表示形式 |
|---|---|---|
| 整数 | 十进制(或其他进制)的数字字符序列 | 整数的二进制编码 |
| 字符 | 字符本身 | 相应的二进制编码(常用的 ASCII 编码) |
举例说,如果我们在源程序里写一个整数 123,程序运行中实际使用的是存在内存单元里的二进制形式 0000000001111011(假设系统采用 16 位二进制编码表示整数)。如在程序里写了字符 'a',程序运行中会有字符 'a' 的二进制编码保存在内存里的某个地方。
由于同样数据的外部和内部表示形式不同,在源程序编译时,以及在程序运行中执行输入输出时,都要做这两种形式之间的转换。编译时的数据转换由编译器完成,C 语言规定各种数据的书写形式(各种字面量),编译器自动把符合规定形式的数据转换为内部形式,供程序运行中使用。输入输出中的数据转换都需要在程序里明确写出。
前面讨论了输出中的数据转换,介绍了如何在调用函数 printf 时描述输出中的转换动作。由于每个有用的程序都要输出,描述在输出过程中的数据转换方式也是编写每个程序都要做的事情。在做这件事时,需要特别关注被输出数据的类型和转换描述之间相互对应。关于程序输入和输入时的信息形式转换将在后面讨论。
标准输出
前面讨论中说,printf 产生的输出将送到显示器或特定输出窗口,其实这里还有些细节。现在说明这方面的情况。
C 语言有一个重要概念是标准输出(与之对应的标准输入概念将在后面讨论),printf 和另一些标准库函数都把生成的信息送到标准输出。读者在学习计算机基础知识时,可能已经了解到操作系统的标准输入和标准输出的概念,在 DOS、Windows 和 UNIX 等各种操作系统中都有这两个概念。标准输出是操作系统为程序的信息交换提供的一种标准通道。在许多操作系统环境下,送到标准输出的信息将默认地被自动送到计算机的显示屏幕,使我们可以在屏幕上看到。在 Windows 等图形用户界面系统里,应用程序送到标准输出的信息可能被显示在一个特殊窗口中。
C 语言的标准输出(和标准输入)概念直接对应于操作系统的相应概念,也就是说,C 程序里的 printf 把信息送到 C 语言规定的标准输出,进而送到操作系统的标准输出,操作系统将根据规则把它们送到应该送的地方。此外,操作系统(包括 DOS、Windows 和 UNIX)一般都支持标准输出的重新定向,允许通过重新定向把送到标准输出的信息转到其他地方,例如送到某个文件或者送到打印机等。重新定向的方式由具体系统规定。
运算对象的求值顺序
前面详细讨论了表达式求值中的各方面问题,包括优先级、结合顺序和括号。虽然有了这些规定,计算过程中仍然有些顺序问题没有完全确定。请看下面表达式:
(5+8)*(6+4)显然,这里子表达式 (5+8) 和 (6+4) 的计算应该先完成,而后再做乘法。但是,乘法的这两个运算对象 (5+8) 和 (6+4) 哪个先算呢?实际上,这也就是问乘法的两个运算对象(更一般性地说是二元运算符的两个运算对象)的计算顺序。对于算术运算符,C 语言对这个问题没有明确规定。这样,某个 C 系统可能先算左边对象,另一 C 系统可能先算右边对象,甚至可以有这样的 C 系统,它有时先算左边对象,有时先算右边对象。
读者可能会说,对上面这个例子,两个运算对象的求值顺序根本不会影响最后的结果。对于这个例子,确实如此。但计算机与数学不同,我们不久就会看到对运算对象的求值顺序敏感的表达式。运算对象的求值顺序是程序语言中的特殊问题,数学里不存在这种问题。这一情况体现出计算机与数学的不同。
C 语言不规定表达式中两个运算对象求值顺序的定义,是想给 C 语言系统的实现者提供方便,使做编译器的人可以根据自己的需要自由确定一种具体计算方式,这样可能提高系统实现的效率,或者简化系统的实现。应当这样理解 C 语言的规定:在写程序时,不应该写出依赖于运算对象的特殊计算顺序的表达式,因为无法保证它能算出所需的结果。
几个常用程序模式
程序模式 2.1 简单计算程序
可以采用如下模式(简单文本输出模式与此类似):
#include <stdio.h>
int main() {
printf("......", ...); /* 计算表达式写在这里 */
return 0;
}程序模式 2.2 使用数学函数的简单计算程序
#include <stdio.h>
#include <math.h>
int main() {
printf("......", ...); /* 计算表达式写在这里 */
return 0;
}本章讨论的重要概念
基本字符,名字,标识符,关键字,数据,类型,类型名,文字量,整数类型,长整数类型,实数类型,浮点类型,双精度类型,长双精度类型,字符类型,特殊字符,字符串,语句,函数,输出函数 printf,函数调用,实际参数(实参),格式描述串,转换描述,运算符,表达式,计算,一元运算符,二元运算符,运算对象,算术运算符(+、-、*、/、%),算术表达式,求值,优先级,结合方式,溢出,上溢,下溢,类型转换,类型强制,数学函数,函数名,函数调用时的类型转换,数值类型的表示范围,整数的十进制写法、八进制写法和十六进制写法,数据的外部表示,数据的内部表示,表示形式的转换,运算对象的求值顺序。
练习
指出下面的哪些字符序列不是合法的标识符:
I am bg--1 a$#24 x_x_2 Eoof_ xf_1_4 3x1 X+ _abc手工计算下列表达式的值:
125 + 01253 * (int)sqrt(34) - sin(6) * 5 + 0 * 2AF24 * 3 / 5 + 636 + -(5 - 23) / 43 * (2L + 4.5f) - 012 + 44cos(2.5f + 4) - 6 * 27L + 1526 - 2.4Larctan(log3(e + π))sqrt(π^2 + 1)
在下面表达式的计算过程中,在哪些地方将发生类型转换,各是从什么类型转换到什么类型,表达式计算的结果是什么?
写程序计算第 3 题中各表达式的值。
写程序计算下面各表达式的值:
- \(\dfrac{2.34}{1+257}\)
- \(\dfrac{1065}{24 * 13}\)
- \(\dfrac{23.582}{7.96 / 3.67}\)
- \(1+\dfrac{2}{3+4 / 5}\)
- \(log_5 \sqrt{2\pi-1}\)
- \(e^{\sqrt{\pi+1}}\)
写几个简单文本输出程序,它们执行时输出一些有名的英文句子或诗。
请试一试所用的 C 语言系统能不能在字符串里写中文。如果可以,请写几个输出中文文本的程序,例如输出李白的“望庐山瀑布”和另外几首你喜爱的唐诗或宋词。
已知铁的比重是 7.86,金的比重是 19.3。请写出几个简单程序,分别计算出直径 100 毫米和 150 毫米的铁球与金球的重量。
写程序计算 \(5x^2+2x+6\) 的两个根,考虑用合适的方式输出。(提示:对这个具体问题,读者可以先自己算出判别式 \(b^2-4ac\),以此作为已知信息就可写出程序。)
在计算机上试验本章正文中的一些程序。对它们做一些修改,观察程序加工和运行的情况,并对程序的行为做出解释。
请在一个能正确工作的输出整数结果的程序里把其中
printf的相应转换描述改成%f或者%ld,看看会出现什么情况。在一个能正确工作的输出双精度结果的程序里把printf的相应转换描述改为%d或者%ld,看看会出现什么情况。