02 数据与简单计算程序

写程序是为了实现某种计算,程序在计算中需要处理各种数据,所以写程序也就是描述数据的处理,必然要涉及数据的描述和从数据出发的计算过程的描述。

人们在学习初等数学时就描述过许多计算,例如可能写出下面的数学表达式:

\[ -\frac{3.24 × 5+sin 2.3}{4} × 6.24 \]

这一表达式描述的是从一些数值出发,通过三角函数和算术运算求一个值。C 程序里也需要(而且可以)写出与此类似的计算描述片段,与上面数学式对应的 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 语言特别规定下划线字符”_“也当做字母看待,这就是说,它可以出现在标识符中任何地方,特别是可以作为标识符的第一个字符。下面是一些标识符的例子:

abcd Beijing C_Programming xt386ex A_great_machine Small_talk_80 FORTRAN_90 _f2048sin a3b06

人们通常把以下划线开始的标识符保留给系统使用。在编写普通程序时,不应该引进这种形式的标识符,以免与系统内部使用的名字冲突,造成程序错误。

如果在一个字符序列中出现了非字母、非数字、非下划线的字符,那么它就不是一个标识符了(当然,完全可能其中一部分是标识符,例如x3+5+y,其中x3y都是标识符,中间的+5+不属于这两个标识符)。另外,非字母开头的字符序列也绝不是标识符。下面是一些非标识符的字符序列:

3set a[32] $$$ sin(2+5)::ab4== 23x5 +=

在考虑标识符是否相同的问题时,C语言区分大小写字母,把同一字母的大写形式和小写形式看做不同字符,这样,aA就是不同字符(也是两个不同的单字符标识符),nameNameNAMEnaMenAME是互不相同的5个标识符。

C语言的合法标识符里有一个特殊的小集合,其中的标识符称为关键字,C语言为这些关键字定义了某种特殊意义,不允许将它们用于其他目的,不能作为普通的名字。我们现在不准备对关键字做更多解释。随着讨论的进展,读者会一个个地接触并记住各关键字,目前只需要记住关键字这一概念。所有关键字都列在附录B中。

除了不能用关键字之外,我们在写程序时,可以用任何形式上符合C语言要求的标识符为自己定义的东西命名,名字可以自由选择。经过长期的程序设计实践,人们认为命名问题不是一件无关紧要的事情,为程序对象选择合理的名字能为人们写程序、读程序提供有益的提示,因此人们倡导采用能反映程序对象的含义的名字(标识符)。

请注意,命名问题不是C语言特殊的东西,每种程序语言都要规定程序中名字的形式,在计算机领域中到处都用到名字,如计算机里的文件和目录,各种应用程序和系统,图形界面的图标和按钮,计算机网络中的每台计算机,都需要命名。在实际命名时,应采用有意义的名字,这一原则在计算机领域中具有广泛适用性。

2.2 数据、类型和简单程序

C语言把程序处理的基本数据对象分成一些集合,属于同一集合的数据具有相同的性质:书写形式统一,实现时采用同样的编码方式(按同样规则对应到二进制编码,采用同样位数),对它们能做同样的操作等。具有这样性质的一个数据集合称为一个类型。

计算机硬件处理的数据也分成不同类型,通常包括字符、整数、浮点数等,CPU为不同类型提供不同指令:对整数有一套算术指令,对浮点数有另一套指令。高级语言中的数据分成不同类型与此有关。但类型的意义不仅在于此,类型是计算机科学的核心概念之一。在学习程序设计和程序语言的过程中,我们将不断与类型打交道,请读者特别注意这个概念。

C语言的基本类型包括字符类型、整数类型、实数类型等、程序里写的、执行中处理的每个基本数据都属于某个基本类型。对于具体的C语言系统,各种基本类型都有确定的表示(编码)方式和表示范围。例如,一个整数类型里只包含有限个整数值,是数学里的整数的一个子集。存在这个类型能表示的最小整数和最大整数,超出该范围的整数在这个类型里根本没有容身之地。

我们很快就会看到这些基本情况的影响。

