C 语言基本概念
本专题介绍 C 语言的一些基本概念,包括预处理指令、函数、变量和语句。
参考资料:《C 语言程序设计 · 现代方法 第 2 2 2 版》
1. 编写一个简单的 C 程序
程序
pun.c
:显示双关语
输出一条双关语:To C, or not to C: that is the question.
#include <stdio.h>
int main(void)
{
printf("To C, or not to C: that is the question.\n");
return 0;
}
#include <stdio.h>
是必不可少的,它 “包含” 了 C 语言标准输入 / 输出库的相关信息。程序的可执行代码都在 main
函数中,这个函数代表 “主” 程序。main
函数中的第一行代码是用来显示期望信息的。printf
函数来自标准输入 / 输出库,可以产生完美的格式化输出。代码 \n
告诉 printf
函数执行完消息显示后要进行换行操作。第二行代码 return 0;
表明程序终止时会向操作系统返回值 0
。
尽管 pun.c
程序十分简短,但是为运行这个程序而包含的内容可能比想象的要多。首先,需要生成一个含有上述程序代码名为 pun.c
的文件(使用任何文本编辑器都可以创建该文件)。文件的名字无关紧要,但是编译器通常要求带上文件的扩展名 .c
。
接下来就需要把程序转化为机器可以执行的形式。对于 C 程序来说,通常包含下列 3 3 3 个步骤:
- 预处理。首先程序会被送交给预处理器(
preprocessor
\text{preprocessor}
preprocessor)。预处理器执行以
#
开头的命令(通常称为指令)。预处理器有点类似于编辑器,它可以给程序添加内容,也可以对程序进行修改。 - 编译。修改后的程序现在可以进入编译器( compiler \text{compiler} compiler)了。编译器会把程序翻译成机器指令(即目标代码)。
- 链接。链接器( linker \text{linker} linker)把由编译器产生的目标代码和所需的其他附加代码(比如程序中用到的库函数)整合在一起,这样才最终产生了完全可执行的程序。
2. 简单程序的一般形式
简单的 C 程序一般具有如下形式:
指令
int main(void)
{
语句
}
- 指令:在编译前修改程序的编辑命令
- 函数:被命名的可执行代码块,如
main
函数 - 语句:程序运行时执行的命令
2.1 指令
在编译 C 程序之前,预处理器会首先对其进行编辑。我们把预处理器执行的命令称为指令。
程序 pun.c
由指令 #include <stdio.h>
开始。这条指令说明,在编译前把 <stdio.h>
中的信息 “包含” 到程序中。<stdio.h>
包含了关于 C 标准输入 / 输出库的信息。C 语言拥有大量类似于 <stdio.h>
的头(
header
\text{header}
header),每个头都包含一些标准库的内容。这段程序中包含 <stdio.h>
的原因是:C 语言没有内置的 “读” 和 “写” 命令,输入 / 输出功能由标准库中的函数实现。
所有指令都是以字符 #
开始的。指令默认只占一行,每条指令的结尾没有分号或其他特殊标记。
2.2 函数
函数是用来构建程序的构建块。函数分为两大类:一类是程序员编写的函数,另一类则是作为 C 语言实现的一部分提供的函数,称为库函数( library function \text{library function} library function)。
在 C 语言中,函数是一系列组合在一起并且赋予了名字的语句。某些函数计算数值,某些函数不这么做。计算数值的函数用 return
语句来指定所 “返回” 的值。例如,对参数进行加
1
1
1 操作的函数可以执行语句
return x + 1;
而当函数要计算参数的平方差时,则可以执行语句
return y * y - z * z;
虽然一个 C 程序可以包含多个函数,但只有 main
函数是必须有的。在执行程序时系统会自动调用 main
函数。main
函数的名字是至关重要的,绝对不能改写成其他任何形式。main
函数会在程序终止时向操作系统返回一个状态码。在 pun.c
程序中,main
前面的 int
表明该函数将返回一个整数值。圆括号中的 void
表明 main
函数没有参数。
语句 return 0;
有两个作用:一是使 main
函数终止,从而结束程序;二是指出 main
函数的返回值是 0
,这个值表明程序正常终止。如果 main
函数的末尾没有 return
语句,程序仍然能终止。但是,许多编译器会产生一条警告信息。
2.3 语句
语句是程序运行时执行的命令。程序 pun.c
只用到两种语句。一种是返回(return
)语句,另一种则是函数调用(
function call
\text{function call}
function call)语句。要求某个函数执行分派给它的任务称为调用这个函数。例如,程序 pun.c
为了在屏幕上显示一条字符串就调用了 printf
函数。
C 语言规定每条语句都要以分号结尾(复合语句除外)。由于语句可以连续占用多行,有时很难确定它的结束位置,因此用分号来向编译器显示语句的结束位置。但指令通常都只占一行,因此不需要用分号结尾。
2.4 显示字符串
printf
是一个功能强大的函数,目前我们只是用它显示了一条字符串字面量(
string literal
\text{string literal}
string literal)—— 用一对双引号包围的一系列字符。当用 printf
函数显示字符串字面量时,最外层的双引号不会出现。
当显示结束时,printf
函数不会自动跳转到下一输出行。为了让 printf
跳转到下一行,必须在要显示的字符串中包含 \n
(换行符)。换行符可以在一个字符串字面量中出现多次。为了显示下列信息:
Brevity is the soul of wit.
--Shakespeare
可以这样写:
printf("Brevity is the soul of wit.\n --Shakespeare");
3. 注释
pun.c
程序仍然缺乏某些重要内容:文档说明。每一个程序都应该包含识别信息,即程序名、编写日期、作者、程序的用途以及其他相关信息。C 语言把这类信息放在注释(
comment
\text{comment}
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
*/
简短的注释还可以与程序中的其他代码放在同一行,称作 “翼型注释”:
int main(void) /* Beginning of main program */
如果忘记终止注释可能会导致编译器忽略程序的一部分。C99 提供了以 //
开始的注释形式:
// This is a comment
这种风格的注释会在行末自动终止。如果要创建多于一行的注释,既可以采用以前的注释风格(/* ... */
),也可以在每一行的前面加上 //
:
// Name: pun.c
// Purpose: Prints a bad pun.
// Author: K. N. King
新的注释风格有两个主要优点:首先,因为注释会在行末自动终止,所以不会出现未终止的注释意外吞噬部分程序的情况;其次,因为每行前面都必须有 //
,所以多行注释更加醒目。
4. 变量和赋值
大多数程序在产生输出之前往往需要执行一系列的计算,因此需要在程序执行过程中有一种临时存储数据的方法。和大多数编程语言一样,C 语言中的这类存储单元被称为变量( variable \text{variable} variable)。
4.1 类型
每一个变量都必须有一个类型( type \text{type} type)。类型用来说明变量所存储的数据的种类,会影响变量的存储方式以及允许对变量进行的操作。数值型变量的类型决定了变量所能存储的最大值和最小值,同时也决定了是否允许在小数点后出现数字。
int
(即
integer
\text{integer}
integer 的简写)型变量可以存储整数,如
0
0
0、
1
1
1、
392
392
392 或者
−
2553
-2553
−2553。但是,整数的取值范围是受限的,最大的整数通常是
2147483647
2147483647
2147483647,但在某些计算机上也可能只有
32767
32767
32767。
float
(即
floating-point
\text{floating-point}
floating-point 的简写)型变量可以存储比 int
型变量大得多的数值。而且,float
型变量可以存储带小数位的数,如
379.125
379.125
379.125。但 float
型变量进行算术运算时通常比 int
型变量慢;更重要的是,float
型变量所存储的数值往往只是实际数值的一个近似值。如果在一个 float
型变量中存储
0.1
0.1
0.1,以后可能会发现变量的值为
0.099
999
999
999
999
87
0.099\,999\,999\,999\,999\,87
0.09999999999999987,这是舍入造成的误差。
4.2 声明
在使用变量之前必须对其进行声明(为编译器所做的描述)。为了声明变量,首先要指定变量的类型,然后说明变量的名字。例如,我们可能这样声明变量 height
和 profit
:
int height;
float profit;
第一条声明说明 height
是一个 int
型变量,这也就意味着变量 height
可以存储一个整数值。第二条声明则表示 profit
是一个 float
型变量。
如果几个变量具有相同的类型,就可以把它们的声明合并:
int height, length, width, volume;
float profit, loss;
当 main
函数包含声明时,必须把声明放置在语句之前:
int main(void)
{
声明
语句
}
在 C99 中,声明可以不在语句之前。例如,main
函数中可以先有一个声明,后面跟一条语句,然后再跟一个声明。
4.3 赋值
变量通过赋值( assignment \text{assignment} assignment)的方式获得值。例如,语句
height = 8;
length = 12;
width = 10;
把数值
8
8
8、
12
12
12 和
10
10
10 分别赋给变量 height
、length
和 width
,
8
8
8、
12
12
12 和
10
10
10 称为常量(
constant
\text{constant}
constant)。变量在赋值或以其他方式使用之前必须声明。
赋给 float
型变量的常量通常都带小数点。当我们把一个包含小数点的常量赋值给 float
型变量时,最好在该常量后面加一个字母 f
(代表
float
\text{float}
float),否则可能会引发编译器的警告:
profit = 2150.48f;
一旦变量被赋值,就可以用它来辅助计算其他变量的值:
height = 8;
length = 12;
width = 10;
volume = height * length * width; /* volume is now 960 */
在 C 语言中,符号 *
表示乘法运算,因此上述语句把存储在 height
、length
和 width
这
3
3
3 个变量中的数值相乘,然后把运算结果赋值给变量 volume
。通常情况下,赋值运算的右侧可以是一个含有常量、变量和运算符的公式(在 C 语言的术语中称为表达式)。
4.4 显示变量的值
用 printf
可以显示出变量的当前值:
printf("Height: %d\n", height);
占位符 %d
用来指明在显示过程中变量 height
的值的显示位置。%d
仅用于 int
型变量,如果要显示 float
型变量,需要用 %f
来代替 %d
。默认情况下,%f
会显示出小数点后
6
6
6 位数字。如果要强制 %f
显示小数点后
p
p
p 位数字,可以把 .p
放置在 %
和 f
之间:
printf("Profit: $%.2f\n", profit);
C 语言没有限制调用一次 printf
可以显示的变量的数量。为了同时显示变量 height
和变量 length
的值,可以使用下面的 printf
调用语句:
printf("Height: %d Length: %d\n", height, length);
程序
dweight.c
:计算箱子的空间重量
对于又大又轻的箱子,运输公司常常要求按照箱子的体积来支付额外费用。通常做法是把体积除以 166 166 166(每磅允许的立方英寸数),如果除得的商大于箱子的实际重量,那么运费就按照空间重量来计算。
我们先编写一个计算特定箱子空间重量的程序,其中箱子的长、宽、高分别是
12
12
12 英寸、
10
10
10 英寸、
8
8
8 英寸。C 语言中除法运算用符号 /
表示。所以,计算箱子空间重量的公式是 weight = volume / 166;
,这里的 weight
和 volume
都是整型变量,分别用来表示箱子的重量和体积。但是这个公式并不是我们所需要的。在 C 语言中,如果两个整数相除,那么结果会被 “截短”:小数点后的所有数字都会丢失。
12
12
12 英寸
×
10
\times 10
×10 英寸
×
8
\times 8
×8 英寸的箱子体积是
960
960
960 立方英寸,
960
960
960 除以
166
166
166 的结果是
5
5
5 而不是
5.783
5.783
5.783,这样使得重量向下取整,而运输公司则希望结果向上取整。一种解决方案是在除以
166
166
166 之前把体积数加上
165
165
165:weight = (volume + 165) / 166;
,这样,体积为
166
166
166 的箱子重量就为
331
/
166
331 / 166
331/166,取整为
1
1
1;而体积为
167
167
167 的箱子重量则为
332
/
166
332 / 166
332/166,取整为
2
2
2。下面给出了利用这种方法编写的计算空间重量的程序。
/* 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
4.5 初始化
当程序开始执行时,某些变量会被自动设置为零,而大多数变量则不会。没有默认值并且尚未在程序中被赋值的变量是未初始化的( uninitialized \text{uninitialized} uninitialized)。如果试图访问未初始化的变量,可能会得到不可预知的结果,比如没有意义的数值,在某些编译器中,甚至可能会导致程序崩溃。
我们可以采用赋值的方法给变量赋初始值,也可以在变量声明中加入初始值。例如,可以在一步操作中声明变量 height
并同时对其进行初始化:
int height = 8;
按照 C 语言的术语,数值 8 8 8 是一个初始化式( initializer \text{initializer} initializer)。
在同一个声明中可以对任意数量的变量进行初始化:
int height = 8, length = 12, width = 10;
上述每个变量都有属于自己的初始化式。而下面的代码中,只有变量 width
拥有初始化式
10
10
10,而变量 height
和 length
都没有,也就是说这两个变量仍然未初始化:
int height, length, width = 10;
4.6 显示表达式的值
printf
的功能不局限于显示变量中存储的数,它可以显示任意数值表达式的值。例如,语句
volume = height * length * width;
printf("%d\n", volume);
可以用以下形式代替:
printf("%d\n", height * length * width);
printf
显示表达式的能力说明了 C 语言的一个通用原则:在任何需要数值的地方,都可以使用具有相同类型的表达式。
5. 读入输入
程序 dweight.c
并不十分有用,因为它仅可以计算出一个箱子的空间重量。为了改进程序,需要允许用户自行录入尺寸。
为了获取输入,就要用到 scanf
函数。它是 C 函数库中与 printf
相对应的函数。scanf
中的字母 f
和 printf
中的字母 f
含义相同,都是表示 “格式化” 的意思。scanf
函数和 printf
函数都需要使用格式串(
format string
\text{format string}
format string)来指定输入或输出数据的形式。
为了读入一个 int
型值,可以使用下面的 scanf
函数调用:
scanf("%d", &i); /* reads an integer; stores into i */
其中,字符串 "%d"
说明 scanf
读入的是一个整数,而 i
是一个 int
型变量,用来存储 scanf
读入的输入。&
运算符先不解释,现在只说明它在使用 scanf
函数时通常是(但不总是)必需的。
读入一个 float
型值时,需要一个形式略有不同的 scanf
调用:
scanf("%f", &x); /* reads a float value; stores into x */
%f
只用于 float
型变量,因此这里假设 x
是一个 float
型变量。字符串 "%f"
告诉 scanf
函数去寻找一个 float
格式的输入值(此数可以含有小数点,但不是必须含有)。
程序
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
‾
\underline{8}
8
Enter length of box:
12
‾
\underline{12}
12
Enter width of box:
10
‾
\underline{10}
10
Volume (cubic inches): 960
Dimensional weight (pounds): 6
提示用户输入的消息(提示符)通常不应该以换行符结束,因为我们希望用户在同一行输入。这样,当用户敲回车键时,光标会自动移动到下一行,因此就不需要程序通过显示换行符来终止当前行了。
6. 定义常量的名字
程序 dweight.c
和 dweight2.c
都用到了常量
166
166
166,在后期阅读程序时也许有些人会不明白这个常量的含义,所以可以采用称为宏定义(
macro definition
\text{macro definition}
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)
当宏包含运算符时,必须用括号把表达式括起来。注意,宏的名字只用了大写字母,这是大多数 C 程序员遵循的规范,但并不是 C 语言本身的要求。
程序
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;
}
这段程序的输出显示如下(用户的输入用下划线标注):
Enter Fahrenheit temperature:
212
‾
\underline{212}
212
Celsius equivalent: 100.0
7. 标识符
在编写程序时,需要对变量、函数、宏和其他实体进行命名,这些名字称为标识符(
identifier
\text{identifier}
identifier)。在 C 语言中,标识符可以含有字母、数字和下划线,但是必须以字母或者下划线开头。例如,times10
、get_next_char
、_done
是合法标识符,而 10times
、get-next-char
是不合法的标识符。
C 语言是区分大小写的。例如,job
、joB
、jOb
是完全不同的标识符,可以同时使用,不过最好不要同时使用,因为不容易区分。
许多程序员都会遵循在标识符中只使用小写字母的规范(宏命名除外)。为了使名字清晰,必要时还会插入下划线:symbol_table
、current_page
、name_and_address
。而另外一些程序员则避免使用下划线,他们的方法是把标识符中的每个单词用大写字母开头(第一个字母有时候也大写):symbolTable
、currentPage
、nameAndAddress
。
C 语言对标识符的最大长度没有限制,但却只要求编译器记住前 31 31 31 个字符(C99 中是 63 63 63 个字符)。不过大多数编译器和链接器都比标准所要求的宽松,所以实际使用中标识符的长度不是问题。
关键字(
keyword
\text{keyword}
keyword)对 C 编译器而言都有着特殊的意义,因此关键字不能作为标识符来使用。下面列出了 C99 中全部
37
37
37 个关键字(C89 中是
32
32
32 个关键字,右上角标有星号表示 C99 新增关键字)。
auto
break
case
char
const
continue
default
do
double
else
enum
extern
float
for
goto
if
inline
∗
int
long
register
restrict
∗
return
short
signed
sizeof
static
struct
switch
typedef
union
unsigned
void
volatile
while
_
Bool
∗
_
Complex
∗
_
Imaginary
∗
\begin{matrix} \textsf{auto} & \textsf{break} & \textsf{case} & \textsf{char} & \textsf{const} & \textsf{continue} & \textsf{default} & \textsf{do} & \textsf{double} & \textsf{else} \\ \textsf{enum} & \textsf{extern} & \textsf{float} & \textsf{for} & \textsf{goto} & \textsf{if} & \textsf{inline}^* & \textsf{int} & \textsf{long} & \textsf{register} \\ \textsf{restrict}^* & \textsf{return} & \textsf{short} & \textsf{signed} & \textsf{sizeof} & \textsf{static} & \textsf{struct} & \textsf{switch} & \textsf{typedef} & \textsf{union} \\ \textsf{unsigned} & \textsf{void} & \textsf{volatile} & \textsf{while} & \_\textsf{Bool}^* & \_\textsf{Complex}^* & \_\textsf{Imaginary}^* \end{matrix}
autoenumrestrict∗unsignedbreakexternreturnvoidcasefloatshortvolatilecharforsignedwhileconstgotosizeof_Bool∗continueifstatic_Complex∗defaultinline∗struct_Imaginary∗dointswitchdoublelongtypedefelseregisterunion
8. C 程序的书写规范
我们可以把 C 程序看成是一连串记号(
token
\text{token}
token),即许多在不改变意思的基础上无法再分割的字符组。标识符和关键字都是记号,像 +
和 -
这样的运算符、逗号和分号这样的标点符号以及字符串字面量,也都是记号。
大多数情况下,程序中记号之间的空格数量没有严格要求。除非两个记号合并后会产生第三个记号,否则在一般情况下记号之间根本不需要留有间隔。事实上,添加足够的空格和空行可以使程序更便于阅读和理解。C 语言允许在记号之间插入任意数量的间隔,这些间隔可以是空格符、制表符和换行符。这一规则对于程序布局有如下积极意义:
- 语句可以分开放在任意多行内。
- 记号间的空格使我们更容易区分记号。
- 缩进有助于轻松识别程序嵌套。
- 空行可以把程序划分成逻辑单元,从而使读者更容易辨别程序的结构。
虽然可以在记号之间添加额外的空格,但是绝不能在记号内添加空格,否则可能会改变程序的意思或者引发错误。尽管把空格加在字符串字面量中会改变字符串的意思,但这样做是允许的。然而把换行符加进字符串中(换句话说,就是把字符串分裂成两行)却是非法的,把字符串从一行延续到下一行需要一种特殊的方法才可以实现。