Introduction …引入C语言的基本元素。…一些基本概念,比如变量与常量、算数运算、控制流、函数、基本输入/输出等。
1. hello, world 学习一门新程序设计语言的惟一途径就是使用它编写程序。对于所有语言的初学者来说,编写的第一个程序几乎都是相同的,即:
请打印出下列内容
hello, world
尽管这个练习很简单,但对于初学语言的人来说,它任然可能成为一大障碍,因为要实现这个目的,我们首先必须编写程序文本,然后成功地进行编译,并加载、运行,最后输出到某个地方。掌握了这些操作细节以后,其他事情就比较容易了。
hello.c 1 2 3 4 5 6 #include <stdio.h> main() { printf ("hello, world\n" ); }
如何运行这个程序 1 2 3 4 $ cc hello.c $ ./a.out hello, world $
一个C语言程序,无论其大小如何,都是由函数 和变量 组成的。函数中包含一些语句 ,以指定所要执行的计算操作;变量则用于存储计算过程中使用的值。…通常情况下,函数的命名没有限制,但main
是一个特殊的函数名——每个程序都从main
函数的起点开始执行,这意味着每个程序都必须在某个位置包含一个main
函数。
main
函数通常会调用其他函数来帮助完成某些工作,被调用的函数可以是程序设计人员自己编写的,也可以来自于函数库。
#include <stdio.h>
用于告诉编译器在本程序中包含标准输入/输出库的信息。
函数之间进行数据交换的一种方法是调用函数向被调用函数提供一个值(称为参数 )列表。函数名后面的一对圆括号将参数列表括起来。在本例中,main
函数不需要任何参数,因此用空参数表()
表示。
函数中的语句用一对花括号{}
括起来。
调用函数时,只需要使用函数名加上用圆括号括起来的参数表即可。
用双引号括起来的字符序列称为字符串 或字符串常量 。
在C语言中,字符序列\n
表示换行符 。…请注意,\n
只代表一个字符。类似于\n
的转义字符序列 为表示无法输入的字符或不可见字符提供了一种通用的可扩充的机制。
2. 变量与算术表达式 …一些新的概念,包括注释、声明、变量、算术表达式、循环以及格式化输出。
/* ... */
称为注释 ,它简单地解释了该程序是做什么用的。包含在/*
与*/
之间的字符序列将被编译器忽略。注释可以自由地运用在程序中,使得程序更容易理解。程序中允许出现空格、制表符或换行符之处,都可以使用注释。
在C语言中,所有变量都必须先声明后使用。声明通常放在函数起始处,在任何可执行语句之前。声明 用于说明变量的属性,它由一个类型名和一个变量表组成。…int
与float
类型的取值范围取决于具体的机器。…一些基本数据类型。另外,还存在这些基本数据类型的数组 、结构 、联合 ,指向这些类型的指针 以及返回这些类型值的函数 。
…赋值语句 ,它们为变量设置初值。各条语句均以分号结束。
…可以用循环语句重复…这是while
循环语句的用途。…测试圆括号中的条件;如果条件为真,则执行循环体(括在花括号中的语句);…当圆括号中的条件测试结果为假时,循环结束,并继续执行跟在while
循环语句之后的下一条语句。在本程序中,循环语句后没有其他语句,因此整个程序的执行终止。
while
语句的循环体可以是用花括号括起来的一条或多条语句,也可以是不用花括号包括的单条语句。在这两种情况下,我们总是把由while
控制的语句缩进一个制表位,这样就可以很容易地看出循环语句中包含哪些语句。这种缩进方式突出了程序的逻辑结构。尽管C编译器并不关心程序的外观形式,但正确的缩进以及保留适当空格的程序设计风格对程序的易读性非常重要。我们建议每行只书写一条语句,并在运算符两边各加上一个空格字符,这样就可以使得运算的结合关系更清楚明了。相比而言,花括号的位置就不那么重要了。我们从比较流行的一些风格中选择了一种。读者可以选择适合自己的一种风格,并养成一直使用这种风格的好习惯。
在C语言及许多其他语言中,整数除法操作将执行舍位 ,结果中的任何小数部分都会被舍弃。
printf
是一个通用输出格式化函数。…它们在数目和类型上都必须匹配,否则将出现错误的结果。
顺便指出,printf
函数并不是C语言本身的一部分,C语言本身并没有定义输入/输出功能。printf
仅仅是标准库函数中一个有用的函数而已,这些标准库函数在C语言程序中通常都可以使用。但是,ANSI
标准定义了printf
函数的行为,因此,对每个符合该标准的编译器和库来说,该函数的属性都是相同的。
如果某个算术运算符的所有操作数均为整型,则执行整型运算。但是,如果某个算术运算符有一个浮点型操作数和一个整型操作数,则在开始运算之前整型操作数将会被转换为浮点型。不过,即使浮点常量取的是整型值,在书写时最好还是为它加上一个显式的小数点,这样可以强调其浮点性质,便于阅读。
3. for
语句 对于某个特定任务我们可以采用多种方法来编写程序。
…表达式现在变成了printf
函数的第三个参数,它不再是一个单独的赋值语句。
…是C语言中一个通用规则的实例:在允许使用某种类型变量值的任何场合,都可以使用该类型的更复杂的表达式。
for
语句是一种循环语句,它是对while
语句的推广。如果将for
语句与前面介绍的while
语句比较,就会发现for
语句的操作更直观一些。圆括号中共包含3个部分,各部分之间用分号隔开。第一部分是初始化部分,仅在进入循环前执行一次。第二部分是控制循环的测试或条件部分。循环控制将对该条件求值,如果结果值为真(true
),则执行循环体。此后将执行第三部分以将循环变量fahr增加一个步长,并再次对条件求值。如果计算得到的条件值为假(false
),循环将终止执行。与while
语句一样,for
循环语句的循环体可以只有一条语句,也可以是用花括号括起来的一组语句。初始化部分(第一部分)、条件部分(第二部分)与增加步长部分(第三部分)都可以是任何表达式。
在实际编程过程中,可以选择while
与for
中的任意一种循环语句,主要要看使用哪一种更清晰。for
语句比较适合初始化和增加步长都是单条语句并且逻辑相关的情形,因为它将循环控制语句集中放在一起,且比while
语句更紧凑。
4. 符号常量 …“幻数”并不是一个好习惯,它们几乎无法向以后阅读该程序的人提供什么信息,而且使程序的修改变得更加困难。处理这种幻数的一种方法是赋予它们有意义的名字。#define
指令可以把符号名 (或称为符号常量 )定义为一个特定的字符串。在该定义之后,程序中出现的所有在#define
中定义的名字 (既没有用引号引起来,也不是其他名字的一部分)都将用相应的替换文本 替换。其中,名字与普通变量名的形式相同:它们都是以字母打头的字母和数字的序列;替换文本可以是任何字符序列,而不仅限于数字。
符号常量名通常用大写字母拼写,这样可以很容易与用小写字母拼写的变量名相区别。注意,#define
指令行的末尾没有分号。
5. 字符输入/输出 标准库提供的输入/输出模型非常简单。无论文本从何处输入,输出到何处,其输入/输出都是按照字符流的方式处理。文本流 是由多行字符构成的字符序列,而每行字符则由0各或多个字符组成,行末是一个换行符。标准库负责使每个输入/输出流都能够遵守这一模型。使用标准库的C语言程序员不必关心在程序之外这些行是如何表示的。
标准库提供了一次读/写一个字符的函数,其中最简单的是getchar
和putchar
两个函数。每次调用时,getchar
函数从文本流中读入下一个输入字符 ,并将其作为结果值返回。…这种字符通常是通过键盘输入的。每次调用putchar
函数时将打印一个字符。…通常是显示在屏幕上。putchar
与printf
这两个函数可以交替使用,输出的次序与调用的次序一致。
5.1. 文件复制 借助于getchar
与putchar
函数,可以在不了解其他输入/输出知识的情况下编写出数量惊人的有用的代码。最简单的例子就是把输入一次一个字符地复制到输出。…关系运算符!=
表示“不等于”。
字符在键盘、屏幕或其他的任何地方无论以什么形式表现,它在机器内部都是以位模式存储的。char
类型专门用于存储这种字符型数据,当然任何整型(int
)也可以用于存储字符型数据。因为某些潜在的重要原因,我们在此使用int
类型。
这里需要解决如何区分文件中有效数据和输入结束符的问题。C语言采取的解决方法是:在没有输入时,getchar
函数将返回一个特殊值,这个特殊值与任何实际字符都不同。这个值称为EOF
(end of file,文件结束)。我们在声明变量c的时候,必须让它大到足以存放getchar
函数返回的任何值。这里之所以不把c声明成char
类型,是因为它必须足够大,除了能存储任何可能的字符外还要能存储文件结束符EOF
。因此,我们将c声明成int
类型。
EOF
定义在头文件<stdio.h>
中,是一个整型数。其具体数值是什么并不重要,只要它与任何char
类型的值都不相同即可。这里使用符号常量,可以确保程序不需要依赖于其对应的任何特定的数值。
对于经验比较丰富的C语言程序员,可以把这个字符复制程序写得更精炼一些。…赋值操作是一个表达式,并且具有一个值,即赋值后左边变量保存的值。也就是说,赋值可以作为更大的表达式的一部分出现。
习惯这种风格后,读者就会发现按照这种方式编写的程序更易阅读。我们经常会看到这种风格。(不过,如果我们过多地使用这种类型的复杂语句,编写的程序可能会很难理解,应尽量避免这种情况。)
对while
语句的条件部分来说,赋值表达式两边的圆括号不能省略。…优先级…
5.2. 字符计数 …引入了一个新的运算符++
,其功能是执行加1操作。…更精炼一些,且通常效率也更高。…自减运算符--
。…既可以作为前缀运算符,也可以作为后缀运算符。…这两种形式在表达式中具有不同的值。
long
整型数(长整型)至少要占用32位存储单元。在某些机器上int
与long
类型的长度相同,但在一些机器上,int
类型的值可能只有16位存储单元的长度(最大值为32767),这样,相当小的输入都可能使int
类型的计数变量溢出。
使用double
(双精度浮点数)类型可以处理更大的数字。
…C语言的语法规则要求for
循环语句必须有一个循环体,因此用单独的分号代替。单独的分号称为空语句 ,它正好能满足for
语句的这一要求。把它单独放在一行是为了更加醒目。
…这一点很重要。while
语句与for
语句的优点之一就是在执行循环体之前就对条件进行测试。如果条件不满足,则不执行循环体,这就可能出现循环体一次都不执行的情况。在出现0长度的输入时,程序的处理应该灵活一些。在出现边界条件时,while
语句与for
语句有助于确保程序执行合理的操作。
5.3. 行计数 …统计行数等价于统计换行符的个数。
…这里再次用缩进方式表明语句之间的控制关系。
双等于号==
是C语言中表示“等于”关系的运算符。由于C语言将单等于号=
作为赋值运算符,因此使用双等于号==
表示相等的逻辑关系,以示区分。这里提醒注意,在表示“等于”逻辑关系的时候(应该用==
),C语言初学者有时会错误地写成单等于号=
。…即使这样误用了,其结果通常任然是合法的表达式,因此系统不会给出警告信息。
单引号中的字符表示一个整型值,该值等于此字符在机器字符集中对应的数值,我们称之为字符常量 。但是,它只不过是小的整型数的另一种写法而已。例如,'A'
是一个字符常量;在ASCII
字符集中其值为65(即字符A
的内部表示值为65)。当然,用'A'
要比用65好,因为'A'
的意义更清楚,且与特定的字符集无关。
字符串常量中使用的转义字符序列也是合法的字符常量,比如,'\n'
代表换行符的值,在ASCII
字符集中其值为10。我们应当注意到,'\n'
是单个字符,在表达式中它不过是一个整型数而已;而"\n"
是一个仅包含一个字符的字符串常量。
5.4. 单词计数 …这里对单词的定义比较宽松,它是任何其中不包含空格、制表符或换行符的字符序列。
…我们在这里使用了符号常量…这样程序更易读。在较小的程序中,这种做法也许看不出有什么优势,但在较大的程序中,如果从一开始就这样做,因此而增加的一点工作量与提高程序可读性带来的好处相比是值得的。读者也会发现,如果程序中的幻数都以符号常量的形式出现,对程序进行大量修改就会相对容易得多。
…赋值结合次序是由右至左。
运算符||
代表OR
(逻辑或)。相应地,运算符&&
代表AND
(逻辑与),它仅比||
高一个优先级。由&&
或||
连接的表达式由左至右求值,并保证在求值过程中只要能够判断最终的结果为真或假,求值就立即终止。
…一个else
部分,它指定当if
语句中的条件部分为假时所要执行的动作。…if-else
中的两条语句有且仅有一条语句被执行。这两条语句都既可以是单条语句,也可以是括在花括号内的语句序列。
6. 数组 所有的输入字符可以分成12类,因此可以用一个数组存放各个数字出现的次数,这样比使用10个独立的变量更方便。
在C语言中,数组下标总是从0开始。
数组下标可以是任何整型表达式,包括整型变量以及整型常量。
由定义可知,char
类型的字符是小整型,因此char
类型的变量和常量在算术表达式中等价于int
类型的变量和常量。这样做既自然又方便。
…多路判定。在这种方式中,各条件从前往后依次求值,直到满足某个条件,然后执行对应的语句部分。这部分语句执行完成后,整个语句体执行结束(其中的任何语句都可以是括在花括号中的若干条语句)。如果所有条件都不满足,则执行位于最后一个else
之后的语句(如果有的话)。
就程序设计风格而言,我们建议读者采用上面所示的缩进格式以体现该结构的层次关系。否则,如果每个if
都比前一个else
向里缩进一些距离,那么较长的判定序列就可能超出页面的右边界。
switch
语句提供了编写多路分支程序的另一种方式,它特别适合于判定某个整型或字符表达式是否与一个常量集合中的某个元素相匹配的情况。
7. 函数 C语言中的函数等价于Fortran语言中的子程序或函数,也等价于Pascal语言中的过程或函数。函数为计算的封装提供了一种简便的方法,此后使用函数时不需要考虑它是如何实现的。使用设计正确的函数,程序员无需考虑功能是如何实现的,而只需知道它具有哪些功能就够了。在C语言中可以简单、方便、高效地使用函数。我们经常会看到在定义后仅调用了一次的短函数,这样做可以使代码段更清晰易读。
函数定义的一般形式为:
1 2 3 4 5 返回值类型 函数名(0个或多个参数声明) { 声明部分 语句序列 }
函数定义可以以任意次序出现在一个源文件或多个源文件中,但同一函数不能分割存放在多个文件中。如果源程序分散在多个文件中,那么,在编译和加载时,就需要做更多的工作,但这是操作系统的原因,并不是语言的属性决定的。
…声明参数的类型、名字以及该函数返回结果的类型。参数使用的名字只在函数内部有效,对其他任何函数都是不可见的:其他函数可以使用与之相同的参数名字而不会引起冲突。变量也是这样。
我们通常把函数定义中圆括号内列表中出现的变量称为形式参数 ,而把函数调用中与形式参数对应的值称为实际参数 。
函数计算所得的结果通过return
语句返回给main
函数。关键字return
后面可以跟任何表达式,形式为:return 表达式;
。函数不一定都有返回值。不带表达式的return
语句将把控制权返回给调用者,但不返回有用的值。这等同于在到达函数的右终结花括号时,函数就“到达了尽头”。主调函数也可以忽略函数返回的值。
由于main
本身也是函数,因此也可以向其调用者返回一个值,该调用者实际上就是函数的执行环境。一般来说,返回值为0表示正常终止,返回值为非0表示出现异常情况或出错结束条件。
…这种声明称为函数原型 。它必须与函数的定义和用法一致。如果函数的定义、用法与函数原型不一致,将出现错误。
函数原型与函数声明中参数名不要求相同。事实上,函数原型中的参数名是可选的。但是,合适的参数名能够起到很好的说明性作用,因此我们在函数原型中总是指明参数名。
回顾一下,ANSI C同较早版本C语言之间的最大区别在于函数的声明与定义方式的不同。…如果没有声明某个参数的类型,则默认为int
类型。函数声明中不允许包含参数列表,这样编译器就无法在此时检查函数调用的合法性。函数在默认情况下将被假定返回int
类型的值,因此整个函数的声明可以全部省略。
在ANSI C中定义的函数原型语法中,编译器可以很容易检测出函数调用中参数数目和类型方面的错误。ANSI C任然支持旧式的函数声明与定义,这样至少可以有一个过渡阶段。但我们还是强烈建议读者:在使用新式的编译器时,最好使用新式的函数原型声明方式。
8. 参数——传值调用 在C语言中,所有函数参数都是“痛过值”传递的。也就是说,传递给被调用函数的参数值存放在临时变量中,而不是存放在原来的变量中。
最主要的区别在于,在C语言中,被调用函数不能直接修改主调函数中变量的值,而只能修改其私有的临时副本的值。
传值调用的利大于弊。在被调用函数中,参数可以看作是便于初始化的局部变量,因此额外使用的变量更少,这样程序可以更紧凑简洁。
必要时,也可以让函数能够修改主调函数中的变量。这种情况下,调用者需要向被调用函数提供待设置值的变量的地址 (从技术角度看,地址就是指向变量的指针 ),而被调用函数则需要将对应的参数声明为指针类型,并通过它间接访问变量。
如果是数组参数,情况就有所不同了。当把数组名用作参数时,传递给函数的值是数组起始元素的位置或地址——它并不复制数组元素本身。在被调用函数中,可以通过数组下标访问或修改数组元素的值。
9. 字符数组 字符数组是C语言中最常用的数组类型。
…声明中提供数组大小的目的是留出存储空间。函数中没有必要指明数组的长度,这是因为数组的大小是在main
函数中设置的。
有些函数返回有用的值,而有些函数仅用于执行一些动作,并不返回值。函数的返回值类型为void
,它显式说明该函数不返回任何值。
…把字符'\0'
(即空字符 ,其值为0)插入到它创建的数组的末尾,以标记字符串的结束。这一约定已被C语言采用。
值得一提的是,即使是上述这样很小的程序,在传递参数时也会遇到一些麻烦的设计问题。
10. 外部变量与作用域 函数中的变量是函数的私有变量或局部变量。…其他函数不能直接访问它们。其他函数中声明的变量也同样如此。函数中的每个局部变量只在函数被调用时存在,在函数执行完毕退出时消失。这也是其他语言通常把这类变量称为自动变量 的原因。以后我们使用“自动变量”代表“局部变量”。(…static
存储类,这种类型的局部变量在多次函数调用之间保持值不变。)
由于自动变量只在函数调用执行期间存在,因此,在函数的两次调用之间,自动变量不保留前次调用时的赋值,且在每次进入函数时都要显式为其赋值。如果自动变量没有赋值,则其中存放的是无效值。
除自动变量外,还可以定义位于所有函数外部 的变量,也就是说,在所有的函数中都可以通过变量名访问这种类型的变量。由于外部变量可以在全局范围内访问,因此,函数间可以通过外部变量交换数据,而不必使用参数表。再者,外部变量在程序执行期间一直存在,而不是在函数调用时产生、在函数执行完毕时消失。即使在对外部变量赋值的函数返回后,这些变量任然保持原来的值不变。
外部变量必须定义在所有函数之外,且只能定义一次,定义后编译程序将为它分配存储单元。在每个需要访问外部变量的函数中,必须声明相应的外部变量,此时说明其类型。声明时可以用extern
语句显式声明,也可以通过上下文隐式声明。
…定义了外部变量,声明了各外部变量的类型,这样编译程序将为它们分配存储单元。从语法角度看,外部变量的定义与局部变量的定义是相同的,但由于它们位于各函数的外部,因此这些变量是外部变量。函数在使用外部变量之前,必须要知道外部变量的名字。要达到改目的,一种方式是在函数中使用extern
类型的声明。这种类型的声明除了在前面加了一个关键字extern
外,其他方面与普通变量的声明相同。
某些情况下可以省略extern
声明。在源文件中,如果外部变量的定义出现在使用它的函数之前,那么在那个函数中就没有必要使用extern
声明。…在通常的做法中,所有外部变量的定义都放在源文件的开始处,这样就可以省略extern
声明。
如果程序包含在多个源文件中,而某个变量在file1 文件中定义、在file2 和file3 文件中使用,那么在文件file2 与file3 中就需要使用extern
声明来建立该变量与其定义之间的联系。人们通常把变量和函数的extern
声明放在一个单独的文件中(习惯上称之为头文件),并在每个源文件的开头使用#include
语句把所要用的头文件包含进来。后缀名.h
约定为头文件名的扩展名。
…但为了与老版本的C语言程序兼容,ANSI C语言把空参数表看成老版本C语言的声明方式,并且对参数表不再进行任何检查。在ANSI C中,如果要声明空参数表,则必须使用关键字void
进行显式声明。
读者应该注意到,这一节中我们在谈论外部变量时谨慎地使用了定义 (define)与声明 (declaration)这两个词。“定义”表示创建变量或分配存储单元,而“声明”指的是说明变量的性质,但并不分配存储单元。
顺便提一下,现在越来越多的人把用到的所有东西都作为外部变量使用,因为似乎这样可以简化数据的通信——参数表变短了,且在需要时总可以访问这些变量。但是,即使在不使用外部变量的时候,它们也是存在的。过分依赖外部变量会导致一定的风险,因为它会使程序中的数据关系模糊不清——外部变量的值可能会被意外地或不经意地修改,而程序的修改又变得十分困难。…其一便是使用了外部变量;另一方面,…从而使函数失去了通用性。