C语言规定了基本类型的名字(类型名)。在这里,基本类型的名字由一个或几个标识符(都是关键字)构成,其形式与前面讲的”名字”有所不同。本节将介绍几个最常用的类型,对前几章的工作而言,这些类型已经足够了。后面章节会介绍C语言里所有的基本类型。这样安排的目的是希望讨论能尽快进入主题——程序与程序设计。

还应提出文字量(字面量)的概念,文字量就是直接写在程序里的数据。C语言规定了各种基本类型的文字量的书写形式。例如,程序里写的一个整数类型的数据就是一个”整型文字量”。为简单起见,人们也常把整型文字量简称为”整数”,其他情况也采用类似说法。后面讨论里常用这种简单说法,只在特别需要时才用更严格的说法。

2.2.1 几个常用类型

本节介绍几个最常用的类型。

整数类型和整数

为适应不同需要,C语言里提供了多个整数类型,使用最多的是一般整数类型(简称为整型)和长整数类型(简称长整型)。前者的类型名是int;后者的类型名为long int,可以简写为longintlong都是关键字。

不同整数类型的差异在于它们可能采用不同的二进制编码位数,因此可能具有不同表示范围。任何整数类型都是数学整数的一个连续取值的子集,intlong类型的取值范围都是以0为中心的某个数值区间里的所有整数,具体情况在2.5节解释。

整型(int类型)的文字量(整数)有几种书写形式,最常用的是十进制写法,这样的整数就是普通数字字符的一个连续序列,其中不能有空白或其他字符。C语言规定十进制整数的第一个字符不能是0,除非要写的数本身是0。下面是一些整数的例子:

123 30425278 0 906

长整数是另一个不同的类型。C语言为长整数规定了一种专门写法,其特殊之处是在表示数值的数字序列最后附一个字母lL作为后缀。由于小写字母l容易与数字1混淆,建议读者用大写的L。下面是一些长整数的例子:

123L 304125278L 110L 906L

C语言允许在整数的前面写正负号,加上负号的整数表示负整数。还可以采用其他方式写整型的字面量(整数),有关情况参看2.5节。

实数类型和实数

C语言有三个表示实数的类型:单精度浮点数类型,简称浮点类型,类型名为float;双精度浮点数类型,简称双精度类型,类型名为double;长双精度类型,类型名为long double。这些类型的文字量也分别被称做”浮点数”、“双精度数”和”长双精度数”。所有整数类型和实数类型统称为算术类型。

每个实数类型能表示的数也只是数学中的实数的一个小子集合,不仅表示的范围有限,表示的精度(有效数字位数)也有限。具体情况参看2.5节的介绍。

人们写C程序时使用最多的实数类型是双精度类型double。双精度数的基本部分是一个数字序列,但需要或者包含表示小数点的圆点”.”(可以是第一个或最后一个字符),或者在数字序列后面有一个指数部分——以eE开头的另一数字序列(可以有正负号),指数以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。表示长双精度数的后缀是lL。下面是一些浮点数和长双精度数的例子:

13.2F 1.7853E-2F 24.68700f .32F 0.337f
12.869L 3.417E34L .05L 5.E88L 1.L

负数同样用在实数前加负号的方式表示。

字符类型、字符和字符串

字符类型的数据主要用于程序的输入和输出。此外,文字处理是计算机的一个重要应用领域,该领域的应用程序必须使用和处理字符形式的数据。由于大部分程序都需要与人打交道,需要接收人的输入信息(例如给程序发的命令、提供的数据),还需要产生输出信息给人看,因此字符类型的数据在程序里用得很多。

C语言里最常用的字符类型的类型名是char。字符类型的数据值包括计算机所用编码字符集中的所有字符。目前微机和工作站使用最多的是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/23.0/2.0得到的结果不同,前者得到整数1而后者得到双精度的1.5。这种情况有时会带来很迷惑人的结果。例如,两个表达式

1/3*31*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值参与随后的加法运算。从实数类型到整型的转换方式是直接丢掉小数部分。

