学习一门新程序设计语言的惟一途径就是使用它编写程序。对于所有语言的初学者来说编写的第一个程序几乎都是相同的,即:
请打印出下列内容
hello, world
尽管这个练习很简单,但对于初学语言的人来说,它仍然可能成为一大障碍,因为要实现这个目的,我们首先必须编写程序文本,然后成功地运行编译,并加载、运行,最后输出到某个地方。掌握了这些操作细节以后,其它事情就比较容易了。
在C语言中,我们可以用下列程序打印出"hello,world”
#include
int main() {
printf("hello, world\n");
}
如何运行这个程序取决于所使用的系统。这里举一个特殊的例子。在 UNIX 操作系统中, 首先必须在某个文件中建立这个源程序,并以”.c”作为文件的扩展名,例如 hello.c,然后再通过下列命令进行编译:
cc hello.c
如果源程序没有什么错误(例如漏掉字符或拼错字符),编译过程将顺利进行,并生成一个可执行文件 a.out。然后,我们输入:
a.out
即可运行 a.out,打印出下列信息: hello,world
在其它操作系统中,编译、加载、运行等规则会有所不同。
下面对程序本身做些说明。一个C语言程序,无论其大小如何,都是由函数和变量组成的。函数中包含一些语句,以指定所要执行的计算操作;变量则用于存储计算过程中使用的值。
在本例中,函数的名字为main。通常情况下,函数的命名没有限制,但 main 是一个特殊的函数名----每个程序都从 main 函数的起点开始执行,这意味着每个程序都必须在某个位置包含一个 main 函数。 main 函数通常会调用其它函数来帮助完成某些工作,被调用的函数可以是程序设计人员自己编写的,也可以来自于函数库。上述程序段中的第一行语句
#include
用于告诉编译器在本程序中包含标准输入/输出库的信息。许多C语言源程序的开始处都包含这一行语句。我们将在后面对标准库进行详细介绍。
函数之间进行数据交换的一种方法是调用函数向被调用函数提供一个值(称为参数)列表。函数名后面的一对圆括号将参数列表括起来。在本例中,main 函数不需要任何参数,因此用空参数表()表示。
函数中的语句用一对花括号{}括起来。本例中的 main 函数仅包含下面一条语句:
printf("hello,world\n");
调用函数时,只需要使用函数名加上用圆括号括起来的参数表即可。上面这条语句将"hello,world\n"。作为参数调用 printf 函数。printf 是一个用于打印输出的库函数,在此处,它打印双引号中间的字符串。
用双引号括起来的字符序列称为字符串或字符串常量,如"hello,world\n"就是一个字符串。目前我们仅使用字符串作为 printf 及其它函数的参数。 在C语言中,字符序列\n 表示换行符,在打印中遇到它时,输出打印将换行,从下一行的左端行首开始。如果去掉字符串中的\n(这是个值得一做的练习),即使输出打印完成后也不会换行。在 printf 函数的参数中,只能用\n 表示换行符。如果用程序的换行代替\n,例如:
printf("hello, world
");
C编译器将会产生一条错误信息。
printf函数永远不会自动换行,这样我们可以多次调用该函数以分阶段得到一个长的输出行。上面给出的第一个程序也可以改写成下列形式:
#include
int main() {
printf("hello, ");
printf("world");
printf("\n");
}
这段程序与前面的程序的输出相同。 请注意,\n 只代表一个字符。类似于\n 的转义字符序列为表示无法输入的字符或不可见字符提供了一种通用的可扩充的机制。除此之外,C语言提供的转义字符序列还包括:\t 表示制表符;\b 表示回退符;\"表示双引号;\\表示反斜杠符本身。
我们来看下一个程序,使用公式℃=(5/9)(℉-32)打印下列华氏温度与摄氏温度对照表:
℉ | ℃ |
---|---|
1 | -17 |
20 | -6 |
40 | 4 |
60 | 15 |
80 | 26 |
100 | 37 |
120 | 48 |
140 | 60 |
160 | 71 |
180 | 82 |
200 | 93 |
220 | 104 |
240 | 115 |
260 | 126 |
280 | 137 |
300 | 148 |
此程序中仍然只包括一个名为 main 的函数定义。它比前面打印"hello,world”的程序长一些,但并不复杂。这个程序中引入了一些新的概念,包括注释、声明、变量、算术表达式、循环以及格式化输出。该程序如下所示:
#include
/* 当fahr=0,20,,300时,分别打印华氏温度与摄氏温度对照表 */
int main() {
int fahr, celsius;
int lower, upper, step;
lower = 0; /* 温度表的下限卖 */
upper = 300; /* 温度表的上限 */
step = 20; /* 步长 */
fahr = lower;
while (fahr <= upper) {
celsius = 5 * (fahr - 32) / 9;
printf("%d\t%d\n", fahr, celsius);
fahr = fahr + step;
}
}
其中: /*当fahr=0,20,300时,分别打印华氏温度与摄氏温度对照表 */
称为注释,此处,它简单地解释,该程序是做什么用的。包含在/*与*/之间的字符序列将被编译器忽略。注释可以自由地运用在程序中,使得程序更易于理解。程序中允许出现空格、制表符或换行符之处,都可以使用注释。
在C语言中,所有变量都必须先声明后使用。声明通常放在函数起始处,在任何可执行语句之前。声明用于说明变量的属性,它由一个类型名和一个变量表组成,例如:
int fahr, celsius;int lower, upper, step;
其中,类型 int 表示其后所列变量为整数,与之相对应的 float 表示所列变量为浮点数(即,可以带有小数部分的数)。int 与 float 类型的取值范围取决于具体的机器。对于 int 类型,通常为 16 位,其取值范围在-32768~32767 之间,也有用 32 位表示的 int 类型。float 类型通常是 32 位,它至少有6位有效数字,取值范围一般在 10^-38~10^38之间。
除 int 与 float 类型之外,c语高还提供了其它一些基本数据类型,例如:
char 字符--一个字节
short 短整型
long 长整型
double 双精度浮点型
这些数据类型对象的大小也取决于具体的机器。另外,还存在这些基本数据类型的数组、结构、联合,指向这些类型的指针以及返回这些类型值的函数。我们将在后续相应的章节中分别介绍。 在上面的温度转换程序中,最开始执行的计算是下列4个赋值语句:
lower = 0;
upper = 300;
step = 20;
fahr = lower;
它们为变量设置初值。各条语句均以分号结束。
温度转换表中的各行计算方式相同,因此可以用循环语句重复输出各行。这是 while 循环语句的用途:
while (fahr <= upper) {
...
}
while 循环语句的执行方式是这样的:首先测试圆括号中的条件;如果条件为真(fahr<=upper),则执行循环体(括在花括号中的3条语句);然后再重新测试圆括号中的条件,如果为真,则再次执行循环体;当圆括号中的条件测试结果为假(fahr>upper)时,循环结束,并继续执行跟在 while 循环语句之后的下一条语句。在本程序中,循环语句后没有其它语句,因此整个程序的执行终止。
while 语句的循环体可以是用花括号括起来的一条或多条语句(如上面的温度转换程序),也可以是不用花括号包括的单条语句,例如:
while (i < j)
i = 2 * i;
在这两种情况下,我们总是把由 while 控制的语句缩进一个制表位,这样就可以很容易地看出循环语句中包含哪些语句。这种缩进方式突出了程序的逻辑结构。尽管 C 编译器并不关心程序的外观形式,但正确的缩进以及保留适当空格的程序设计风格对程序的易读性非常重要。我们建议每行只书写一条语句,并在运算符两边各加上一个空格字符,这样可以使得运算的结合关系更清楚明了。相比而言,花括号的位置就不那么重要了。我们从比较流行的一些风格中选择了一种,读者可以选择适合自己的一种风格,并养成一直使用这种风格的好习惯。
在该程序中,绝大部分工作都是在循环体中完成的。循环体中的赋值语句
celsius = 5 * (fahr - 32) / 9;
用于计算与指定华氏温度相对应的摄氏温度值,并将结果赋值给变量 celsius。在该语句中,之所以把表达式写成先乘5然后再除以9而不是直接写成5/9,其原因是在C语言及许多其它语言中,整数除法操作将执行舍位,结果中的任何小数部分都会被舍弃。由于5和9都是整数,5/9相除后经截取所得的结果为0,因此这样求得的所有摄氏温度都将为 0。
从该例子中也可以看出 printf 函数的一些功能。printf 是一个通用输出格式化函数,后面将对此做详细介绍。该函数的第一个参数是待打印的字符串,其中的每个百分号()表示其它的参数(第二个、第三个、……..参数)之一进行替换的位置,并指定打印格式。例如,%d 指定一个整型参数,因此语句
printf("%d\t%d\n", fahr, celsius);
用于打印两个整数 fahr 与 celsius 的值,并在两者之间留一个制表符的空间(\t)。printf 函数的第一个参数中的各个名分别对应于第二个、第三个、…参数,它们在数目和类型上都必须匹配,否则将出现错误的结果。
顺便指出,printf函数并不是C语言本身的一部分,C语言本身并没有定义输入/输出功能。printf 仅仅是标准库函数中一个有用的函数而已,这些标准序函数在 ℃语言程序中通常都可以使用。但是,ANSI标准定义了printf函数的行为,因此,对每个符合该标准的编译器和库来说,该函数的属性都是相同的。
上述的温度转换程序存在两个问题。比较简单的问题是,由于输出的数不是右对齐的,所以输出的结果不是很美观。这个问题比较容易解决:如果在 printf 语句的第一个参数的 %d 中指明打印宽度,则打印的数字会在打印区域内右对齐。例如,可以用语句
printf(" %3d %6d\n", fahr, celsius);
打印 fahr 与 celsius 的值,这样,fahr 的值占3个数字宽,celsius 的值占6个数字宽,输出的结果如下所示:
0 -17
20 -6
40 4
60 15
80 26
100 37
...
另一个较为严重的问题是,由于我们使用的是整型算术运算,因此经计算得到的摄氏温度值不太精确,例如,与 0下对应的精确的摄氏温度应该为-17.8℃,而不是-17℃。为了得到更精确的结果,应该用浮点算术运算代替上面的整型算术运算。这就需要对程序做适当修改。下面是该程序的又一种版本
#include
/* print Fahrenheit-Celsius table
for fahr = 0, 20, ..., 300; floating-point version */
int main() {
float fahr, celsius;
float lower, upper, step;
lower = 0; /* lower limit of temperatuire scale */
upper = 300; /* upper limit */
step = 20; /* step size */
fahr = lower;
while (fahr <= upper) {
celsius = (5.0 / 9.0) * (fahr - 32.0);
printf("%3.0f %6.1f\n", fahr, celsius);
fahr = fahr + step;
}
}
这个程序与前一个程序基本相同,不同的是,它把fahr 与 celsius 声明为 float 类型,转换公式的表述方式也更自然一些。在前一个程序中,之所以不能使用5/9的形式, 是因为按整型除法的计算规则,它们相除并舍位后得到的结果为0。但是,常数中的小数点表明该常数是一个浮点数,因此,5.0/9.0是两个浮点数相除,结果将不被舍位。
如果某个算术运算符的所有操作数均为整型,则执行整型运算。但是,如果某个算术运算符有一个浮点型操作数和一个整型操作数,则在开始运算之前整型操作数将会被转换为浮点型。例如,在表达式 fahr - 32 中,32 在运算过程中将被自动转换为浮点数再参与运算。不过,即使浮点常量取的是整型值,在书写时最好还是为它加上一个显式的小数点,这样可以强调其浮点性质,便于阅读。
在这里需要注意,赋值语句
fahr = lower;
与条件测试语句
while (fahr <= upper)
也都是按照这种方式执行的,即在运算之前先把 int 类型的操作数转换为 float 类型的操作数。
printf 中的转换说明名3.0f 表明待打印的浮点数(即 fahr)至少占3个字符宽,且不带小数点和小数部分;86.1f表明另一个待打印的数(celsius)至少占6个字符宽,且小数点后面有1位数字。其输出如下所示:
0 -17.8
20 -6.7
40 4.4
...
格式说明可以省略宽度与精度,例如,%6f 表示待打印的浮点数至少有6个字符宽;%.2f指定待打印的浮点数的小数点后有两位小数,但宽度没有限制:%f则仅仅要求按照浮点数打印该数。
此外,printf 函数还支持下列格式说明:%o表示八进制数;%x 表示十六进制数;%c表示字符;%s表示字符串;%%表示百分号(%)本身。
练习:
1.修改温度转换程序,使之能在转换表的顶部打印一个标题。
2.编写一个程序打印摄氏温度转换为相应华氏温度的转换表。
参考代码:
#include
int main(void)
{
float fahr, celsius;
int lower, upper, step;
lower = 0;
upper = 300;
step = 20;
printf("F C\n\n");
fahr = lower;
while(fahr <= upper)
{
celsius = (5.0 / 9.0) * (fahr - 32.0);
printf("%3.0f %6.1f\n", fahr, celsius);
fahr = fahr + step;
}
return 0;
}
题2
#include
int main(void)
{
float fahr, celsius;
int lower, upper, step;
lower = 0;
upper = 300;
step = 20;
printf("C F\n\n");
celsius = lower;
while(celsius <= upper)
{
fahr = (9.0/5.0) * celsius + 32.0;
printf("%3.0f %6.1f\n", celsius, fahr);
celsius = celsius + step;
}
return 0;
}
对于某个特定任务我们可以采用多种方法来编写程序。下面这段代码也可以实现前面的温度转换程序的功能:
#include
/*打印华氏温度-摄氏温度对照表*/
int main() {
int fahr;
for (fahr = 0; fahr <= 300; fahr = fahr + 20)
printf("%3d %6.1f\n", fahr, (5.0 / 9.0) * (fahr - 32));
}
这个程序与上节中介绍的程序执行结果相同,但程序本身却有所不同。最主要的改进在于它去掉了大部分变量,而只使用了一个 int 类型的变量 fahr。在新引入的 for 语句中,温度的下限、上限和步长都是常量,而计算摄氏温度的表达式现在变成了 printf 函数的第三个参数,它不再是一个单独的赋值语句。
以上几点改进中的最后一点是C语言中一个通用规则的实例:在允许使用某种类型变量值的任何场合,都可以使用该类型的更复杂的表达式。因为printf 函数的第三个参数必须是与名%6.1f匹配的浮点值,所以可以在此处使用任何浮点表达式。
for 语句是一种循环语句,它是对 while 语句的推广。如果将 for 语句与前面介绍的while 语句比较,就会发现 for 语句的操作更直观一些。圆括号中共包含3个部分,各部分之间用分号隔开。第一部分
fahr = 0
是初始化部分,仅在进入循环前执行一次。第二部分
fahr <= 300
是控制循环的测试或条件部分。循环控制将对该条件求值,如果结果值为真(true),则执行循环体(本例中的循环体仅包含一个 printf函数调用语句)。此后将执行第三部分
fahr = fahr + 20
以将循环变量 fahr 增加一个步长,并再次对条件求值。如果计算得到的条件值为假(false),循环将终止执行。与 while 语句一样,for 循环语句的循环体可以只有一条语句,也可以是用花括号括起来的一组语句。初始化部分(第一部分)、条件部分(第二部分)与增加步长部分(第三部分)都可以是任何表达式。
在实际编程过程中,可以选择 while与 for 中的任意一种循环语句,主要要看使用哪一种更清晰。for语句比较适合初始化和增加步长都是单条语句并且逻辑相关的情形,因为它将循环控制语句集中放在一起,且比 while 语句更紧凑。
练习:
1.修改温度转换程序,要求以逆序(即按照从300度到0度的顺序)打印温度转换表。
参考代码:
#include
int main(void)
{
float fahr, celsius;
int lower, upper, step;
lower = 0;
upper = 300;
step = 20;
printf("C F\n\n");
celsius = upper;
while(celsius >= lower)
{
fahr = (9.0/5.0) * celsius + 32.0;
printf("%3.0f %6.1f\n", celsius, fahr);
celsius = celsius - step;
}
return 0;
}
#include
int main(void)
{
float fahr, celsius;
int lower, upper, step;
lower = 0;
upper = 300;
step = 20;
printf("C F\n\n");
for(celsius = upper; celsius >= lower; celsius = celsius - step)
{
fahr = (9.0/5.0) * celsius + 32.0;
printf("%3.0f %6.1f\n", celsius, fahr);
}
return 0;
}
#include
/* print Fahrenheit-Celsius table */
int
main()
{
int fahr;
for (fahr = 300; fahr >= 0; fahr = fahr - 20)
printf("%3d %6.1f\n", fahr, (5.0/9.0)*(fahr-32));
return 0;
}
在结束讨论温度转换程序前,我们再来看一下符号常量。在程序中使用 300、20 等类似的“幻数”并不是一个好习惯,它们几乎无法向以后阅读该程序的人提供什么信息,而且使程序的修改变得更加困难。处理这种幻数的一种方法是赋予它们有意义的名字。#define 指令可以把符号名(或称为符号常量)定义为一个特定的字符串:
#define 名字 替代文本
在该定义之后,程序中出现的所有在#define 中定义的名字(既没有用引号引起来,也不是其它名字的一部分)都将用相应的替换文本替换。其中,名字与普通变量名的形式相同:它们都是以字母打头的字母和数字序列:替换文本可以是任何字符序列,而不仅限于数字。
#include
#define LOWER 0 /* lower limit of table */
#define UPPER 300 /* upper limit */
#define STEP 20 /* step size */
/* print Fahrenheit-Celsius table */
int main() {
int fahr;
for (fahr = LOWER; fahr <= UPPER; fahr = fahr + STEP)
printf("%3d %6.1f\n", fahr, (5.0 / 9.0) * (fahr - 32));
}
其中,LOWER、UPPER 与 STEP 都是符号常量,而非变量,因此不需要出现在声明中。符号常量名通常用大写字母拼写,这样可以很容易与用小写字母拼写的变量名相区别。注意#define 指令行的末尾没有分号。
接下来我们看一组与字符型数据处理有关的程序。标准库提供的输入/输出模型非常简单。无论文本从何处输入,输出到何处,其输入/输出都是按照字符流的方式处理。文本流是由多行字符构成的字符序列,而每行字符则由 0个或多个字符组成,行末是一个换行符。标准库负责使每个输入/输出流都能够遵守这一模型。使用标准库的C语言程序员不必关心在程序之外这些行是如何表示的。
标准库提供了一次读/写一个字符的函数,其中最简单的是 getchar 和 putchar 两个函数。每次调用时,getchar 函数从文本流中读入下一个输入字符,并将其作为结果值返回。也就是说,在执行语句
c = getchar()
之后,变量 c中将包含输入流中的下一个字符。这种字符通常是通过键盘输入的。关于从文件输入字符的方法,我们将在后面讨论。
每次调用 putchar 函数时将打印一个字符。例如,语句
putchar()
将把整型变量c的内容以字符的形式打印出来,通常是显示在屏幕上。
字符在键盘、屏幕或其它的任何地方无论以什么形式表现,它在机器内部都是以位模式存储的。char 类型专门用于存储这种字符型数据,当然任何整型(int)也可以用于存储字符型数据。因为某些潜在的重要原因,我们在此使用int 类型。
#include
int main() {
int a = getchar();
int b = getchar();
int c = getchar();
putchar(a);
printf("%c", b);
putchar(c);
}
putchar与printf这两个函数可以交替调用,输出的次序与调用的次序一致。
借助于 getchar 与 putchar 函数,可以在不了解其它输入/输出知识的情况下编写出数量惊人的有用的代码。最简单的例子就是把输入一次一个字符地复制到输出,其基本思想 如下:
将上述基本思想转换为C语言程序为:
#include
/* copy input to output; 1st version */
int main() {
int c;
c = getchar();
while (c != EOF) {
putchar(c);
c = getchar();
}
}
其中,关系运算符!=表示“不等于”。
这里需要解决如何区分文件中有效数据与输入结束符的问题。C语言采取的解决方法是:在没有输入时,getchar 函数将返回一个特殊值,这个特殊值与任何实际字符都不同。这个值称为 EOF(end of file,文件结束)。我们在声明变量c的时候,必须让它大到足以存放 getchar 函数返回的任何值。这里之所以不把c声明成 char 类型,是因为它必须足够大,除了能存储任何可能的字符外还要能存储文件结束符 EOF。因此,我们将c声明成 int 类型。
EOF 它与任何 char 类型的值都不相同。定义在头文件中,是个整型数(-1),这里使用符号常量,可以确保程序不需要依赖于其对应的任何特定的数值。
对于经验比较丰富的 C语言程序员,可以把这个字符复制程序编写得更精炼一些。在C语言中,类似于
c = getchar()
之类的赋值操作是一个表达式,并且具有一个值,即赋值后左边变量保存的值。也就是说,赋值可以作为更大的表达式的一部分出现。如果将为c赋值的操作放在 while 循环语句的测试部分中,上述字符复制程序便可以改写成下列形式:
#include
/* copy input to output; 2nd version */
int main() {
int c;
while ((c = getchar()) != EOF)
putchar(c);
}
在该程序中,while 循环语句首先读一个字符并将其赋值给c,然后测试该字符是否为文件结束标志。如果该字符不是文件结束标志,则执行 while语句体,并打印该字符。随后重复执行 while 语句。当到达输入的结尾位置时,while 循环语句终止执行,从而整个 main 函数执行结束。
以上这段程序将输入集中化,getchar 函数在程序中只出现了一次,这样就缩短了程序,整个程序看起来更紧凑。习惯这种风格后,读者就会发现按照这种方式编写的程序更易阅读。我们经常会看到这种风格。(不过,如果我们过多地使用这种类型的复杂语句,编写的程序可能会很难理解,应尽量避免这种情况。)
对 while语句的条件部分来说,赋值表达式两边的圆括号不能省略。不等于运算符!=的优先级比赋值运算符=的优先级要高,这样,在不使用圆括号的情况下关系测试!=将在赋值=操作之前执行。因此语句
c =getchar()!= EOF
等价于语句
c=(getchar()!= EOF)
该语句执行后,c的值将被置为0或1(取决于调用 getchar 函数时是否碰到文件结束标志),这并不是我们所希望的结果。
练习:
1.验证表达式 getchar()!= EOF 的值是0还是 1。
2.编写一个打印 EOF 值的程序。
参考代码:
#include
int main(void)
{
printf("Press a key. ENTER would be nice :-)\n\n");
printf("The expression getchar() != EOF evaluates to %d\n", getchar() != EOF);
return 0;
}
#include
int main(void)
{
printf("The value of EOF is %d\n\n", EOF);
return 0;
}
下列程序用于对字符进行计数,它与上面的复制程序类似。
#include
/* count characters in input; 1st version */
int main() {
long nc;
nc = 0;
while (getchar() != EOF)
++nc;
printf("%ld\n", nc);
}
在 CLion 的控制台中,发送 EOF 需要使用 Debug 模式,并在结束位置使用 Ctrl + D 快捷键即可。
其中,语句 ++nc;
引入了一个新的运算符++,其功能是执行加1操作。可以用语句 nc = nc +1代替它,但语句++nc 更精炼一些,且通常效率也更高。与该运算符相应的是自减运算符--。++与--这两个运算符既可以作为前缀运算符(如++nc),也可以作为后缀运算符(如 nc++)。我们在后面案例中将看到这两种形式在表达式中具有不同的值,但++nc 与 nc++都使 nc 的值增加 1。目前,我们只使用前缀形式。
该字符计数程序使用 long 类型的变量存放计数值,而没有使用 int 类型的变量。long整型数(长整型)至少要占用 32 位存储单元。在某些机器上 int 与 long 类型的长度相同,但在一些机器上,int 类型的值可能只有 16 位存储单元的长度(最大值为 32767),这样,相当小的输入都可能使 int 类型的计数变量溢出。转换说明%ld 告诉 printf 函数其对应的参数是 long 整型。
使用 double(双精度浮点数)类型可以处理更大的数字。我们在这里不使用 while 循环语句,而用 for 循环语句来展示编写此循环的另一种方法:
#include
/* count characters in input; 2nd version */
int main() {
double nc;
for (nc = 0; getchar() != EOF; ++nc);
printf("%.0f\n", nc);
}
对于 float 与 double 类型。printf 函数都使用 %f 进行说明。%.0f 强制不打印小数点和小数部分,因此小数部分的位数为 0。
在该程序段中,for 循环语句的循环体是空的,这是因为所有工作都在测试(条件)部分与增加步长部分完成了。但C语言的语法规则要求 for 循环语句必须有一个循环体,因此用单独的分号代替。单独的分号称为空语句,它正好能满足 for 语句的这一要求。把它单独放在一行是为了更加醒目。
在结束讨论字符计数程序之前,我们考虑以下情况:如果输入中不包含字符,那么,在第一次调用 getchar 函数的叫候,while 语句或 for 语句中的条件测试从一开始就为假,程序的执行结果将为 0,这也是正确的结果。这一点很重要。while 语句与 for 语句的优点之一就是在执行循环体之前就对条件进行测试,如果条件不满足,则不执行循环体,这就可能出现循环体一次都不执行的情况。在出现0长度的输入时,程序的处理应该灵活一些,在出现边界条件时,while语句与 for 语句有助于确保程序执行合理的操作。
接下来的这个程序用于统计输入中的行数。我们在上面提到过,标准库保证输入文本流以行序列的形式出现,每一行均以换行符结束。因此,统计行数等价于统计换行符的个数。
#include
/* count lines in input */
int main() {
int c, nl;
nl = 0;
while ((c = getchar()) != EOF)
if (c == '\n')
++nl;
printf("%d\n", nl);
}
在该程序中,while 循环语句的循环体是一个 if 语句,它控制自增语句++nl。if 语句先测试圆括号中的条件,如果该条件为真,则执行其后的语句(或括在花括号中的一组语句)。这里再次用缩进方式表明语句之间的控制关系。
双等于号==是C语言中表示“等于”关系的运算符。由于C 语言将单等于号=作为赋值运算符,因此使用双等于号==-表示相等的逻辑关系,以示区分。这里提醒注意,在表示“等于”逻辑关系的时候(应该用==),C语言初学者有时会错误地写成单等于号=。即使这样误用了,其结果通常仍然是合法的表达式,因此系统不会给出警告信息。
单引号中的字符表示一个整型值,该值等于此字符在机器字符集中对应的数值,我们称之为字符常量。但是,它只不过是小的整型数的另一种写法而已。例如,'A' 是一个字符常量;在 ASCII 字符集中其值为 65(即字符 A 的内部表示值为 65)。当然,用''要比用 65 好,因为。'A'的意义更清楚,且与特定的字符集无关。
字符串常量中使用的转义字符序列也是合法的字符常量,比如,"\n'代表换行符的值,在 ASCI 字符集中其值为 10。我们应当注意到,"\n'是单个字符,在表达式中它不过是一个整型数而已;而"\n"是一个仅包含一个字符的字符串常量。有关字符串与字符之间的关系,我们将在第2章进一步讨论。
最后单词数统计案例。这里对单词的定义比较宽松,它是任何其中不包含空格、制表符或换行符的字符序列。下面这段程序是 UNIX 系统 中 wc 程序的骨干部分:
#include
#define IN 1 /* inside a word */
#define OUT 0 /* outside a word */
/* count lines, words, and characters in input */
int main() {
int c, nl, nw, nc, state;
state = OUT;
nl = nw = nc = 0;
while ((c = getchar()) != EOF) {
++nc;
if (c == '\n')
++nl;
if (c == ' ' || c == '\n' || c = '\t')
state = OUT;
else if (state == OUT) {
state = IN;
++nw;
}
}
printf("%d %d %d\n", nl, nw, nc);
}
程序执行时,每当遇到单词的第一个字符,它就作为一个新单词加以统计。state 变量记录程序当前是否正位于一个单词之中,它的初值是“不在单词中”,即初值被赋为 OUT。我们在这里使用了符号常量 IN 与 OUT,而没有使用其对应的数值1与 0,这样程序更易读。在较小的程序中,这种做法也许看不出有什么优势,但在较大的程序中,如果从一开始就这样做,因此而增加的一点工作量与提高程序可读性带来的好处相比是值得的。读者也会发现,如果程序中的幻数都以符号常量的形式出现,对程序进行大量修改就会相对容易得多。
下列语句
nl = nw = nc = 0;
将把其中的 3个变量 n1、nw 与 nc 都设置为 0。这种用法很常见,但要注意这样一个事实:在兼有值与赋值两种功能的表达式中,赋值结合次序是由右至左。所以上面这条语句等同于
n1 = (nw = (nc = 0));
运算符||代表 OR(逻辑或),所以下列语句
if (c == ' ' || c== '\n' || c == '\t')
的意义是“如果c是空格,或c是换行符,或c是制表符”(前面讲过,转义字符序列\t 是制表符的可见表示形式)。相应地,运算符&&代表 AND(逻辑与),它仅比 || 高一个优先级。由 && 或 || 连接的表达式由左至右求值,并保证在求值过程中只要能够判断最终的结果为真或假,求值就立即终止。如果 c 是空格,则没有必要再测试它是否为换行符或制表符,这样就不必执行后面两个测试。在这里,这一点并不特别重要,但在某些更复杂的情况下这样做就有必要了,不久我们将会看到这种例子。
这段程序中还包括一个 else 部分,它指定当 if 语句中的条件部分为假时所要执行的动作。其一般形式为:
if(表述式)
语句 1
else
语句 2
其中,if-else 中的两条语句有且仅有一条语句被执行。如果表达式的值为真,则执行语句 1,否则执行语句 2。这两条语句都既可以是单条语句,也可以是括在花括号内的语句序列。在单词计数程序中,else之后的语句仍是一个 if 语句,该 if 语句控制了包含在花括号内的两条语句。
练习:
练习 1-8编写一个统计空格、制表符与换行符个数的程序。
练习 1-9编写一个将输入复制到输出的程序,并将其中连续的多个空格用一个空格代替。
练习 1-10编写一个将输入复制到输出的程序,并将其中的制表符替换为\t,把回退符替换为\b,把反斜杠替按为\\。这样可以将制表符和回退符以可见的方式显示出来。
练习 1-11你准备如何测试单词计数程序?如果程序中存在某种错误,那么什么样的输入最可能发现这类错误呢。
练习 1-12 编写一个程序,以每行一个单词的形式打印其输入。
在这部分内容中,我们来编写一个程序,以统计各个数字、空白符(包括空格符、制表符及换行符)以及所有其它字符出现的次数。这个程序的实用意义并不大,但我们可以通过该程序讨论 C 语言多方面的问题。
所有的输入字符可以分成12类,因此可以用一个数组存放各个数字出现的次数,这样比使用 10 个独立的变量更方便。下面是该程序的一种版本:
#include
/* count digits, white space, others */
int main() {
int c, i, white, other;
int digit[10];
white = other = 0;
for (i = 0; i < 10; ++i)
digit[i] = 0;
while ((c = getchar()) != EOF)
if (c >= '0' && c <= '9')
++digit[c - '0'];
else if (c == ' ' || c == '\n' || c == '\t')
++white;
else
++other;
printf("digits =");
for (i = 0; i < 10; ++i)
printf(" %d", digit[i]);
printf(", white space = %d, other = %d\n",
white, other);
}
该程序中的声明语句
int digit[10];
将变量 digit 声明为由 10 个整型数构成的数组。在C语言中,数组下标总是从0开始,因此该数组的 10 个元素分别为 digit[0]、digit[1]、…、digit[9],这可以通过初始化和打印数组的两个 for 循环语句反映出来。
数组下标可以是任何整型表达式,包括整型变量(如i)以及整型常量。
该程序的执行取决于数字的字符表示属性。例如,测试语句
if (c >= '0' && c <= '9')
用于判断c中的字符是否为数字。如果它是数字,那么该数字对应的数值是
c- '0'
只有当'0'、'1'、…、'9'具有连续递增的值时,这种做法才可行。幸运的是,所有的字符集都是这样的。
由定义可知,char 类型的字符是小整型,因此 char 类型的变量和常量在算术表达式中等价于 int 类型的变量和常量。这样做既自然又方便,例如,c-'0'是一个整型表达式,如果存储在 c中的字符是'0'~'9',其值将为 0~9,因此可以充当数组 ndigit 的合法下标。
判断一个字符是数字、空白符还是其它字符的功能可以由下列语句序列完成:
if (c >= '0' && c <= '9')
++digit[c - '0'];
else if (c == ' ' || c == '\n' || c == '\t')
++white;
else
++other;
程序中经常使用下列方式表示多路判定:
if(条件 1)
语句 1
else if(条件 1)
语句 2 ...else
语句 n
在这种方式中,各条件从前往后依次求值,直到满足某个条件,然后执行对应的语句部分。这部分语句执行完成后,整个语句体执行结束(其中的任何语句都可以是括在花括号中的若干条语句)。如果所有条件都不满足,则执行位于最后一个 else 之后的语句(如果有的话)。类似于前面的单词计数程序,如果没有最后一个 else 及对应的语句,该语句体将不执行任何动作。在第一个 if 与最后一个 else之间可以有0个或多个下列形式的语句序列:
else if(条件 1)
语句 2
就程序设计风格而言,我们建议读者采用上面所示的缩进格式以体现该结构的层次关系,否则,如果每个 if 都比前一个 else 向里缩进一些距离,那么较长的判定序列就可能超出页面的右边界。
练习:
函数为计算的封装提供了一种简便的方法,此后使用函数时不需要考虑它是如何实现的。使用设计正确的函数,程序员无需考虑功能是如何实现的,而只需知道它具有哪些功能就够了。在 C 语言中可以简单、方便、高效地使用函数。我们经常会看到在定义后仅调用了一次的短函数,这样做可以使代码段更清晰易读。
到目前为止,我们所使用的函数(如 printf、getchar 和 putchar 等)都是函数库中提供的函数。现在,让我们自己动手来编写一些函数。C语言没有像其他编程语言一样提供类似于**的求幂运算符,我们现在通过编写一个求幂的函数 power (m,n)来说明函数定义的方法。power(m,n)函数用于计算整数m的n次幂,其中n是正整数。对函数调用power(2,5)来说,其结果值为 32。该函数并非一个实用的求幂函数,它只能处理较小的整数的正整数次幂,但这对于说明问题已足够了。(标准库中提供了一个计算x^y的函数 pow(x,y)。)
下面是函数 power(m,n)的定义及调用它的主程序,这样我们可以看到一个完整的程序结构。
#include
int power(int m, int n);
/* test power function */
int main() {
int i;
for (i = 0; i < 10; ++i)
printf("%d %d %d\n", i, power(2, i), power(-3, i));
return 0;
}
/* power: raise base to n-th power; n >= 0 */
int power(int base, int n) {
int i, p;
p = 1;
for (i = 1; i <= n; ++i)
p = p * base;
return p;
}
函数定义的一般形式为:
返回值类型 函数名(0个或多个参数声明) {
声明部分语句序列
}
函数定义可以以任意次序出现在一个源文件或多个源文件中,但同一函数不能分割存放在多个文件中。如果源程序分散在多个文件中,那么,在编译和加载时,就需要做更多的工作,但这是操作系统的原因,并不是语言的属性决定的。我们暂且假定将main 和 power 这两个函数放在同一文件中,这样前面所学的有关运行C语言程序的知识仍然有效。
main 函数在下列语句中调用了两次 power 函数:
printf("%d %d %d\n", i, power(2, i), power(-3, i));
每次调用时,main 函数向 power 函数传递两个参数;在调用执行完成时,power 函数向 main函数返回一个格式化的整数并打印。在表达式中,power(2,i)同2和i一样都是整数。
power 函数的第一行语句
int power(int base,int n)
声明参数的类型、名字以及该函数返回结果的类型。power 函数的参数使用的名字只在 power函数内部有效,对其它任何函数都是不可见的:其它函数可以使用与之相同的参数名字而不会引起冲突。变量i与p也是这样:power 函数中的i与main 函数中的i无关。
我们通常把函数定义中圆括号内列表中出现的变量称为形式参数,而把函数调用中与形式参数对应的值称为实际参数。 power 函数计算所得的结果通过 return 语句返回给 main 函数。关键字 return 的后面可以跟任何表达式,形式为:
return 表达式;
函数不一定都有返回值。不带表达式的 return 语句将把控制权返回给调用者,但不返回有用的值。这等同于在到达函数的右终结花括号时,函数就“到达了尽头”。主调函数也可以忽略函数返回的值。
main 函数的末尾有一个 return 语句。由于 main 本身也是函数,因此也可以向其调用者返回一个值,该调用者实际上就是程序的执行环境。一般来说,返回值为0表示正常终止,返回值为非0表示出现异常情况或出错结束条件。为简洁起见,前面的 main 函数都省略了 return 语句,但我们将在以后的 main 函数中包含 return 语句,以提醒大家注意,程序还要向其执行环境返回状态。
int power(int m, int n);
表明 power 函数有两个 int 类型的参数,并返回一个 int 类型的值。这种声明称为函数原型,它必须与 power 函数的定义和用法一致。如果函数的定义、用法与函数原型不一致,将出现错误。
在C语言的最初定义中,可以在程序的开头声明 power 函数可以不指明参数列表,这样编译器就无法在此时检查 power 函数调用的合法性。后来不再推荐这张写法。
如果不想声明函数也可以将函数编写的代码替换前面的声明部分,即将 power 移到 main 方法的前面。
习惯其它语言的程序员可能会对C 语言的函数参数传递方式感到陌生。在C语言中,所有函数参数都是“通过值”传递的。也就是说,传递给被调用函数的参数值存放在临时变量中,而不是存放在原来的变量中。这与其它某些语言是不同的,比如,Fortran 等语言是“通过引用调用”,Pascal 则采用 var 参数的方式,在这些语言中,被调用的函数必须访问原始参数,而不是访问参数的本地副本。
最主要的区别在于,在C语言中,被调用函数不能直接修改主调函数中变量的值,而只能修改其私有的临时副本的值。
传值调用的利大于弊。在被调用函数中,参数可以看作是便于初始化的局部变量,因此额外使用的变量更少。这样程序可以更紧凑简洁。侧如,下面的这个 power 函数利用了这性质:
/* power: raise base to n-th power; n >= 0 */
int power(int base, int n) {
int i, p;
p = 1;
for (i = 1; i <= n; ++i)
p = p * base;
return p;
}
其中,参数n用作临时变量,并通过随后执行的 for 循环语句递减,直到其值为 0,这样就不需要额外引入变量 i;power 函数内部对n的任何操作不会影响到调用函数中n的原始参数值。
必要时,也可以让函数能够修改主调函数中的变量。这种情况下,调用者需要向被调用函数提供待设置值的变量的地址(从技术角度看,地址就是指向变量的指针),而被调用函数则需要将对应的参数声明为指针类型,并通过它间接访问变量。我们将在后面课程中讨论指针。
如果是数组参数,情况就有所不同了。当把数组名用作参数时,传递给函数的值是数组起始元素的位置或地址--它并不复制数组元素本身。在被调用函数中,可以通过数组下标访问或修改数组元索的值。这是下一节将要讨论的问题。
练习:
练习 1-15 重新编写 1.2节中的温度转换程序,使用数实现温度转换计算。
字符数组是C语言中最常用的数组类型。下面我们通过编写一个程序,来说明字符数组以及操作字符数组的函数的用法。该程序读入一组文本行,并把最长的文本行打印出来。该算法的基本框架非常简单:
while(还有未处理的行)
if(该行比已处理的最长行还要长)
保存该行为最长行
保存该行的长度
打印最长的行
从上面的框架中很容易看出,程序很自然地分成了若干片断,分别用于读入新行、测试读入的行、保存该行,其余部分则控制这一过程。
因为这种划分方式比较合理,所以可以按照这种方式编写程序。首先,我们编写一个独立的函数 getline,它读取输入的下一行。我们尽量保持该函数在其它场台也有用。至少getline 函数应该在读到文件末尾时返回一个信号;更为有用的设计是它能够在读入文本行时返回该行的长度,而在遇到文件结束符时返回0。由于0不是有效的行长度,因此可以作为标志文件结束的返回值。每一行至少包括一个字符,只包含换行符的行,其长度为1。
当发现某个新读入的行比以前读入的最长行还要长时,就需要把该行保存起来。也就是说,我们需要用另一个函数 copy 把新行复制到一个安全的位置。
最后,我们需要在主函数 main 中控制 getline 和 copy 这两个函数。以下便是我们编写的程序:
#include
#define MAXLINE 1000 /* maximum input line length */
int get_line(char line[], int maxline);
void copy(char to[], char from[]);
/* print the longest input line */
int main() {
int len; /* current line length */
int max; /* maximum length seen so far */
char line[MAXLINE]; /* current input line */
char longest[MAXLINE]; /* longest line saved here */
max = 0;
while ((len = get_line(line, MAXLINE)) > 0)
if (len > max) {
max = len;
copy(longest, line);
}
if (max > 0) /* there was a line */
printf("%s", longest);
return 0;
}
/* get_line: read a line into s, return length */
int get_line(char s[], int lim) {
int c, i;
for (i = 0; i < lim - 1 && (c = getchar()) != EOF && c != '\n'; ++i)
s[i] = c;
if (c == '\n') {
s[i] = c;
++i;
}
s[i] = '\0';
printf("get_line:%s",s);
return i;
}
/* copy: copy 'from' into 'to'; assume to is big enough */
void copy(char to[], char from[]) {
int i;
i = 0;
while ((to[i] = from[i]) != '\0')
++i;
}
程序的开始对 getline 和 copy 这两个函数进行了声明,这里假定它们都存放在同一个文件中。 main 与 getline 之间通过一对参数及一个返回值进行数据交换。在 getline 函数中,两个参数是通过程序行
int getline(char s[], int lim)
声明的,它把第一个参数s声明为数组,把第二个参数 lim 声明为整型,声明中提供数组大小的目的是留出存储空间。在 getline 函数中没有必要指明数组s的长度,这是因为该数组的大小是在 main 函数中设置的。如同 power 函数一样,getline 函数使用了一个 return语句将值返回给其调用者。上述程序行也声明了 getline 数的返回值类型为 int。由于函数的默认返回值类型为 int,因此这里的 int 可以省略。
有些函数返回有用的值,而有些函数(如 copy)仅用于执行一些动作,并不返回值。copy函数的返回值类型为 void,它显式说明该函数不返回任何值。
getline 函数把字符"\0'(即空字符,其值为 0)插入到它创建的数组的末尾,以标记字符串的结束。这一约定已被C语言采用:当在C语言程序中出现类似于 "hello\0" 的字符串常量时,它将以字符数组的形式存储,数组的各元素分别存储字符串的各个字符,并以'\0'标志字符串的结束。
printf 函数中的格式规范名%s 规定,对应的参数必须是以这种形式表示的字符串。copy 函数的实现正是依赖于输入参数由"\0'结束这一事实,它将"\0'拷贝到输出参数中。(也就是说,空字符'\0'不是普通文本的一部分。)
值得一提的是,即使是上述这样很小的程序,在传递参数时也会遇到一些麻烦的设计问题。例如,当读入的行长度大于允许的最大值时,main 函数应该如何处理,getline 函数的执行是安全的,无论是否到达换行符字符,当数组满时它将停止读字符。main 函数可以通过测试行的长度以及检查返回的最后一个字符来判定当前行是否太长,然后再根据具体的情况处理。为了简化程序,我们在这里不考虑这个问题。
调用 getline 函数的程序无法预先知道输入行的长度,因此 getline 函数需要检查是否溢出。另一方面,调用 copy 函数的程序知道(也可以找出)字符串的长度,因此该函数不需要进行错误检查。
练习:
练习 1-16 修改打印最长文本行的程序的主程序 main,使之可以打印任意长度的输入行的长度,并尽可能多地打印文本。
练习 1-17 编写一个程序,打印长度大于 80个字符的所有输入行。
练习 1-18 编写一个程序,删除每个输入行末尾的空格及制表符,并删除完全是空格的行。
练习 1-19 编写函数 reverse(s),将字符串s中的字符顺序颠倒过来。使用该函数编写一个程序,每次颠倒一个输入行中的字符顺序。
练习 2-1 编写一个程序以确定分别由 signed 及 unsigned 限定的 char、short、int 与 1ong 类型变量的取值范围。采用打印标准头文件中的相应值以及直接计算两种方式实现。后一种方法的实现较困难一些,因为要确定各种浮点类型的取值范围。
对变量的命名与符号常量的命名存在一些限制条件,名字是由字母和数字组成的序列,但其第一个字符必须为字母。下划线"”被看做是字母,通常用于命名较长的变量名,以提高其可读性。由于例程的名字通常以下划线开头,因此变量名不要以下划线开头。大写字母与小写字母是有区别的,所以,x与x是两个不同的名字。在传统的C语言用法中,变量名使用小写字母,符号常量名全部使用大写字母。
类似于 if、else、int、float 等关键字是保留给语言本身使用的,不能把它们用做变量名。所有关健字中的字符都必须小写。选择的变量名要能够尽量从字面上表达变量的用途,这样做不容易引起混淆。局部变量般使用较短的变量名(尤其是循环控制变量),外部变量使用较长的名字。
C 语言只提供了下列几种基本数据类型: char(字符型,占用一个字节,可以存放本地字符集中的一个字符)int(整型,通常反映了所用机器中整数的最自然长度)float(单精度浮点型)double(双精度浮点型)此外,还可以在这些基本数据类型的前面加上一些限定符。short 与 long 两个限定符用于限定整型:
short int sh;
long int counter;
在上述这种类型的声明中,关键字 int 可以省略。通常很多人也习惯这么做。
short 与 long 两个限定符的引入可以为我们提供满足实际需要的不同长度的整型数。int 通常代表特定机器中整数的自然长度。short 类型通常为 16 位,long 类型通常为 32位,int 类型可以为16 位或 32 位。各编译器可以根据硬件特性自主选择合适的类型长度,但要遵循下列限制: short 与 int 类型至少为 16 位,而 long 类型至少为 32 位,并且 short类型不得长于 int 类型,而 int 类型不得长于 long 类型。
类型限定符 signed 与 unsigned 可用于限定 char 类型或任何整型。unsigned 类型的数总是正值或 0,并遵守算术模 2^n定律,其中n是该类型占用的位数。例如,如果 char 对象占用8位,那么 unsigned char 类型变量的取值范围为 0~255,而 signed char 类型变量的取值范围则为-128~127(在采用对二的补码的机器上)。不带限定符的 char 类型对象是否带符号则取决于具体机器,但可打印字符总是正值。
类似于 1234 的整数变量属于 int 类型。long 类型的变量以字母l或L结尾,如123456789L。如果一个整数太大以至于无法用 int 类型表示时,也将被当作 long 类型处理。无符号变量以字母u或U结尾。后缀 ul 或 UL 表明是 unsigned long 类型。
浮点数变量中包含一个小数点(如 123.4)或一个指数(如 1e-2),也可以两者都有。没有后缀的浮点数变量为 double 类型,而后缀f或F表示 float 类型。
字符串变量也叫字符串字面值,是用双引号括起来的0个或多个字符组成的字符序列。
"I am a string"
或
"" /* 空字符串 */
都是字符串。双引号不是字符串的一部分,它只用于限定字符串。字符变量中使用的转义字符序列同样也可以用在字符串中。在字符串中使用\"表示双引号字符。
从技术角度看,字符串变量就是字符数组。字符串的内部表示使用一个空字符"\0'作为串的结尾,因此。存储字符串的物理存储单元数比括在双引号中的字符数多一个。这种表示方法也说明,C语言对字符串的长度没有限制,但程序必须扫描完整个字符串后才能确定字符串的长度。标准库函数 strlen(s)可以返回字符串参数s的长度,但长度不包括末尾的'\0'下面是我们设计的 strlen 函数的一个版本:
#include
int str_len(char s[]);
int main() {
char s[] = "hello world!";
printf("s:%s\n", s);
printf("length:%d", str_len(s));
}
/* strlen: return length of s */
int str_len(char s[]) {
int i;
while (s[i] != '\0')
++i;
return i;
}
标准头文件中声明了 strlen 和其它字符串函数。
#include
#include
int main() {
char s[] = "hello world!";
printf("s:%s\n", s);
printf("length:%d", strlen(s));
}
我们应该搞清楚字符常量与仅包含一个字符的字符串之间的区别:'x'与"x"是不同的。前者是一个整数,其值是字母x在机器字符集中对应的数值(内部表示值);后者是一个包含一个字符(即字母x)以及一个结束符"\0'的字符数组。
任何变量的声明都可以使用 const 限定符限定。该限定符指定变量的值不能被修改称为常量。对数组而言,const限定符指定数组所有元素的值都不能被修改:
const double e = 2.71828182845905;
const char msg[] = "warning: ";
const 限定符也可配合函数参数使用,它表明函数不能修改参数元素的值
int strlen(const char[]);
枚举常量是另外一种类型的常量。枚举是一个常量整型值的列表,例如:
enum boolean { NO, YES };
在没有显式说明的情况下,enum 类型中第一个枚举名的值为 0,第二个为 1,依此类推。如果只指定了部分枚举名的值,那么未指定值的枚举名的值将依着最后一个指定值向后递增,参看下面两个例子中的第二个例子:
enum escapes { BELL = '\a', BACKSPACE = '\b', TAB = '\t',
NEWLINE = '\n', VTAB = '\v', RETURN = '\r' };
enum months { JAN = 1, FEB, MAR, APR, MAY, JUN,JUL, AUG, SEP, OCT, NOV, DEC };
/*FEB 的值为 2,MAR 的值为 3,依此类推*/
不同枚举中的名字必须互不相同。同一枚举中不同的名字可以具有相同的值。 枚举为建立常量值与名字之间的关联提供了一种便利的方式。相对于#define 语句来说,它的优势在于常量值可以自动生成。尽管可以声明 enum 类型的变量,但编译器不检查这种类型的变量中存储的值是否为该枚举的有效值。不过,举变量提供这种检查,因此枚举比#define 更具优势。此外,调试程序可以以符号形式打印出枚举变量的值。
练习:
练习 2-1 编写一个程序以确定分别由 signed 及 unsigned 限定的 char、short、int 与 1ong 类型变量的取值范围。采用打印标准头文件中的相应值以及直接计算两种方式实现。后一种方法的实现较困难一些,因为要确定各种浮点类型的取值范围。
二元算术运算符包括:+、-、*、/、%号(取模运算符)。整数除法会截断结果中的小数部 分。表达式
x % y
的结果是 x 除以 y的余数,当x能被y整除时,其值为 0。例如,如果某一年的年份能被 4整除但不能被 100 整除,那么这一年就是闰年,此外,能被 400 整除的年份也是闰年。因此,可以用下列语句判断闰年:
if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0)
printf("%d is a leap year\n", year);
else
printf("%d is not a leap year\n", year);
取模运算符不能应用于 float 或 double 类型。在有负操作数的情况下,整数除法截取的方向以及取模运算结果的符号取决于具体机器的实现,这和处理上溢或下溢的情况是样的。
二元运算符+和-具有相同的优先级,它们的优先级比运算符*、/和的优先级低。算术运算符采用从左到右的结合规则。
关系运算符包括下列几个运算符:
> >= < <=
它们具有相同的优先级。优先级仅次于它们的是相等性运算符:
== !=
关系运算符的优先级比算术运算符低。因此,表达式
i < lim -1 等价于 i < (lim - 1)
逻辑运算符&&与||有一些较为特殊的属性,由&&与||连接的表达式按从左到右的顺序进行求值,并且,在知道结果值为真或假后立即停止计算。绝大多数C语言程序运用了这些属性。例如,下列在功能上与前面的输入函数 getline中的循环语句等价的循环语句:
for (i=0; i
在读入一个新字符之前必须先检查数组s中足否还有空间存放这个字符,因此必须首先测试条件 i 小于 lim-1。如果这一测试失败,就没有必要继续读入下一字符。 类似地,如果在调用 getchar 函数之前就测试c是否为EOF,结果也是不正确的,因此,函数的调用与赋值都必须在对c中的字符进行测试之前进行。 运算符&&的优先级比的优先级高,但两者都比关系运算符和相等性运算符的优先级低。因此,表达式
i
就不需要另外加圆括号。但是,由于运算符!=的优先级高于赋值运算符的优先级,因此,在表达式
(c = getchar()) != '’\n'
中,就需要使用圆括号,这样才能达到预期的目的:先把函数返回值赋值给c,然后再将与'\n'进行比较。
根据定义,在关系表达式或逻辑表达式中,如果关系为真,则表达式的结果值为数值1:如果为假,则结果值为数值 0。
逻辑非运算符!的作用是将非0操作数转换为0,将操作数0转换为1。该运算符通常用于下列类似的结构中:
if(!valid)
一般不采用下列形式:
if(valid == 0)
当然,很难评判上述两种形式哪种更好。类似于!va1id的用法读起来更直观一些(“如果不是有效的”),但对于一些更复杂的结构可能会难于理解。
C语言提供了两个用于变量递增与递减的特殊运算符。自增运算符++使其操作数递增1,自减运算符使其操作数递减1。我们经常使用++运算符递增变量的值,如下所示:
if (c = '\n')
++nl;
++与--这两个运算符特殊的地方主要表现在:它们既可以用作前缀运算符(用在变量前面,如++n)。也可以用作后缀运算符(用在变量后面,如n++)。在这两种情况下,其效果都是将变量n的值加1。但是,它们之间有一点不同。表达式++n 先将n的值递增1,然后再使用变量n的值,而表达式n++则是先使用变量n的值,然后再将n的值递增1。也就是说对于使用变量n的值的上下文来说,++n和n++的效果是不同的。如果n的值为5,那么
x = n++;
执行后的结果是将x的值置为5,而
x = ++n;
将x的值置为6。这两条语句执行完成后,变量n的值都是6。自增与自减运算符只能作用于变量,类似于表达式(i+j)++是非法的。
在不需要使用任何具体值且仅需要递增变量的情况下,前缀方式和后缀方式的效果相同。例如:
if (c == '\n')
nl++;
但在某些情况下需要酌情考虑。例如,考虑下面的函数squeeze(s,c),它删除s字符串中出现的所有字符c
/* squeeze: delete all c from s */
void squeeze(char s[], int c) {
int i, j;
for (i = j = 0; s[i] != '\0'; i++)
if (s[i] != c)
s[j++] = s[i];
s[j] = '\0';
}
每当出现一个不是c的字符时,该函数把它拷贝到数组中下标为j的位置,随后才将」的值增加1,以准备处理下一个字符。其中的if语句完全等价于下列语句:
if (s[i] != c) {
s[j] = s[i];
j++;
}
练习:
练习 2-2 在不使用运算符&&或|的条件下编写一个与上面的for循环语句等价的循环语句。