第2章 C语言基本概念
某个人的常量可能是其他人的变量。
本章介绍了C语言的一些基本概念,包括预处理指令、函数、变量和语句。即使是编写最简单的C程序,也会用到这些基本概念。后续几章将会对这些概念进行更详细的描述。
首先,2.1节给出一个简单的C程序,并且描述了如何对这个程序进行编译和链接。接着,2.2节讨论如何使程序通用。2.3节说明如何添加说明性解释,即通常所说的注释。2.4节介绍变量,变量是用来存储程序执行过程中可能会发生改变的数据的。2.5节说明利用scanf函数把数据读入变量的方法。就如2.6节介绍的那样,常量是程序执行过程中不会发生改变的数据,用户可以对其进行命名。最后,2.7节解释C语言的命名(标识符)规则,2.8节给出了C程序的布局规范。
2.1 编写一个简单的C程序
与用其他语言编写的程序相比,C程序较少要求“形式化的东西”。一个完整的C程序可以只有寥寥数行。
程序 显示双关语
在Kernighan和Ritchie编写的经典C语言著作The C Programming Language一书中,第一个程序是极其简短的。它仅仅输出了一条hello,world消息。与大多数C语言书籍的作者不同,我不打算用这个程序作为第一个C程序示例,而更愿意尊重另一个C语言的传统:显示双关语。下面是一条双关语:
To C, or not to C: that is the question.
下面这个名为pun.c的程序会在每次运行时显示上述消息。
pun.c
#include <stdio.h> int main(void) { printf("To C, or not to C: that is the question.\n"); return 0; }
2.2节会对这段程序中的一些格式进行详尽的说明,这里仅做简要介绍。程序中第一行
#include <stdio.h>
是必不可少的,它“包含”了C语言标准输入/输出库的相关信息。程序的可执行代码都在main函数中,这个函数代表“主”程序。main函数中的第一行代码是用来显示期望信息的。printf函数来自标准输入/输出库,可以产生完美的格式化输出。代码\n告诉printf函数执行完消息显示后要进行换行操作。第二行代码,
return 0;
表明程序终止时会向操作系统返回值0。
2.1.1 编译和链接
尽管pun.c程序十分简短,但是为运行这个程序而包含的内容可能比想象的要多。首先,需要生成一个含有上述程序代码名为pun.c的文件(使用任何文本编辑器都可以创建该文件)。文件的名字无关紧要,但是编译器通常要求带上文件的扩展名.c。
接下来,就需要把程序转化为机器可以执行的形式。对于C程序来说,通常包含下列3个步骤。
·预处理。首先程序会被送交给预处理器(preprocessor)。预处理器执行以#开头的命令(通常称为指令)。预处理器有点类似于编辑器,它可以给程序添加内容,也可以对程序进行修改。
·编译。修改后的程序现在可以进入编译器(compiler)了。编译器会把程序翻译成机器指令(即目标代码)。然而,这样的程序还是不可以运行的。
·链接。在最后一个步骤中,链接器(linker)把由编译器产生的目标代码和所需的其他附加代码整合在一起,这样才最终产生了完全可执行的程序。这些附加代码包括程序中用到的库函数(如printf函数)。
幸运的是,上述过程往往是自动实现的,因此人们会发现这项工作不是太艰巨。事实上,由于预处理器通常会和编译器集成在一起,所以人们甚至可能不会注意到它在工作。
根据编译器和操作系统的不同,编译和链接所需的命令也是多种多样的。在UNIX系统环境下,通常把C编译器命名为cc。为了编译和链接pun.c程序,需要在终端或命令行窗口录入如下命令:
% cc pun.c
(字符%是UNIX系统的提示符,不需要输入。)在使用编译器cc时,系统自动进行链接操作,而无需单独的链接命令。
在编译和链接好程序后,编译器cc会把可执行程序放到默认名为a.out的文件中。编译器cc有许多选项,其中有一个选项(-o选项)允许为含有可执行程序的文件选择名字。例如,假设要把文件pun.c生成的可执行文件命名为pun,那么只需录入下列命令:
% cc -o pun pun.c
GCC编译器
GCC编译器是最流行的C编译器之一,它随Linux发行,但也有面向其他很多平台的版本。这种编译器的使用与传统的UNIX cc编译器相似。例如,编译程序pun.c可以使用以下命令:
% gcc -o pun pun.c
本章最后的“问与答”部分将提供更多关于GCC的信息。
2.1.2 集成开发环境
到目前为止,我们一直通过在操作系统提供的特殊窗口中键入命令的方式来调用“命令行”编译器。事实上,还可以使用集成开发环境(integrated development environment, IDE)进行编译。集成开发环境是一个软件包,我们可以在其中编辑、编译、链接、执行甚至调试程序。组成集成开发环境的各个部分可以协调工作。例如,当编译器发现程序中有错误时,它会让编辑器把包含出错代码的行突出显示出来。集成开发环境有很多种,本书不打算一一讨论它们,但我建议读者了解一下自己的平台上可以运行哪些集成开发环境。
2.2 简单程序的一般形式
下面一起来仔细研究一下pun.c程序,并且由此归纳出一些通用的程序格式。简单的C程序一般具有如下形式:
指令 int main(void) { 语句 }
在这个模板以及本书的其他类似模板中,所有以Courier字体显示的语句都代表实际的C语言程序代码,而所有以中文楷体显示的部分则表示需要由程序员提供的内容。
注意如何使用大括号来标出main函数的起始和结束。C语言使用{和}的方式非常类似于其他语言中begin和end的用法。这也说明了有关C语言一个共识:C语言极其依赖缩写词和特殊符号,这是C程序非常简洁(或者不客气地说含义模糊)的一个原因。
即使是最简单的C程序也依赖3个关键的语言特性:指令(在编译前修改程序的编辑命令)、函数(被命名的可执行代码块,如main函数)和语句(程序运行时执行的命令)。下面将详细讨论这些特性。
2.2.1 指令
在编译C程序之前,预处理器会首先对其进行编辑。我们把预处理器执行的命令称为指令。第14章和第15章会详细讨论指令,这里只关注#include指令。
程序pun.c由下列这行指令开始:
#include <stdio.h>
这条指令说明,在编译前把<stdio.h>中的信息“包含”到程序中。<stdio.h>包含了关于C标准输入/输出库的信息。C语言拥有大量类似于<stdio.h>的头(header)(➤15.2节),每个头都包含一些标准库的内容。这段程序中包含<stdio.h>的原因是:C语言不同于其他的编程语言,它没有内置的“读”和“写”命令。输入/输出功能由标准库中的函数实现。
所有指令都是以字符#开始的。这个字符可以把C程序中的指令和其他代码区分开来。指令默认只占一行,每条指令的结尾没有分号或其他特殊标记。
2.2.2 函数
函数类似于其他编程语言中的“过程”或“子例程”,它们是用来构建程序的构建块。事实上,C程序就是函数的集合。函数分为两大类:一类是程序员编写的函数,另一类则是作为C语言实现的一部分提供的函数。我们把后者称为库函数(library function),因为它们属于一个由编译器提供的函数“库”。
术语“函数”来源于数学。在数学中,函数是指根据一个或多个给定参数进行数值计算的规则:
C语言对“函数”这个术语的使用则更加宽松。在C语言中,函数仅仅是一系列组合在一起并且赋予了名字的语句。某些函数计算数值,某些函数不这么做。计算数值的函数用return语句来指定所“返回”的值。例如,对参数进行加1操作的函数可以执行语句
return x + 1 ;
而当函数要计算参数的平方差时,则可以执行语句
return y * y - z * z;
虽然一个C程序可以包含多个函数,但只有main函数是必须有的。main函数是非常特殊的:在执行程序时系统会自动调用main函数。在第9章,我们将学习如何编写其他函数,在此之前的所有程序都只包含一个main函数。
main函数的名字是至关重要的,绝对不能改写成begin或者start,甚至写成MAIN也不行。
如果main是一个函数,那么它会返回一个值吗?是的。它会在程序终止时向操作系统返回一个状态码。我们再来看看pun.c程序:
#include <stdio.h> int main(void) { printf("To C, or not to C: that is the question.\n"); return 0; }
main前面的int表明该函数将返回一个整数值。圆括号中的void表明main函数没有参数。语句
return 0;
有两个作用:一是使main函数终止(从而结束程序),二是指出main函数的返回值是0。在后面我们还将详细论述main函数的返回值(➤9.5节)。但是现在我们始终让main函数的返回值为0,这个值表明程序正常终止。
如果main函数的末尾没有return语句,程序仍然能终止。但是,许多编译器会产生一条警告信息(因为函数应该返回一个整数却没有这么做)。
2.2.3 语句
语句是程序运行时执行的命令。本书后面的几章(主要集中在第5章和第6章)将进一步探讨语句。程序pun.c只用到两种语句。一种是返回(return)语句,另一种则是函数调用(function call)语句。要求某个函数执行分派给它的任务称为调用这个函数。例如,程序pun.c为了在屏幕上显示一条字符串就调用了printf函数:
printf("To C, or not to C: that is the question.\n");
C语言规定每条语句都要以分号结尾。(就像任何好的规则一样,这条规则也有一个例外:后面会遇到的复合语句(➤5.2节)就不以分号结尾。)由于语句可以连续占用多行,有时很难确定它的结束位置,因此用分号来向编译器显示语句的结束位置。但指令通常都只占一行,因此不需要用分号结尾。
2.2.4 显示字符串
printf是一个功能强大的函数,第3章将会进一步介绍。到目前为止,我们只是用printf函数显示了一条字符串字面量(string literal)——用一对双引号包围的一系列字符。当用printf函数显示字符串字面量时,最外层的双引号不会出现。
当显示结束时,printf函数不会自动跳转到下一输出行。为了让printf跳转到下一行,必须在要显示的字符串中包含\n(换行符)。写换行符就意味着终止当前行,然后把后续的输出转到下一行。为了说明这一点,请思考把语句
printf("To C, or not to C: that is the question.\n");
替换成下面两个对printf函数的调用后所产生的效果:
printf("To C, or not to C: "); printf("that is the question.\n");
第一条printf函数的调用语句显示出To C, or not to C:,而第二条调用语句则显示出that is the question.并且跳转到下一行。最终的效果和前一个版本的printf语句完全一样,用户不会发现什么差异。
换行符可以在一个字符串字面量中出现多次。为了显示下列信息:
Brevity is the soul of wit. --Shakespeare
可以这样写:
printf("Brevity is the soul of wit.\n --Shakespeare\n");
2.3 注释
我们的pun.c程序仍然缺乏某些重要内容:文档说明。每一个程序都应该包含识别信息,即程序名、编写日期、作者、程序的用途以及其他相关信息。C语言把这类信息放在注释(comment)中。符号/*标记注释的开始,而符号*/则标记注释的结束。例如:
/* This is a comment */
注释几乎可以出现在程序的任何位置上。它既可以单独占行也可以和其他程序文本出现在同一行中。下面展示的程序pun.c就把注释加在了程序开始的地方:
/* Name: pun.c */ /* Purpose: Prints a bad pun. */ /* Author: K. N. King */ #include <stdio.h> int main(void) { printf("To C, or not to C: that is the question.\n"); return 0 ; }
注释还可以占用多行。一旦遇到符号/*,那么编译器读入(并且忽略)随后的内容直到遇到符号*/为止。如果愿意,还可以把一串短注释合并成为一条长注释:
/* Name: pun.c Purpose: Prints a bad pun. Author: K. N. King */
但是,上面这样的注释可能难于阅读,因为人们阅读程序时可能不易发现注释的结束位置。所以,单独把*/符号放在一行会很有帮助:
/* Name: pun.c Purpose: Prints a bad pun. Author: K. N. King */
更好的方法是用一个“盒形”格式把注释单独标记出来:
/********************************************************** * Name: pun.c * * Purpose: Prints a bad pun. * * Author: K. N. King * **********************************************************/
有些程序员通过忽略3条边框的方法来简化盒形注释:
/* * Name: pun.c * Purpose: Prints a bad pun. * Author: K. N. King */
简短的注释还可以与程序中的其他代码放在同一行:
int main(void) /* Beginning of main program */
这类注释有时也称作“翼型注释”。
如果忘记终止注释可能会导致编译器忽略程序的一部分。请思考一下下面的示例:
printf("My "); /* forgot to close this comment... printf("car "); printf("has "); /* so it ends here */ printf("fleas");
因为在第一条注释中遗漏了结束标志,所以编译器忽略掉了中间的两条语句,因此程序最终只打印了My fleas。
C99提供了另一种类型的注释,以//(两个相邻的斜杠)开始:
// This is a comment
这种风格的注释会在行末自动终止。如果要创建多于一行的注释,既可以使用以前的注释风格(/* ... */),也可以在每一行的前面加上//:
// Name: pun.c // Purpose: Prints a bad pun. // Author: K. N. King
新的注释风格有两个主要优点:首先,因为注释会在行末自动终止,所以不会出现未终止的注释意外吞噬部分程序的情况;其次,因为每行前面都必须有//,所以多行的注释更加醒目。
2.4 变量和赋值
很少有程序会像2.1节中的示例那样简单。大多数程序在产生输出之前往往需要执行一系列的计算,因此需要在程序执行过程中有一种临时存储数据的方法。和大多数编程语言一样,C语言中的这类存储单元被称为变量(variable)。
2.4.1 类型
每一个变量都必须有一个类型(type)。类型用来说明变量所存储的数据的种类。C语言拥有广泛多样的类型。但是现在,我们将只限定在两种类型范围内:int类型和float类型。由于类型会影响变量的存储方式以及允许对变量进行的操作,所以选择合适的类型是非常关键的。数值型变量的类型决定了变量所能存储的最大值和最小值,同时也决定了是否允许在小数点后出现数字。
int(即integer的简写)型变量可以存储整数,如0、1、392或者-2553。但是,整数的取值范围(➤7.1节)是受限制的。最大的整数通常是2147483647,但在某些计算机上也可能只有32767。
float(即floating-point的简写)型变量可以存储比int型变量大得多的数值。而且,float型变量可以存储带小数位的数,如379.125。但float型变量也有一些缺陷。进行算术运算时float型变量通常比int型变量慢;更重要的是,float型变量所存储的数值往往只是实际数值的一个近似值。如果在一个float型变量中存储0.1,以后可能会发现变量的值为0.09999999999999987,这是舍入造成的误差。
2.4.2 声明
在使用变量之前必须对其进行声明(为编译器所做的描述)。为了声明变量,首先要指定变量的类型,然后说明变量的名字。(程序员决定变量的名字,命名规则见2.7节。)例如,我们可能这样声明变量height和profit:
int height; float profit;
第一条声明说明height是一个int型变量,这也就意味着变量height可以存储一个整数值。第二条声明则表示profit是一个float型变量。
如果几个变量具有相同的类型,就可以把它们的声明合并:
int height, length, width, volume; float profit, loss;
注意每一条完整的声明语句都要以分号结尾。
在main函数的第一个模板中并没有包含声明。当main函数包含声明时,必须把声明放置在语句之前:
int main(void) { 声明 语句 }
第9章我们将会看到,函数和程序块(包含嵌入声明的语句,➤10.3节)一般都有这样的要求。就书写格式而言,建议在声明和语句之间留出一个空行。
在C99中,声明可以不在语句之前。例如,main函数中可以先有一个声明,后面跟一条语句,然后再跟一个声明。为了与以前的编译器兼容,本书中的程序不会采用这一规则。但是,考虑到C++和Java程序中在使用时才声明变量的情况很常见,估计将来在C99程序中这种做法也会很流行。
2.4.3 赋值
变量通过赋值(assignment)的方式获得值。例如,语句
height = 8; length = 12; width = 10;
把数值8、12和10分别赋给变量height、length和width,8、12和10称为常量(constant)。
变量在赋值或以其他方式使用之前必须先声明。也就是说,我们可以这样写:
int height; height = 8;
但下面这样是不行的:
height = 8; /*** WRONG ***/ int height;
赋给float型变量的常量通常都带小数点。例如,如果profit是一个float型的变量,可能会这样对其赋值:
profit = 2150.48;
当我们把一个包含小数点的常量赋值给float型变量时,最好在该常量后面加一个字母f(代表float):
profit = 2150.48f;
不加f可能会引发编译器的警告。
正常情况下,要将int型的值赋给int型的变量,将float型的值赋给float型的变量。混合类型赋值(如把int型的值赋给float型变量或者把float型的值赋给int型变量)是可以的,但不一定安全,见4.2节。
一旦变量被赋值,就可以用它来辅助计算其他变量的值:
height = 8; length = 12; width = 10; volume = height * length * width; /* volume is now 960 */
在C语言中,符号*表示乘法运算,因此上述语句把存储在height、length和width这3个变量中的数值相乘,然后把运算结果赋值给变量volume。通常情况下,赋值运算的右侧可以是一个含有常量、变量和运算符的公式(在C语言的术语中称为表达式)。
2.4.4 显示变量的值
用printf可以显示出变量的当前值。以
Height: h
为例,这里的表示变量height的当前值。我们可以通过如下的printf调用来实现输出上述信息的要求:
printf("Height: %d\n", height);
占位符%d用来指明在显示过程中变量height的值的显示位置。注意,由于在%d后面放置了\n,所以printf在显示完height的值后会跳到下一行。
%d仅用于int型变量。如果要显示float型变量,需要用%f来代替%d。默认情况下,%f会显示出小数点后6位数字。如果要强制%f显示小数点后位数字,可以把放置在%和f之间。例如,为了显示信息
Profit: $2150.48
可以把printf写为如下形式:
printf("Profit: $%.2f\n", profit);
C语言没有限制调用一次printf可以显示的变量的数量。为了同时显示变量height和变量length的值,可以使用下面的printf调用语句:
printf("Height: %d Length: %d\n", height, length);
程序 计算箱子的空间重量
运输公司特别不喜欢又大又轻的箱子,因为箱子在卡车或飞机上运输时要占据宝贵的空间。事实上,对于这类箱子,公司常常要求按照箱子的体积而不是重量来支付额外的费用。在美国,通常的做法是把体积除以166(这是每磅允许的立方英寸数)。如果除得的商(也就是箱子的“空间”重量或“体积”重量)大于箱子的实际重量,那么运费就按照空间重量来计算。(除数166是针对国际运输的,计算国内运输的空间重量时通常用194代替。)
假设运输公司雇你来编写一个计算箱子空间重量的程序。因为刚刚开始学习C语言,所以你决定先编写一个计算特定箱子空间重量的程序来试试身手,其中箱子的长、宽、高分别是12英寸、10英寸和8英寸。C语言中除法运算用符号/表示。所以,很显然计算箱子空间重量的公式如下:
weight = volume / 166;
这里的weight和volume都是整型变量,分别用来表示箱子的重量和体积。但是上面这个公式并不是我们所需要的。在C语言中,如果两个整数相除,那么结果会被“截短”:小数点后的所有数字都会丢失。12英寸×10英寸×8英寸的箱子体积是960立方英寸,960除以166的结果是5而不是5.783,这样使得重量向下取整;而运输公司则希望结果向上取整。一种解决方案是在除以166之前把体积数加上165:
weight = (volume + 165) / 166;
这样,体积为166的箱子重量就为331/166,取整为1;而体积为167的箱子重量则为332/166,取整为2。下面给出了利用这种方法编写的计算空间重量的程序。
dweight.c
/* Computes the dimensional weight of a 12" x 10" x 8" box */ #include <stdio.h> int main(void) { int height, length, width, volume, weight; height = 8; length = 12; width = 10; volume = height * length * width; weight = (volume + 165) / 166; printf("Dimensions: %dx%dx%d\n", length, width, height); printf("Volume (cubic inches): %d\n", volume); printf("Dimensional weight (pounds): %d\n", weight); return 0; }
这段程序的输出结果是:
Dimensions: 12x10x8 Volume (cubic inches): 960 Dimensional weight (pounds): 6
2.4.5 初始化
当程序开始执行时,某些变量会被自动设置为零,而大多数变量则不会(➤18.5节)。没有默认值并且尚未在程序中被赋值的变量是未初始化的(uninitialized)。
如果试图访问未初始化的变量(例如,用printf显示变量的值,或者在表达式中使用该变量),可能会得到不可预知的结果,如2568、-30891或者其他同样没有意义的数值。在某些编译器中,可能会发生更坏的情况(甚至是程序崩溃)。
我们当然可以总是采用赋值的方法给变量赋初始值,但还有更简便的方法:在变量声明中加入初始值。例如,可以在一步操作中声明变量height并同时对其进行初始化:
int height = 8;
按照C语言的术语,数值8是一个初始化式(initializer)。
在同一个声明中可以对任意数量的变量进行初始化:
int height = 8, length = 12, width = 10;
注意,上述每个变量都有属于自己的初始化式。在接下来的例子中,只有变量width拥有初始化式10,而变量height和变量length都没有(也就是说这两个变量仍然未初始化):
int height, length, width = 10;
2.4.6 显示表达式的值
printf的功能不局限于显示变量中存储的数,它可以显示任意数值表达式的值。利用这一特性既可以简化程序,又可以减少变量的数量。例如,语句
volume = height * length * width; printf("%d\n", volume);
可以用以下形式代替:
printf("%d\n", height * length * width);
printf显示表达式的能力说明了C语言的一个通用原则:在任何需要数值的地方,都可以使用具有相同类型的表达式。
2.5 读入输入
程序dweight.c并不十分有用,因为它仅可以计算出一个箱子的空间重量。为了改进程序,需要允许用户自行录入尺寸。
为了获取输入,就要用到scanf函数。它是C函数库中与printf相对应的函数。scanf中的字母f和printf中的字母f含义相同,都是表示“格式化”的意思。scanf函数和printf函数都需要使用格式串(format string)来指定输入或输出数据的形式。scanf函数需要知道将获得的输入数据的格式,而printf函数需要知道输出数据的显示格式。
为了读入一个int型值,可以使用下面的scanf函数调用:
scanf("%d", &i); /* reads an integer; stores into i */
其中,字符串"%d"说明scanf读入的是一个整数,而i是一个int型变量,用来存储scanf读入的输入。&运算符(➤11.2节)在这里很难解释清楚,因此现在只说明它在使用scanf函数时通常是(但不总是)必需的。
读入一个float型值时,需要一个形式略有不同的scanf调用:
scanf("%f", &x); /* reads a float value; stores into x */
%f只用于float型变量,因此这里假设x是一个float型变量。字符串"%f"告诉scanf函数去寻找一个float格式的输入值(此数可以含有小数点,但不是必须含有)。
程序 计算箱子的空间重量(改进版)
下面是计算空间重量程序的一个改进版。在这个改进的程序中,用户可以录入尺寸。注意,每一个scanf函数调用都紧跟在一个printf函数调用的后面。这样做可以提示用户何时输入,以及输入什么。
dweight2.c
/* Computes the dimensional weight of a box from input provided by the user */ #include <stdio.h> int main(void) { int height, length, width, volume, weight; printf("Enter height of box: "); scanf("%d", &height); printf("Enter length of box: "); scanf("%d", &length); printf("Enter width of box: "); scanf("%d", &width); volume = height * length * width; weight = (volume + 165) / 166; printf("Volume (cubic inches): %d\n", volume); printf("Dimensional weight (pounds): %d\n", weight); return 0; }
这段程序的输出显示如下(用户的输入用下划线标注):
Enter height of box: 8 Enter length of box: 12 Enter width of box: 10 Volume (cubic inches): 960 Dimensional weight (pounds): 6
提示用户输入的消息(提示符)通常不应该以换行符结束,因为我们希望用户在同一行输入。这样,当用户敲回车键时,光标会自动移动到下一行,因此就不需要程序通过显示换行符来终止当前行了。
dweight2.c程序还存在一个问题:如果用户输入的不是数值,程序就会出问题。3.2节会更详细地讨论这个问题。
2.6 定义常量的名字
当程序含有常量时,建议给这些常量命名。程序dweight.c和程序dweight2.c都用到了常量166。在后期阅读程序时也许有些人会不明白这个常量的含义。所以可以采用称为宏定义(macro definition)的特性给常量命名:
#define INCHES_PER_POUND 166
这里的#define是预处理指令,类似于前面所讲的#include,因而在此行的结尾也没有分号。
当对程序进行编译时,预处理器会把每一个宏替换为其表示的值。例如,语句
weight = (volume + INCHES_PER_POUND - 1) / INCHES_PER_POUND;
将变为
weight = (volume + 166 - 1) / 166;
效果就如同在前一个地方写的是后一条语句。
此外,还可以利用宏来定义表达式:
#define RECIPROCAL_OF_PI (1.0f / 3.14159f)
当宏包含运算符时,必须用括号(➤14.3节)把表达式括起来。
注意,宏的名字只用了大写字母。这是大多数C程序员遵循的规范,但并不是C语言本身的要求。(至今,C程序员沿用此规范已经几十年了,希望读者不要打破此规范。)
程序 华氏温度转换为摄氏温度
下面的程序提示用户输入一个华氏温度,然后输出一个对应的摄氏温度。此程序的输出格式如下(跟前面的例子一样,用户的输入信息用下划线标注出来):
Enter Fahrenheit temperature: 212
Celsius equivalent: 100.0
这段程序允许温度值不是整数,这也是摄氏温度显示为100.0而不是100的原因。首先来阅读一下整个程序,随后再讨论程序是如何构成的。
celsius.c
/* Converts a Fahrenheit temperature to Celsius */ #include <stdio.h> #define FREEZING_PT 32.0f #define SCALE_FACTOR (5.0f / 9.0f) int main(void) { float fahrenheit, celsius; printf("Enter Fahrenheit temperature: "); scanf("%f", &fahrenheit); celsius = (fahrenheit - FREEZING_PT) * SCALE_FACTOR; printf("Celsius equivalent: %.1f\n", celsius); return 0; }
语句
celsius = (fahrenheit - FREEZING_PT) * SCALE_FACTOR;
把华氏温度转换为相应的摄氏温度。因为FREEZING_PT表示的是常量32.0f,而SCALE_FACTOR表示的是表达式(5.0f / 9.0f),所以编译器会把这条语句看成是
celsius = (fahrenheit - 32.0f) * (5.0f / 9.0f);
在定义SCALE_FACTOR时,表达式采用(5.0f / 9.0f)的形式而不是(5 / 9)的形式,这一点非常重要,因为如果两个整数相除,那么C语言会对结果向下取整。表达式(5 / 9)的值将为0,这并不是我们想要的。
最后的printf函数调用输出相应的摄氏温度:
printf("Celsius equivalent: %.1f\n", celsius);
注意,使用%.1f显示celsius的值时,小数点后只显示一位数字。
2.7 标识符
在编写程序时,需要对变量、函数、宏和其他实体进行命名。这些名字称为标识符(identifier)。在C语言中,标识符可以含有字母、数字和下划线,但是必须以字母或者下划线开头。(在C99中,标识符还可以使用某些“通用字符名”,25.4节。)
下面是合法标识符的一些示例:
times10 get_next_char _done
接下来这些则是不合法的标识符:
10times get-next-char
不合法的原因是:符号10times是以数字而不是以字母或下划线开头的;符号get-next-char包含了减号,而不是下划线。
C语言是区分大小写的;也就是说,在标识符中C语言区别大写字母和小写字母。例如,下列标识符全是不同的:
job joB jOb jOB Job JoB JOb JOB
上述8个标识符可以同时使用,且每一个都有完全不同的意义。(看起来使人困惑!)除非标识符之间存在某种关联,否则明智的程序员会尽量使标识符看起来各不相同。
因为C语言是区分大小写的,许多程序员都会遵循在标识符中只使用小写字母的规范(宏命名除外)。为了使名字清晰,必要时还会插入下划线:
symbol_table current_page name_and_address
而另外一些程序员则避免使用下划线,他们的方法是把标识符中的每个单词用大写字母开头:
symbolTable currentPage nameAndAddress
(第一个字母有时候也用大写。)前一种风格在传统C中很常见,但现在后面的风格更流行一些,这主要归功于它在Java和C#(以及C++)中的广泛使用。当然还存在其他一些合理的规范,只要保证整个程序中对同一标识符按照同一种方式使用大写字母就行。
C对标识符的最大长度没有限制,所以不用担心使用较长的描述性名字。诸如current_page这样的名字比cp之类的名字更容易理解。
关键字
表2-1中的所有关键字(keyword)对C编译器而言都有着特殊的意义,因此这些关键字不能作为标识符来使用。注意,其中有5个关键字是C99新增的。
表2-1 关键字
①仅C99有。
因为C语言是区分大小写的,所以程序中出现的关键字必须严格按照表2-1所示的格式全部采用小写字母。(C99关键字_Bool、_Complex和_Imaginary例外。)标准库中函数(如printf)的名字也只能包含小写字母。某些可怜的程序员用大写字母录入了整个程序,结果却发现编译器不能识别关键字和库函数的调用。应该避免这类情况发生。
请注意有关标识符的其他限制。某些编译器把特定的标识符(如asm)视为附加关键字。属于标准库的标识符也是受限的(➤21.1节)。误用这些名字可能会导致编译或链接出错。以下划线开头的标识符也是受限的。
2.8 C程序的书写规范
我们可以把C程序看成是一连串记号(token),即许多在不改变意思的基础上无法再分割的字符组。标识符和关键字都是记号。像+和-这样的运算符、逗号和分号这样的标点符号以及字符串字面量,也都是记号。例如,语句
printf( "Height: %d\n", height);
是由7个记号组成的:
printf ( "Height: %d\n" , height ) ; ① ② ③ ④ ⑤ ⑥ ⑦
其中记号①和⑤记号都是标识符,记号③是字符串字面量,而记号②、记号④、记号⑥和记号⑦则是标点符号。
大多数情况下,程序中记号之间的空格数量没有严格要求。除非两个记号合并后会产生第三个记号,否则在一般情况下记号之间根本不需要留有间隔。例如,可以删除2.6节的程序celsius.c中的大多数间隔,而只保留诸如int和main之间以及float和fahrenheit之间的空格。
/* Converts a Fahrenheit temperature to Celsius */ #include <stdio.h> #define FREEZING_PT 32.0f #define SCALE_FACTOR (5.0f/9.0f) int main(void){float fahrenheit,celsius;printf( "Enter Fahrenheit temperature:");scanf("%f", &fahrenheit); celsius=(fahrenheit-FREEZING_PT)*SCALE_FACTOR; printf("Celsius equivalent: %.1f\n", celsius);return 0;}
事实上,如果这个页面再宽一些,可以将整个main函数都放在一行中。但是,不能把整个程序写在一行内,因为每条预处理指令都要求独立成行。
当然,用这种方式压缩程序并不是个好主意。事实上,添加足够的空格和空行可以使程序更便于阅读和理解。幸运的是,C语言允许在记号之间插入任意数量的间隔,这些间隔可以是空格符、制表符和换行符。这一规则对于程序布局有如下积极意义。
·语句可以分开放在任意多行内。例如,下面的语句非常长,很难将它压缩在一行内:
printf("Dimensional weight (pounds): %d\n", (volume + INCHES_PER_POUND - 1) / INCHES_PER_POUND);
·记号间的空格使我们更容易区分记号。基于这个原因,我通常会在每个运算符的前后都放上一个空格:
volume = height * length * width;
此外,我还会在每个逗号后边放一个空格。某些程序员甚至在圆括号和其他标点符号的两边都加上空格。
·缩进有助于轻松识别程序嵌套。例如,为了清晰地表示出声明和语句都嵌套在main函数中,应该对它们进行缩进。
·空行可以把程序划分成逻辑单元,从而使读者更容易辨别程序的结构。就像没有章节的书一样,没有空行的程序很难阅读。
2.6节中的程序celsius.c体现了上面提到的几种布局方法。我们来仔细阅读一下这个程序中的main函数:
int main(void) { float fahrenheit, celsius; printf("Enter Fahrenheit temperature: "); scanf("%f", &fahrenheit); celsius = (fahrenheit - FREEZING_PT) * SCALE_FACTOR; printf("Celsius equivalent: %.1f\n", celsius); return 0; }
首先,观察一下运算符=、-和*两侧的空格是如何使这些运算符凸现出来的;其次,留心为了明确声明和语句属于main函数,如何对它们采取缩进格式;最后,注意如何利用空行将main划分为5部分:(1)声明变量fahrenheit和celsius,(2)获取华氏温度,(3)计算变量celsius的值,(4)显示摄氏温度,(5)返回操作系统。
在讨论程序布局问题的同时,还要注意一下记号{和记号}的放置方法:记号{放在了main()的下面,而与之匹配的记号}则放在了独立的一行中,并且与记号{排在同一列上。把记号}独立放在一行中可以便于在函数的末尾插入或删除语句,而将记号}与记号{排在一列上是为了便于找到main函数的结尾。
最后要注意的是:虽然可以在记号之间添加额外的空格,但是绝不能在记号内添加空格,因为这样做可能会改变程序的意思或者引发错误。如果写成
fl oat fahrenheit, celsius; /*** WRONG ***/
或
fl oat fahrenheit, celsius; /*** WRONG ***/
在程序编译时会报错。尽管把空格加在字符串字面量中会改变字符串的意思,但这样做是允许的。然而,把换行符加进字符串中(换句话说,就是把字符串分裂成两行)却是非法的:
printf("To C, or not to C: that is the question.\n"); /*** WRONG ***/
把字符串从一行延续到下一行(➤13.1节)需要一种特殊的方法才可以实现。这种方法将在稍后的章节中学到。
问与答
问:GCC是什么的简称?(p.8)
答:GCC最初是GNU C compiler的简称。现在指GNU Compiler Collection,这是因为最新版本的GCC能够编译用Ada、C、C++、Fortran、Java和Objective-C等多种语言编写的程序。
问:明白了,但GNU又是什么意思呢?
答:GNU指的是“GNU's Not UNIX!”(发音为guh-NEW),它是自由软件基金会(Free Software Foundation)的一个项目。自由软件基金会是由Richard M. Stallman发起的一个组织,旨在抗议对UNIX软件授权的各种限制。从它的网站可以看出,自由软件基金会认为用户应该可以自由地“运行、复制、发布、研究、改变和改进”软件。GNU项目从头开始重写了许多传统的UNIX软件,并使公众能够免费地获得。
GCC和其他GNU软件对于Linux操作系统来说是至关重要的。Linux本身只是操作系统的“内核”(处理程序调度和基本输入/输出服务的部分),为了获得具体完整功能的操作系统,GNU软件是必要的。
网站www.gnu.org提供了更多有关GNU项目的信息。
问:GCC有什么过人之处呢?
答:我们说GCC重要,不仅仅是因为它能免费获取、能编译很多语言。GCC还可以在许多操作系统下运行,并为多种不同的CPU生成代码(支持所有广为使用的操作系统和CPU)。GCC是许多基于UNIX的操作系统(包括Linux、BSD和Mac OS X)的主要编译器,并广泛用于商业软件开发。有关GCC的更多信息请参考gcc.gnu.org。
问:GCC发现程序中错误的能力如何?
答:GCC有多个命令行选项来控制程序检查的彻底程度。使用这些选项可以帮助我们有效地找出程序中潜在的故障区域。下面是一些比较常用的选项。
这些选项常常可以结合使用:
% gcc -O -Wall -W -pedantic -ansi -std=c99 -o pun pun.c
问:为什么C语言如此简明扼要?如果在C语言中用begin和end代替{和},用integer代替int,如此等等,程序似乎更加易读。(p.9)
答:据说,C程序的简洁性是由开发该语言时贝尔实验室的环境造成的。第一个C语言编译器是运行在DEC PDP-11计算机(一种早期的小型计算机)上的,而程序员用电传打字机(实际上是一种与计算机相连的打字机)录入程序和打印列表。由于电传打字机的速度非常慢(每秒钟只能打出10个字符),所以在程序中尽量减少字符数量显然是十分有利的。
问:在某些C语言书中,main函数的结尾使用的是exit(0)而不是return 0,二者是否一样呢?(p.10)
答:当出现在main函数中时,这两种语句是完全等价的:二者都终止程序执行,并且向操作系统返回0值。使用哪种语句完全依据个人喜好而定。
问:如果main函数末尾没有return语句会产生什么后果?(p.10)
答:return语句不是必需的;如果没有return语句,程序一样会终止。在C89中,返回给操作系统的值是未定义的。在C99中,如果main函数声明中的返回类型是int(如我们的例子所示),程序会向操作系统返回0;否则程序会返回一个不确定的值。
问:编译器是完全移除注释还是用空格替换掉注释呢?
答:一些早期的编译器会删除每条注释中的所有字符,使得语句
a/**/b = 0;
可能被编译器理解成
ab = 0;
然而,依据C标准,编译器必须用一个空格字符替换每条注释语句,因此上面提到的技巧并不可行。我们实际上会得到下面的语句:
a b = 0;
问:如何发现程序有没有未终止的注释?
答:如果运气好的话,程序将无法通过编译,因为这样的注释会导致程序非法。如果程序可以通过编译,也有几种方法可以用。通过用调试器逐行地执行程序,就会发现是否有些行被跳过了。某些集成开发环境会使用特别的颜色把注释和其他代码区分开来。如果你使用的是这样的开发环境,就会很容易发现未终止的注释,因为误把程序文本包含到注释中会导致颜色不同。此外,诸如lint(➤1.2节)之类的程序也可以提供帮助。
问:在一个注释中嵌套另一个注释是否合法?
答:传统风格的注释(/*...*/)不允许嵌套。例如,下面的代码就是不合法的:
/* /*** WRONG ***/ */
第2行的符号*/会和第一行的/*相匹配,所以编译器将会把第3行的*/标记为一个错误。
C语言禁止注释嵌套有些时候也是个问题。假设我们编写了一个很长的程序,其中包含了许多短小的注释。为了临时屏蔽程序的某些部分(比如在测试过程中),我们首先会想到用/*和*/“注释掉”相应的程序行。但是,如果这些代码行中包含有传统风格的注释,这种方法就行不通了。不过,C99注释(以//开始的注释)可以嵌套在传统风格的注释中,这是这类注释的另一个优势。
后面我们将看到,可以用一种更好的方法来屏蔽部分程序(➤14.4节)。
问:float类型的名字由何而来?(p.12)
答:float是floating-point的缩写形式,它是一种存储数的方法,而这些数中的小数点是“浮动的”。float类型的值通常分成两部分存储:小数部分(或者称为尾数部分)和指数部分。例如,12.0这个数可以以的形式存储,其中1.5是小数部分,而3是指数部分。有些编程语言把这种类型称为real类型而不是float类型。
问:为什么浮点常量需要以字母f结尾?(p.14)
答:完整的解释见第7章。这里只简单回答一下:包含小数点但却不以f结尾的常量是double(double precision的缩写)型的。double型的值比float型的值存储得更精确,并且可以存储比float型更大的值,因此在给float型变量赋值时需要加上字母f。如果不加f,编译器可能会生成一条警告消息,告诉你存储到float型变量中的数可能超出了该变量的取值范围。
*问:对标识符的长度真的没有限制吗?(p.19)
答:是,又不是。C89标准声称标识符可以任意长,但却只要求编译器记住前31个字符(C99中是63个字符)。因此,如果两个名字的前31个字符都相同,编译器可能会无法区别它们。
更复杂的情况是,C标准对于具有外部链接(➤18.2节)的标识符有特殊的规定,而大多数函数名都属于这类标识符。因为链接器必须能识别这些名字,而一些早期的链接器又只能处理短名字,所以在C89中只有前6个字符才是有效的。此外,还不区分字母的大小写。因此ABCDEFG和abcdefg可能会被作为相同的名字处理。(C99中,前31个字符有效,且字母区分大小写。)
大多数编译器和链接器都比标准所要求的宽松,所以实际使用中这些规则都不是问题。不要担心标识符太长,还是注意不要把它们定义得太短吧。
问:缩进时应该使用多少空格?(p.20)
答:这是个难以回答的问题。如果预留的空间过少,会不易察觉到缩进;如果预留的太多,则可能会导致行宽超出屏幕(或页面)的宽度。许多C程序员采用8个空格(即一个制表键)来缩进嵌套语句,这可能太多了。研究表明,缩进3个空格是最合适的,但许多程序员不太习惯于非2的幂次。我通常习惯于缩进3或4个空格,但是考虑到页面的需要,本书采用了2个空格的缩进方式。
练习题注1
注1符号标出的习题在网站knking.com/books/c2上有答案。以后各章也使用这一约定。
2.1节
1. 建立并运行由Kernighan和Ritchie编写的著名的“hello, world”程序:
#include <stdio.h> int main (void) { printf("hello, world\n"); }
在编译时是否有警告信息?如果有,需要如何进行修改呢?
2.2节
2. 思考下面的程序:
#include <stdio.h> int main(void) { printf("Parkinson's Law:\nWork expands so as to "); printf("fill the time\n"); printf("available for its completion.\n"); return 0; }
(a) 请指出程序中的指令和语句。
(b) 程序的输出是什么?
2.4节
3. 通过下列方法缩写程序dweight.c:(1)用初始化式替换对变量height、length和width的赋值;(2)去掉变量weight,在最后的printf语句中计算(volume + 165)/ 166。
4. 编写一个程序来声明几个int型和float型变量,不对这些变量进行初始化,然后显示它们的值。这些值是否有规律?(通常情况下没有。)
2.7节
5. 判断下列C语言标识符哪些是不合法的?
(a) 100_bottles
(b) _100_bottles
(c) one__hundred__bottles
(d) bottles_by_the_hundred_
6. 为什么说在标识符中使用多个相邻的下划线(如current___balance)不太合适?
7. 判断下列哪些是C语言的关键字?
(a) for
(b) If
(c) main
(d) printf
(e) while
2.8节
8. 下面的语句中有多少记号?
answer=(3*q-p*p)/3;
9. 在练习题8的记号之间插入空格,使该语句更易于阅读。
10. 在dweight.c程序(➤2.4节)中,哪些空格是必不可少的?
编程题
1. 编写一个程序,使用printf在屏幕上显示下面的图形:
2. 编写一个计算球体体积的程序,其中球体半径为10 m,参考公式。注意,分数4/3应写为4.0f/3.0f。(如果分数写成4/3会产生什么结果?)提示:C语言没有指数运算符,所以需要对自乘两次来计算。
3. 修改上题中的程序,使用户可以自行录入球体的半径。
4. 编写一个程序,要求用户输入一个美元数量,然后显示出增加5%税率后的相应金额。格式如下所示:
Enter an amount: 100.00
With tax added: $105.00
5. 编程要求用户输入的值,然后显示如下多项式的值:
提示:C语言没有指数运算符,所以需要对进行自乘来计算其幂。(例如,x*x*x就是x的三次方。)
6. 修改上题,用如下公式对多项式求值:
注意,修改后的程序所需的乘法次数减少了。这种多项式求值方法即Horner法则(Horner's Rule)。
7. 编写一个程序,要求用户输入一个美金数量,然后显示出如何用最少的20美元、10美元、5美元和1美元来付款:
Enter a dollar amount: 93
$20 bills: 4
$10 bills: 1
$5 bills: 0
$1 bills: 3
提示:将付款金额除以20,确定20美元的数量,然后从付款金额中减去20美元的总金额。对其他面值的钞票重复这一操作。确保在程序中始终使用整数值,不要用浮点数。
8. 编程计算第一、第二、第三个月还贷后剩余的贷款金额:
Enter amount of loan: 20000.00 Enter interest rate: 6.0 Enter monthly payment: 386.66 Balance remaining after first payment: $19713.34 Balance remaining after second payment: $19425.25 Balance remaining after third payment: $19135.71
在显示每次还款后的余额时保留两位小数。提示:每个月的贷款余额减去还款金额后,还需要加上贷款余额与月利率的乘积。月利率的计算方法是把用户输入的利率转换成百分数再除以12。