还有一些与类型转换有关的问题:

  1. 在类型转换中可能丢失信息。上面示例中的显式类型转换显然会丢失信息:从双精度值转换为整数值时,原数的小数部分丢掉了。算术运算的自动转换有时也可能丢失信息,一个典型例子是把long类型的数据转换到float类型。通常float类型的精度位数比long类型少,因此可能丢失long类型值的低位信息。

  2. 如果被转换的值无法在转换结果类型里表示,转换结果没有定义。这种情况在算术运算引起的自动转换里不会出现,写强制类型转换时必须注意。

  3. C语言规定数值类型之间都可以互相转换。允许的其他转换在后面章节讨论。

  4. C语言把显式类型转换看做一元运算符,具有与其他一元运算符同样的优先级和结合方式。如果在上面的表达式里不写括号,其意义就会不同。请读者自己分析。

  5. 类型转换是值的转换,从一个值出发产生出另一个不同类型的值。类型转换并不改变原来的值,而是产生一个新值。这一点也非常重要。

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
  • 其他常用函数:
函数 功能 类型特征
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的结果做intdouble的数值转换。

例2.5 简单级数计算。计算\(\sum_{n=1}^{10} sin \frac{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=\frac{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语言的基本字符包括:

  1. 数字字符:0,1,2,3,4,5,6,7,8,9。

  2. 大小写拉丁字母:az,AZ。

  3. 其他一些可打印(可以显示)的字符(如标点符号、运算符号、括号等),包括:

    ~ ! @ # % ^ & * ( ) _ - + = { } [ ] | \ : ; " ' < > , . ? /

    不必死记这些字符,随着学习进展,读者将很容易记住它们的意义和作用。前面程序用过的字符包括标识符里用的”_“、算术运算符(共5个)、小数点、圆括号和花括号、逗号和分号、表示字符和字符串的单引号和双引号、作用特殊的”%“。其中”%“在表达式里表示取模运算,在printf的格式描述串里表示转换描述。

  4. 空格符、换行符、制表符。

  5. 只能用在字符文字量或字符串文字量里的其他字符。

基本数据类型的一些问题

整数类型

整数与长整数(intlong文字量)还可以用八进制或十六进制形式书写。

八进制形式的整数是数字0开始的连续数字序列,其中只允许0~7八个数字,其长整数也是在序列最后加lL。下面是用八进制形式写出的一些整数和长整数:

0236 0527 06254 0531 0765432L

十六进制形式的整数和长整数是由0x0X开头的数字序列(长整数也要带后缀字母lL)。由于数字只有10个,而十六进制形式中需要16个数字,C语言采用计算机领域通行的方式,用字母af或AF表示其余的6个十六进制数字,其对应关系是:

字母 表示的数字
a或A 10
b或B 11
c或C 12
d或D 13
e或E 14
f或F 15

下面是用十六进制形式写出的一些整数和长整数:

0x2073 0xA3B5 0XABCD 0XFFFF 0XF0F00000L

请注意:八进制、十进制和十六进制形式只是整数的不同书写形式。无论采用哪种进制的写法,写出的都是intlong类型的整数。例如,255、0377和0xFF都表示int整数,实际上它们表示同一个数(二百五十五)。C语言为整数提供多种写法,只是为了编程方便,使人们可以根据需要选择适用的书写方式。在写某些复杂程序时,有时用八进制和十六进制形式更方便,后面会看到这方面的例子。

C语言标准没有规定各种整数类型的表示范围,即没规定各整数类型的二进制编码长度。对intlong,C语言只规定了long类型的表示范围不小于int的表示范围,但允许它们的表示范围相同。具体的C语言系统都明确规定了intlong的表示方式和范围。在微型机上的一些早期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)里,intlong int都用32位的二进制数表示。

实数类型

实数的计算机内部表示也由具体系统规定,目前多数C系统采用通行的标准(IEEE 754国际标准,IEEE即电子电器工程师协会,是一个著名的国际性技术组织):

  1. float类型的数用4个字节32位二进制表示,表示的数大约有7位十进制有效数字,数值表示范围约为\(\pm(3.4 × 10^{-38} \sim 3.4 × 10^{38})\)
  2. double类型的数用8个字节64位二进制表示,这种数大约有16位十进制有效数字,数值表示范围约为\(\pm(1.7 × 10^{-308} \sim 1.7 × 10^{308})\)
  3. 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 doubledouble的表示方式一样。有关具体C语言系统中浮点数表示的情况,也应该查阅系统手册,还可以查阅名为float.h的标准库文件。

字符类型

下面是几个最常用的特殊字符的写法:

特殊字符 写法
换行字符 \n
双引号字符 \"
单引号字符 \'
反斜线字符 \\

形式上都是一个反斜线字符(\)后跟另一字符。这样表示的字符可用于字符文字量(如\')或字符串文字量(如"He said: \"Fine!\""表示字符序列He said: "Fine!")。反斜线字符的作用就是表明其后的字符不取原意。这样连续两个字符(或更多几个字符)称为一个换意序列,可用于表示各种无法直接写出的字符。起特殊作用的反斜线字符称为换意字符。还有一些特殊字符也有特别规定的写法,附录B列出了所有特殊字符的写法,还说明了如何用八进制和十六进制编码的形式写各种字符。

需要强调下面两个问题:

  1. 数字字符和数不同。例如1'1',前者是一个int类型的文字量,在计算机里用int类型的编码方式表示,占据int类型数据所需的单元。在常见的微机C系统里可能占了2个字节或4个字节,其中保存1的二进制编码。而'1'char类型的数据,通常占一个字节,其中保存字符1的编码(1的ASCII编码是49)。

  2. 字符或字符串类型的数据也不是标识符。例如,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,函数调用,实际参数(实参),格式描述串,转换描述,运算符,表达式,计算,一元运算符,二元运算符,运算对象,算术运算符(+、-、*、/、%),算术表达式,求值,优先级,结合方式,溢出,上溢,下溢,类型转换,类型强制,数学函数,函数名,函数调用时的类型转换,数值类型的表示范围,整数的十进制写法、八进制写法和十六进制写法,数据的外部表示,数据的内部表示,表示形式的转换,运算对象的求值顺序。

练习

  1. 指出下面的哪些字符序列不是合法的标识符:

    I am bg--1 a$#24 x_x_2 Eoof_ xf_1_4 3x1 X+ _abc
  2. 手工计算下列表达式的值:

    1. 125 + 0125
    2. 3 * (int)sqrt(34) - sin(6) * 5 + 0 * 2AF
    3. 24 * 3 / 5 + 6
    4. 36 + -(5 - 23) / 4
    5. 3 * (2L + 4.5f) - 012 + 44
    6. cos(2.5f + 4) - 6 * 27L + 1526 - 2.4L
    7. arctan(log3(e + π))
    8. sqrt(π^2 + 1)
  3. 在下面表达式的计算过程中,在哪些地方将发生类型转换,各是从什么类型转换到什么类型,表达式计算的结果是什么?

  4. 写程序计算第3题中各表达式的值。

  5. 写程序计算下面各表达式的值:

    1. \(\frac{2.34}{1+257}\)
    2. \(\frac{1065}{24 * 13}\)
    3. \(\frac{23.582}{7.96 / 3.67}\)
    4. \(1+\frac{2}{3+4 / 5}\)
    5. \(log_5 \sqrt{2\pi-1}\)
    6. \(e^{\sqrt{\pi+1}}\)
  6. 写几个简单文本输出程序,它们执行时输出一些有名的英文句子或诗。

  7. 请试一试所用的C语言系统能不能在字符串里写中文。如果可以,请写几个输出中文文本的程序,例如输出李白的”望庐山瀑布”和另外几首你喜爱的唐诗或宋词。

  8. 已知铁的比重是7.86,金的比重是19.3。请写出几个简单程序,分别计算出直径100毫米和150毫米的铁球与金球的重量。

  9. 写程序计算\(5x^2+2x+6\)的两个根,考虑用合适的方式输出。(提示:对这个具体问题,读者可以先自己算出判别式\(b^2-4ac\),以此作为已知信息就可写出程序。)

  10. 在计算机上试验本章正文中的一些程序。对它们做一些修改,观察程序加工和运行的情况,并对程序的行为做出解释。

  11. 请在一个能正确工作的输出整数结果的程序里把其中printf的相应转换描述改成%f或者%ld,看看会出现什么情况。在一个能正确工作的输出双精度结果的程序里把printf的相应转换描述改为%d或者%ld,看看会出现什么情况。