The C Programming Language
本文最后更新于:2024年3月7日 上午
1.1 入门
1.2 变量与算数表达式
printf中的格式说明可以省略宽度与精度。
例如
%6f
表示待打印的浮点数至少有6个字符宽%.2f
表示待打印的浮点数的小数点后有两位小数,但宽度没有限制;%f
则仅仅要求按照浮点数打印该数%6.2f
按照浮点数打印,至少6个字符宽,小数点后有两位小数
1.3 for语句
循环套循环,注意内部循环变量是否需要再次初始化
1 |
|
- 仅打印三次。
1 |
|
- 死循环。
- k赋值为0,0为假。
1.4 符号常量
#define
指令可以把符号名(或称为符号常量)定义为一个特定的字符串
1 |
|
注:
#include #define 是预处理指令不是关键字
在此定义后,
程序中出现的所有在
#define
中定义的名字(既没有用括号引起来,也不是其他名字的一部分)都将用相应的替换文本替换。其中,名字与普通变量名的形式相同:它们都是以字母开头的字母和数字序列;
替换文本可以是任何字符序列,而不仅限于数字。
符号常量,不是变量,不需要出现在声明中。
符号常量名通常用大写字母拼写,更易与小写字母拼写的变量名相区别。
#define
指令行的末尾没有分号。
1.5 字符输入/输出
标准库提供的输入/输出模型非常简单
无论文本从何处输出,输出到何处,其输入/输出都是按照字符流的方式处理。
文本流是由多行字符构成的字符序列,而每行字符则由0个或多个字符组成,行末是一个换行符。
标准库负责使每个输入/输出流都能够遵守这一模型。
标准库提供了一次读/写一个字符的函数
其中最简单的是getchar
和putchar
两个函数。
每次调用时,getchar
函数从文本流中读入下一个输入字符,并将其作为结果值返回。
1 |
|
之后,变量c将包含输入流的下一个字符。这种字符通常是通过键盘输入的,也可以从文件读入。
每次调用putchar
函数时将打印一个字符。例如,语句
1 |
|
将把整形变量c的内容以字符的形式打印出来,通常是显示在屏幕上。
putchar
与printf
这两个函数可以交替调用,输出的次序与调用的次序一致。
1.5.1 文件复制
简单例子:把输入一次一个字符地复制到输出,其基本思想如下:
读一个字符
while(该字符不是文件结束指示符)
输出刚读入的字符
读下一个字符
将上述基本思想转换为C语言程序为:
1 |
|
字符无论以什么形式表现,他在机器内部都是以位模式存储的.
如何区分文件中有效数据与输入结束符的问题。C语言采取的解决方法是:
在没有输入时,getchar
函数将返回一个特殊值,这个特殊值与任何实际字符都不同。
这个值成为EOF(end of file,文件结束)。
这里之所以不把c生命成char类型,是因为它必须足够大,除了能存储任何可能的字符外还要能存储文件结束符EOF。
因此,我们将c声明成int类型。
EOF定义在头文件<stdio.h>
中是一个整形数值,通常为
-1,但它依系统有所不同。
ASCII代码值的范围是0~127,不可能出现-1,与任何char类型的值都不相同。
因此可以用EOF作为文件结束标志。
在Windows系统中,用户可以通过Ctrl+Z
来表示EOF,以结束文本流的输入。
更进一步,类似于
1 |
|
之类的赋值操作是一个表达式,并且具有一个值,即赋值后左边变量保存的值。
也就是说,赋值可以作为更大表达式的一部分出现。
如果将 为c复制的操作放在while循环语句的测试部分中,可改写程序为:
1 |
|
对while语句的条件部分来说,赋值表达式两边的圆括号不能省略。
!= 运算符优先级 比 = 赋值运算符 的优先级要高,若不使用圆括号,关系测试!=将在赋值=操作之前执行。
因此语句
1 |
|
等价于语句
1 |
|
该语句执行后,c的值将被置为0或1(取决于调用getchar
函数时是否碰到文件结束标志)
这并不是我们所希望的结果
1.5.2 字符计数
下列程序用于对字符计数,与上面的复制程序类似。
1 |
|
其中,语句
1 |
|
引入了一个新的运算符++。其功能是执行加1操作。可以用nc=nc+1代替它,但++nc更精炼,通常效率更高。相应的还有--。
++与--既可作为前缀运算符,也可作为后缀运算符,这两种形式在表达式中具有不同的值,但++nc和nc++都使nc的值增加1
我们在这里不使用while循环语句,而用for循环语句来展示编写此循环的另一种方法:
1 |
|
对于float与double类型,printf
函数都使用%f
进行说明。%.0f
强制不打印小数点与小数部分,因此小数部分的位数为0。
在此程序段中:
- for循环语句的循环体是空的,所有工作都在测试(条件)部分与增加步长部分完成了。
- 但C语言语法规则要求for循环语句必须要有一个循环体,因此用单独的分号代替。
- 单独的分号称为空语句。正好能满足for语句这一要求。
- 把它单独放在一行是为了更加醒目。
我们考虑以下情况:如果输入中不包含字符,那么,在第一次调用
getchar
函数时,while语句或for语句中的条件测试从一开始就为假,程序的执行结果将为0,这也是正确的结果。这一点很重要。while语句与for语句的优点之一就是在执行循环体之前就对条件进行测试。如果条件不满足,就不执行循环体,这就可能出现循环体一次都不执行的情况。再出现0长度输入时,程序的处理应该灵活一些。在出现边界条件时,while语句与for语句有助于确保程序执行合理的操作。
1.5.3 行计数
接下来这个程序用于统计输入的行数。
标准库保证输入文本流以行序列的形式出现,每一行均以换行符阶数。
因此,统计行数等价于统计换行符的个数。
1 |
|
while循环语句的循环体是一个if语句,它控制自增语句++n1。
易错点: - 双等于号 == 表示 “等于" 的关系运算符 - 单等于号 = 表示 赋值运算符 - 混用系统并不会给出警告信息
单引号中的字符表示一个整型值,该值等于此字符在机器字符集中对应的数值,我们称之为字符常量。
但是,它只是小的整型数的另一种写法而已,例如 'A' 是一个字符常量,在ASCII字符集中其值为 65(即字符A的内部表示值为 65 )。
当然,用 'A' 要比用 65 好,因为 'A' 的意义更清楚,且与特定字符集无关。
字符串常量中使用的转义字符序列也是合法的字符常量
比如 '' 代表换行符的值,在ASCII字符集中其值为 10
'' 是单个字符,在表达式中仅代表一个整型数
"" 是一个仅包含一个字符的字符串常量
有关字符串与字符的关系,我们将在第2章进一步讨论
1.5.4 单词计数
我们将介绍第4个使用程序用于统计行数、单词数与字母数。
这里对单词的定义比较宽松,它是任何其中不包含空格,制表符或换行符的字符序列。
1 |
|
程序执行时,每遇到单词的第一个字符,它就作为一个新单词加以统计。
state变量记录程序当前是否正位于一个单词之中,它的初值是“不在单词中”,即初值被赋为out。
我们在这里使用了符号常量IN与OUT,而没有使用其对应的数值1与0,这样使程序更易读。
后续需要对程序大量修改也会容易简单的多。
下列语句
1 |
|
将其中的三个变量都设置为0。
在兼有值与赋值两种功能的表达式中,赋值的结合次序是从右到左。
上面这条语句等价于
1 |
|
运算符 || 代表 OR(逻辑或),所以下列语句
1 |
|
的意义是“如果c是空格,或c是换行符,或c是制表符”。
相应的,运算符 && 代表 AND(逻辑与) ,它仅比 || 高一个优先级。
由 && 或 || 连接的表达式,从左到右求值,并保证在求值过程中,满足短路运算,即
只要能够判断最终的结果为真或假,求值就立即停止。
这段程序还包括一个else部分,它指定if语句中的条件部分为假时索要执行的动作。
if-else中的两条语句有且仅有一条语句被执行。
当表达式的值为真(任意非0数,包括负数),则执行if后的语句,否则执行else后的语句。
这两条语句既可以是单条语句,也可以时括在花括号内的语句序列。
在单词计数程序中,else后的语句仍是一个if语句,该if语句控制了包括在花括号内的两条语句。
1 |
|
1.6 数组
编写一个程序,统计各个数字、空白符(包括空格符、制表符以及换行符)以及其他字符出现的次数
所有的输入字符可以分成12类,因此可以用一个数组存放各个数字出现的次数,这样比使用10个独立的变量更方便。
1 |
|
该程序中的声明语句
1 |
|
- 将变量ndigit声明为10个整型数构成的数组
- C语言中,数组下标总是从0开始。
ndigit[0]
到ndigit[9]
。 - 数组下标可以是任何整型表达式,包括整型变量(如i)以及整型常量。
1 |
|
用于判断c中的字符是否为数字。如果是数字,那么该数字对应的数值是c - '0'
只有当 '0' , '1' , \(\cdots\) , '9' 具有连续递增的值时,这种做法才可行。
由定义可知,char类型的字符时小整型,因此char类型的变量和常量在算数表达式中等价于int类型的变量和常量。这样做自然又方便,例如
c - '0'
是一个整型表达式,其值将为0~9,因此可以充当数组的合法下标。
1.7 函数
函数定义形式一般为:
1 |
|
函数定义可以以任意次序出现在一个源文件或多个源文件中,但同一函数不能分割存放在多个文件中。
如果源程序分散在多个文件中,那么,在编译和加载时就需要更多的工作,但这是操作系统决定的,并不是语言的属性决定的。
函数的参数使用的名字只在函数内部有效,对其他任何函数都是不可见的:
其他函数可以使用与之相同的参数名,不会冲突。
我们通常把函数定义中圆括号内列表中出现的变量称为形式参数,而把函数调用中与形式参数对应的值称为实际参数。
1 |
|
出现在main函数之前的声明语句
1 |
|
这种声明称为函数原型,必须与函数定义和用法一致。
- 函数原型与函数声明中的参数名不要求相同。
- 函数原型中的参数名时可选的
可写为
1 |
|
早版本C语言与ANSI C之间最大区别在于函数的声明与定义方式不同,这里不介绍
1.8 参数——传值调用
在C语言中,所有函数参数都是“通过值”传递的。
也就是说,传递给被调用函数的参数值存放在临时变量中,而不是存放在原来的变量中。
与其他语言最主要的区别在于:
在C语言中,被调用函数不能直接修改主调函数中变量的值,而只能修改其私有的临时副本的值。
传值调用利大于弊。
在被调用函数中,参数可以看作时便于初始化的局部变量,因此额外使用的变量更少,这样程序可以更紧凑。
必要时,可以通过指针提供变量的地址,修改主调函数中的变量。
被调用函数参数则需要声明为指针类型。
如果是数组参数,情况就有所不同了。当把数组名用作参数时,传递给函数的值时数组其实元素的位置或地址——它并不赋值数组元素本身。在被调用函数中,可以通过数组下表访问或修改数组元素中的值。
1.9 字符数组
字符数组时C语言中最常用的数组类型。
通过一个程序说明基本用法:该程序读入一组文本行,并把最长的文本行打印出来
1 |
|
编写一个独立的函数
getline
,它读取输入的下一行。尽量保持该函数在其他场合也有用函数应该在读到文件末尾时返回一个信号
更为有用的设计时它能够在读入文本行时返回该行的长度,而在遇到文件结束符时返回0
由于0不是有效的行长度,因此可以作为标志文件结束的返回值
每一行至少包括一个字符,只包含换行符的行,其长度为1
当发现某个新读入的行比已处理最长行还要长,就需要保存该行
用另一个函数
copy
把新行复制到一个安全的位置。在主函数main中控制
getline
和copy
1 |
|
值得一提的时,即使是上述这样很小的程序,在传递参数时也会遇到一些麻烦的设计问题。例如,当读入的行长度大于允许的最大值时,main函数应该如何处理?
getline
函数的执行是安全的,无论是否到达换行符字符,当数组满时他将停止读字符。main
函数可以通过测试行的长度以及检查返回的最后一个字符来判定当前行是否太长,然后再根据具体的情况处理。为了简化程序,我们这里不考虑这个问题。调用
getline
函数的程序无法预先知道输入行的长度,因此getline
函数需要检查是否溢出。另一方面,调用copy
函数的程序知道(也可以找出)字符串的长度,因此该函数不需要进行错的检查
1.10 外部变量与作用域
局部变量、自动变量
main
函数中的变量时main
函数的私有变量或局部变量。
由于它们是在main
函数中声明的,因此其他函数不能直接访问它们。
其他函数中声明的变量也同样如此。
函数中的每个局部变量只在函数被调用时存在,再函数执行完毕退出时消失。
其他语言通常称这类变量为自动变量。
第4章将讨论
static
存储类,这种类型的局部变量再多次函数调用之间保持值不变。
由于自动变量只在函数调用执行期间存在,因此两次调用之间,
- 自动变量不保留前次调用时的赋值。
- 在每次进入函数时都要显式地为其赋值。如果自动变量没有复制,则其中存放的是无效值。
外部变量
除自动变量外,还可以定于位于所有函数外部的变量。
所有函数中都可以通过变量名访问这种类型的变量。外部变量可以在全局范围内访问。
函数可以通过外部变量交换数据,而不必使用参数表。
外部变量在程序执行期间一直存在。即使在对外部变量赋值的函数返回后,这些变量也保持原来的值不变。
- 外部变量必须定义在函数之外
- 外部变量只能定义一次
- 若函数需要访问外部变量,必须声明相应的外部变量
- 声明可通过
extern
语句显式声明,也可通过上下文隐式声明
以下情况可省略extern
声明
- 外部变量的定义出现在使用它的函数之前,那么在那个函数中就没必要使用
extern
声明
如果程序包含在多个源文件中,而某个变量在file1文件中定义、在file2和file3文件中使用,
那么在文件file2与file3中就需要使用extern
声明来建立该变量与其定义之间的联系。
人们通常把变量和函数的
extern
声明放在一个单独的文件中(习惯上称之为头文件,后缀.h
),并在每个源文件的开头使用#inlcude
语句把所要的头文件包含进来。
void
函数参数
在ANSI
C中,若要声明空参数表,则必须使用关键字void
进行显式声明。
1 |
|
定义与声明
- 定义(define):表示创建变量和分配存储单元。
- 声明(declaration):说明变量的性质,但不分配存储单元。
使用外部变量的利弊
优势:
- 可以简化数据的通信——参数表变短了
- 在需要时总可以访问这些变量
风险:
- 使程序中的数据关系模糊不清
- 外部变量的值可能会被意外的或是不经意的修改,并不安全
- 程序的修改变得十分困难
- 函数使用外部变量会将它们所操纵的变量名直接写入函数,导致函数失去通用性
第2章 类型、运算符与表达式
变量和常量使程序处理的两种基本数据对象。声明语句说明变量的名字及类型,也可以指定变量的初值。
运算符指定将要进行的操作。
表达式则将变量与常量组合起来生成新的值。
对象的类型决定该对象可取值的集合以及可以对该对象执行的操作。
ANSI标准对语言的基本类型与表达式做了许多小的修改与增补。所有整型都包括
signed
(带符号)和unsigned
(无符号)两种形式,且可以表示无符号常量与十六进制字符常量。浮点运算可以以单精度运行,还可以使用更高精度的long
double
类型。字符串常量可以编译时连接。ANSI C还支持枚举类型,该语言特性经过了长期的发展才形成。对象可以声明为const
(常量)类型,表明其值不能修改。改变准还对算数整型之间的自动强制转换规则进行了补充,以适合于更多的数据类型。
2.1 变量名
对变量的命名与符号常量存在一些限制条件。
- 名字是由字母和数字组成的序列
- 第一个字符必须为字母
- 下划线"__"被看作字母,通常用于命名较长的变量名。以提高其可读性
注意
- 由于库例程的名字通常以下划线开头,因此变量名不要以下划线开头
- 大小写字母是有区别的。
- 不能用关键字作为变量名。所有关键字中的字符必须小写。
传统C语言用法中,变量名使用小写字母,符号常量名全部使用大写字母。
对于内部名,至少前31个字符有效。
函数名和外部变量名包含的字符数目可能小于31,
这是因为汇编程序和加载程序可能会使用这些外部名,而语言本身是无法控制加载和汇编程序的。
这里外部名指的是在链接过程中所涉及的标识符,其中包括文件间共享的函数名和全局变量名。
因此外部名
abcdefgh
和abcdef
将被当作同一个标识符处理。
选择的变量名要尽量从字面上表达变量的用途。
局部变量一般使用较短的变量名
外部变量使用较长的名字。
2.2 数据类型及长度
C语言仅提供了下列几种基本数据类型
关键字 | 类型 | 大小 | 特点 |
---|---|---|---|
char | 字符型 | 1字节 | 可以存放本地字符集中的一个字符 |
int | 整型 | 4字节 | 通常反映了所用机器中整数的最自然长度 |
float | 单精度浮点型 | 4字节 | |
double | 双精度浮点型 | 8字节 |
此外,还可以在这些基本数据类型的前面加上一定限定符。
short与long
修饰整型时,关键字int可省略,short不得长于int,int不得长于long。
类型 | 通常长度 | 最少长度 |
---|---|---|
short | 16位 | 16位 |
int | 16位/32位 | 16位 |
long | 32位 | 32位 |
signed与unsigned
可用于限定char类型或任何整型
unsign
类型数总是正值或0,并遵循算数模\(2^n\)定律,其中n是该类型占用的位数- 不带类型的char类型对象是否带符号取决于具体机器,但可打印字符总为正值
long double
表示高精度的浮点数。同整型一样,浮点数的长度也取决于具体的实现,float
、double
与long double
类型可以表示相同的长度,也可以表示梁总或三种的长度。有关这些定义的长度可在标准头文件<limits.h>
与<float.h>
中找到。
2.3 常量
整型
类似于
1234
的整数常量属于int
类型long类型的常量以字母
l
或L
结尾(建议使用大写),如123456789L
如果一个整数太大无法用
int
,将自动被当作long
处理无符号常量以
u
或U
结尾后缀
ul
或UL
表明是unsigned long
类型
浮点数
- 浮点数常量中包含一个小数点(
123.4
)或一个指数(1e-2
),也可以二者都有 - 没有后缀的浮点数常量为
double
类型 - 后缀
f
或F
表示float
类型 - 后缀
l
或L
表示long double
类型
八进制、十六进制
前缀
0
的整型常量表示为八进制前缀为
0x
或0X
格式表示为十六进制八进制与十六进制常量也可使用
U
或L
等后缀例如
0XFUL
是一个unsigned long
类型的常量,其值等于十进制数15
字符常量
一个字符常量就是一个整数,书写时将一个字符括在单引号中,如'x'
字符在机器字符集中的数值就是字符常量的值。
字符常量一般用来与其他字符进行比较,但也可以像其他整数一样参与数值运算。
ANSI C语言中的全部转义字符序列
转义字符 | 意义 | 转义字符 | 意义 |
---|---|---|---|
响铃符 | \ | 反斜杠 | |
回退符 | ? | 问号 | |
换页符 | ' | 单引号 | |
换行符 | " | 双引号 | |
回车符 | 八进制数 | ||
横向制表符 | 十六进制数 | ||
纵向制表符 |
字符常量
'\0'
表示值为0的字符,也就是空字符(null)。我们通常用
'\0'
的形式代替0,以强调某些表达式的字符属性,但其数字值为0。
常量表达式
常量表达式是仅仅包含常量的表达式。
- 这种表达式在编译时求值,而不再运行时求值。
例如
1 |
|
字符串常量
字符串常量也叫字符串字面值
用双引号括起来的0个或多个字符组成的字符序列。
例如
1 |
|
- 双引号不是字符串的一部分,它只用于限定字符串
- 字符常量中使用的转义字符序列同样课用在字符串中
- 在字符串中使用
\"
表示双引号字符
编译时可以将多个字符常量连接起来,例如,下列形式
1 |
|
等价于
1 |
|
从技术角度看,字符串常量就是字符数组。
字符串的内部表示使用一个空字符
'\0'
作为字符串的结尾存储字符串的物理存储单元数比括在双引号中的字符多一个
C语言对字符串的长度没有限制
程序必须扫描完整个字符串后才能确定其长度
strlen(s)
返回字符串s的长度,不包括末尾的'\0'
,<string.h>
字符常量与仅包含一个字符的字符串之间的区别
常量 | 本质 | 解释 |
---|---|---|
'x' | 一个整数 | 其值是字母x在机器字符集中对应的数值(内部表示值) |
"x" | 字符数组 | 包含一个字符(即字母x)以及一个结束符'\0' 的字符数组 |
枚举常量
枚举是一个整型常量值的列表。
没有显式说明的情况下,enum类型中的第一个枚举名的值为0,第二个为1,以此类推。
只指定了部分枚举名的值,那么未指定值的枚举名的值将依着最后一个指定的值向后递增。
不同枚举中的名字必须互不相同
同一枚举中不同的名字可以具有相同的值
相比
#define
语句,枚举的优势在于常量值可自动生成
2.4 声明
所有变量都必须先声明后使用(包括隐式声明)
一个声明指定一种变量类型
后面所带的变量表可以包含一个或多个该类型的变量
推荐一行声明一个变量
如果不是自动变量(static,register),仅可初始化一次,并且在程序开始执行前进行,初始化表达式必须为常量表达式
自动变量每次进入函数或程序块都会初始化一次,并且初始化表达式可以为任意表达式
默认情况下,
外部变量与静态变量将被初始化为0
未经显式初始化的自动变量的值为未定义值(即无效值)
任何变量的声明都可以加const
限定符限定
该限定符指定变量的值不能被修改
对数组而言,数组所有元素的值都不能被修改
const
限定符也可配合数组参数使用,表明函数不能修改数组元素的值
1 |
|
如果试图修改const
限定符限定的值,其结果取决于具体实现。