第一章
第一章主要通过华氏温度转换器来讲解例子
首先需要注意的一个点是这个
1 | printf("%d\t%d\n",fahr,celsius); |
这里可以等价Python中的
1 | print(f"{fahr} {celsius}") |
每个百分号表示其他的参数,这里的话就是后面的两个参数fahr和celsius
而\t代表留一个制表符的位置
其他内容与Python没什么区别(例如while)
这就是一开始的代码,在之后我们会一点点完善:
1 |
|
首先是第一个问题,我们在运行之后可以发现数据并不是右对齐的,所以不是很美观
这时候我们可以在%d中间加上数字来指定打印宽度
例如:
1 | printf("%3d\t%6d\n", fahr, celsius); |
其输出如下:
1 | 0 -17 |
可以看到,在指定打印宽度后,相对应的数据就会随之对齐,同时可以发现%6d会比%3d更宽
修改数据类型
由于温度是必定有小数的,不可能是整数,所以这里必须要用到另一个数据类型:float(浮点数)
具体的话只需要修改一开始的声明即可:
1 | float fahr, celsius; |
这时候如果运行的话,可以发现并不是很美观,因为每个数字都带上了很长一串小数,这时候便要用到另一个东西:格式化文本
具体可以如下操作:
1 | printf("%3.1f\t%6.1f\n", fahr, celsius); |
这里需要注意一点的是:%d指的是整型类型,而%f指的是浮点数类型
细心发现,这里比之前的文本多了个.1,这代表着精确到小数点后1位
还有一点,前文的程序中出现了这个运算式:
1 | celsius = 5 * (fahr - 32) / 9; |
这里不可以使用5/9这种形式
这是因为按整型除法的计算规则,这两个数相除并舍位后得到的结果为0
但由于这个时候我们已经将数据类型改为浮点数了,那么便不会出现舍位的情况
小拓展:为什么会出现整型舍位的情况
接下来来讲讲为什么上文不可以为5/9
首先需要明白的一点是,这两个数的数据类型为整型,所以除出来的结果必定是一个整数
而5/9的结果约为0.55,保留整数部分后为0
舍位,指的也就是舍掉某一位,这里是整型类型,所以必须舍掉小数部分
另外,如果某个算术运算符的所有操作数有整型和浮点数,那么在开始运算之前所有的整型数都会被转换成浮点数
比如说这里:
1 | fahr - 32 |
这里的话32会被转换为浮点数(32.0)再参加运算
For 循环语句
接下来介绍C语言里面的For 循环语句
其具体的框架如下:
1 | for(第一个式子; 第二个式子; 第三个式子){ |
第一个式子
在For语句中,括号内第一个式子指的是初始化变量
例如:
1 | for(fahr = 0; 第二个式子; 第三个式子){ |
需要注意的一点是这个式子可以省略,但必须保留分号:
1 | for(; 第二个式子; 第三个式子){ |
但在这个时候初始化的工作就必须在For循环外面实现
第二个式子
第二个式子的作用是判断是否继续执行循环,如果为真(不为0),则继续执行
如果为假(0),则结束循环
例如:
1 | for(; fahr <= 300; 第三个式子){ |
这个式子同样可以省略,但如果省略会导致无限循环,除非有break语句退出循环
与上面一样,如果省略同样需要保留分号
第三个式子
第三个式子指的是每次循环后执行的语句,一般用于更新循环变量的值
相应例子如下:
1 | for( ; ; fahr += 1){ |
第三个式子与前面一样,可以省略不写,但如果不在循环体内更新变量的话,可能会导致无限循环
下面通过一个例子来说明具体的用法
1 | for(fahr = 0; fahr <= 300; fahr += 150){ |
首先这个循环先定义变量fahr为0,之后开始执行循环,每次循环打印一次fahr
同时在每次训话循环结束后,变量fahr的值增加150,直到fahr的值大于300的时候结束循环
符号常量
符号常量指的是将一个符号定义为一个字符串
例如,可以将300定义为UPPER
在C语言中,使用符号常量的办法是利用#define
具体操作方法如下:
1 |
例如:
1 |
利用这个小方法,我们之前的例子可以改写为:
1 |
|
相较于之前的直接在main()里面定义变量,这种方法会更加直观(前提是你的符号常量是明确的,无歧义的)
另外,细心的人可能发现了,用#define定义符号常量的时候并不需要在末尾加分号
还需要注意一点的是,符号常量的名字一般来讲是大写,为了与其他用小写的变量名作区分
拓展内容
以下内容K&R书中在第一章未提及
#define 的本质
#define 的本质为文本替换,也就是说在编译前的预处理阶段,会把整个程序里面出现的符号变量的名字(不包括在引号里面的)替换为替换文本的东西
以上面的例子为例,在预处理阶段会扫一遍有没有单独出现的UPPER,如果有,就替换为300
文件复制
接下来来讲讲文件复制
在这里需要用到两个全新的函数getchar()和putchar()
其中getchar()为获取输入,例如:
1 | c = getchar(); |
此时变量c就被赋予了对应的输入内容
接下来是putchar(),作用为输出括号内的内容,例如:
1 | putchar(c); |
这样就输出了变量c的内容
接下来通过一个具体的例子来说明一下使用的方法:
1 |
|
通过上面的代码,我们便实现了一个输出的内容为输入的内容的小工具
相当于复制后自动输出的效果
这里要说明一下,EOF指的是end of file,通过按Ctrl+Z后回车即可触发
当然,还可以把上面的这个写的更加紧凑一点:
1 |
|
接下来讲解一下
首先循环第一步,请求输入,并把输入的内容赋值给变量c,而后开始判断,如果变量c不为EOF,则继续执行,否则直接结束循环
这里要特别注意的一点是,while括号内必须这样写,才可以正常识别
这是由于在C语言中,不等于运算符!=的优先度是大于赋值运算符的=,正因如此,如果没加括号,则会变成这样子:
1 | while (c = (getchar() != EOF)){ |
会造成这样的原因与运算符的优先级有关,这一点会在之后讲到,这里先不进行拓展
文件复制
接下来来讲讲文件复制
在这里需要用到两个全新的函数getchar()和putchar()
其中getchar()为获取输入,例如:
1 | c = getchar(); |
此时变量c就被赋予了对应的输入内容
接下来是putchar(),作用为输出括号内的内容,例如:
1 | putchar(c); |
这样就输出了变量c的内容
接下来通过一个具体的例子来说明一下使用的方法:
1 |
|
通过上面的代码,我们便实现了一个输出的内容为输入的内容的小工具
相当于复制后自动输出的效果
这里要说明一下,EOF指的是end of file,通过按Ctrl+Z后回车即可触发
当然,还可以把上面的这个写的更加紧凑一点:
1 |
|
接下来讲解一下
首先循环第一步,请求输入,并把输入的内容赋值给变量c,而后开始判断,如果变量c不为EOF,则继续执行,否则直接结束循环
这里要特别注意的一点是,while括号内必须这样写,才可以正常识别
这是由于在C语言中,不等于运算符!=的优先度是大于赋值运算符的=,正因如此,如果没加括号,则会变成这样子:
1 |
|
这样就变成了先判断输入的数是不是EOF,如果不是,赋值变量c为1,如果是的话则赋值为0
这样就变成了先判断输入的数是不是EOF,此时返回的结果为一个布尔值,True为1,False为0
此时变量c的结果只有1或0
需要注意的一点是,这里使用的类型为int,而不是char
这是因为EOF的值为-1,超出了char的范围(0~255)
当然,也有一种情况char的范围为(-128~127),但在这种情况,有些字符会被判断为负值(例如扩展 ASCII 码的128~255),这个时候便会与EOF混淆
拓展内容
如果你试着执行上面括号不对的版本并输入
你会发现输出的内容并不是一个单独的1
这是因为你输入的内容都会进行一次判断,每次判断都会输出相应的内容
比如你输入123,那么会以此判断1和2和3
这时候就又会发现,诶,输出怎么是4个1,这是因为你输入的时候还需要回车,回车是换行符\n,也需要进行一次判断,这就导致了有4个1
字符计数
接下来学习一个新的运算符++
这个运算符的作用是使得这个变量加一,具体效果如下:
1 | ++nc; |
此时nc的值会加一
那么有什么作用呢,接下来通过一个例子来说明
1 |
|
这段代码的大意为:每次执行循环若不为EOF,则变量nc的值加一
输入为EOF的时候,退出循环,并且打印nc的值
这里还有一个需要补充的点
可以看到这里有一个新的数据类型:long
这个类型的特点如下:
在一些机器上int和long的长度相同,但在一些机器上,int类型长度只有16位的存储单元的长度(最大值为32767)
而long类型的存储单元长度有32位(差不多21亿),这就可以避免了输入过多导致报错的情况
对应的格式为%ld
此外还有另外一个数据类型:double可以储存的长度为64位
1 |
|
在这个语句中,for的循环体是空的,但由于C语言的语法规则要求必须有一个循环体,所以这里用一个单独的分号来表示
像这样的单独一个分号被称为空语句
这里需要说明的一点是,while语句和for语句的一大有点就是在执行循环体之前就会对条件进行测试
如果条件不满足,则不会执行循环体
行计数
接下来来讲讲行计数,这里对应原文的1.5.3的内容
首先,为了实现行计数,我们需要用到前文提及到的几个点
- 第一
while语句的检测是一个字符一个字符的检测 - 第二 换行符
\n虽然看不到输入,但也是实际存在的
利用这几个点,便可以写出一个行计数的小程序
接下来讲讲思路
首先第一个,要先大概明白要干什么,主体的思路大概为检测输入的内容,如果出现了换行符,则次数加一,直到结束输出次数
具体代码如下:
1 | int main() { |
这里的(c == '\n')需要注意的是必须为单引号,这是因为在C语言中这两者的作用并不相同(不跟Python一样)
单引号表示字符常量,用于表示单个字符,一般用于给char变量赋值:
1 | char c = 'A'; |
而双引号则表示一串字符(实际上为一个以\0结尾的字符数组),一般用于定义字符串
这里要表示的是换行符\n,而不是这个字符串\n,所以应该用单引号
单词计数
接下来讲讲单词计数的内容
这部分的内容主要计数分成了几个点
- 计数字符数
- 计数单词数
- 计数行数
首先讲解一下思路,由于字符是无论怎么样,只要有输入就算是有的,所以可以在每一次检测的时候加一即可
接下来是单词数,这里讲的单词指的是任何不包含空格、制表符或换行符的字符序列的字符
最后的行数,其实也就是指换行符的数量\n
这时候可能就有个大概的思路了
每次循环都加一(字符数),如果为换行符的话,则行数加一,如果不为空格、制表符等符号的话,则单词数加一
稍微思考一下便可以得出这个例子:
1 | int main() { |
当然,也可以使用K&R里面的例子:
1 |
|
数组
接下来来讲讲数组的内容,这里对应的是原文的1.6的部分
首先简单介绍一下数组,数组是一个可以储存变量的容器,你可以往里面填入值
格式如下:
1 | ndigit[x] = y; |
其中x为你要填入的位置(默认以0开始),y表示这个位置的值
利用这个特性,我们便可以用更聪明的方式来统计东西
首先我们需要明白一个点,在ASCII码中0~9是连一块的
而如果我们将基准定为输入的数字(例如2)
此时通过2 - 0,便可以得出位置2(对应数组里面的3号位)
那么这样的话,便可以到这样的代码:
1 | for (i = 0; i < 10; ++i) { |
首先第一个的for语句其作用为初始化每个数字出现的次数
而下一个while语句便是重点
首先循环体if语句的判定条件限定在了0~9,并且正如上面所讲的一样,判断内的部分为指定位置的值的增加(说白了就是增加次数到指定的位置上)
这样有什么好处呢,这样的话便不用自己去定义位置,使代码更为简洁
完整代码如下:
1 | int main() { |
这里用else语句而不是在一开始就加1的原因是这里为其他字符,而不是所有字符
所以放到最后面来兜底
函数
接下来讲讲函数方面的内容,这里对应书中的1.7 函数
首先是一个简单的引入。在之前的例子中,我们有使用过这些内置的函数:printf()、getrchar()、putchar()
而这次的目的就是自己编写一个函数,并且成功调用这个函数
任务说明
在Python中,有一个运算符**其作用为进行幂计算
1 | print(x**y) |
这里变量x代表底数,变量y代表幂,例如:
1 | print(2 ** 3) |
而在C中,并没有类似的运算符,所以我们可以自己编写一个运算符
思路
首先幂的计算便是n个底数进行相乘,所以这里有个简单的方法就是利用for语句进行多次循环,最后返回计算好的结果
代码实现
通过上面的思路,可以比较轻松的写出下面这个函数,接下来开始逐一介绍
1 | int power(int base, int n) { |
首先我们可以模拟一下运算的流程,比如说2的3次方
1 | 第一次:2 |
这里其实有个很巧的点,如果n为0的话,那就是2的0次方了,根据for语句条件i <= n才会执行循环体内的内容
此时由于n为0,不满足循环条件,所以并不执行循环
但是很巧的一点是,2的0次方恰好为1
这也是设计的一个巧妙的点
接下来开始说明
首先函数power接受两个值:base和n
这里的话base作为底数,n作为幂
由于循环判断条件是i <= n,而循环体内的条件为每次循环都乘底数一遍,直到不循环为止
最后循环结束,输出对应的值
完整代码如下:
1 | int main() { |
语法笔记
上面为K&R中提供的案例,接下来来讲讲K&R里面的笔记
定义函数
接下来来讲讲定义函数的部分、
首先给出定义函数的结构:
1 | 返回的类型 函数的名字(声明参数) |
需要注意的一点是,函数的定义可以在一个文件的任何地方,这跟Python是截然不同的(Python需要先定义才能调用,C可以把定义的部分放到最后面)
函数的参数
每个函数声明的参数仅在这个函数内部生效,不影响其他函数
正因如此,其他函数便可以与之使用同样的参数名
通常把函数定义中出现的参数称为形式参数
而把函数调用中与形式参数对应的值称为实际参数
举个例子:
1 | int main() { |
上面这个例子中,形式参数为base和n,实际参数为2和3
return语句
在调用了函数后,我们要怎么让结果输出出去呢,这时候便可以使用到return语句了
这个语句的效果是返回return后面的表达式,比如上面的例子就返回了p
如果你仔细观察的话,便可以发现main其实也是一个函数,但是这个函数可以省略return语句
而其他函数如果有指定返回值的类型但是没有给出返回值,则属于 “未定义行为”(返回的结果不可以预测)
这里的声明返回值类型指的是这里:
1 | int main(){ |
main前面的int便是指明了返回值的类型,
函数原型
在作者的例子中还使用了函数原型
1 | int power(int m, int n); |
也就是上面这里例子中的int power(int m, int n);
这里表明了函数power有两个int类型的参数,并且返回一个int类型的值
特别需要注意的一点是,函数原型必须与函数的定义和用法一致,否则会出现报错
但函数原型和定义函数的参数名可以不相同,例如在上面这个例子中,函数原型的参数分别为m,n
而定义函数的时候为base和n
有趣的一点是,函数原型的参数名是可选的,也就是说你可以写成这样:
1 | int power(int, int); |
不过一般为了说明,不会省略参数名
参数——传值调用
接下来简单讲一下传参的问题
在C语言中,函数内部的所有参数值都是通过值传递的,传递的参数值都被传递到临时变量中,与原来的变量相隔开
正因如此,被调用的函数不能修改外部主调函数的变量值,只能修改自己函数内部的值
举个例子:
1 | int power(int, int); |
在上面这个例子中,函数power的参数由c决定,也就是函数内的n
这里可以看到,在执行函数后,原本作为参数传进去的c并没有在外部修改,仍然保持原有的值
当然,也可以让函数修改主调函数的变量,但需要用到指针,这里不展开说明
字符数组
字符数组可以将一个单词的字母储存到这个数组的单个位置里面
接下来我们通过一个例子来说明这一点
这里的例子使用了K&R中的例子:
编写一个程序,使其在结束的时候打印出最长输入的那一行
思路
首先我们先捋一捋思路,这里的关键词为 打印最长输入,既然有最长,那么肯定得用到比较大小,所以必然得用到if语句来对长度进行比较
下一个点是为了打印出最长的输出,所以这里必然得储存输入,所以可能得用到临时变量来储存
很好,来总结一下思路
- 接受输入
- 检查是否为最长
- 如果是最长就储存输入,并且计算其长度用于比较
- 如果不是最长就跳过换下一个输入
- 最后结束返回储存的最长输入
难点
在Python中有个函数专门来统计列表的长度:len(),但是在C语言里面则没有类似的函数
这时候我们要思考怎么去编写这样的一个函数
我们不妨可以联想到之前一直用到的一个点:while循环和getchar()可以一个一个字符查看
而又因为换行符\n我们是看不到的,所以就可以用到这个思路:
- 获取输入
- 如果不是换行符就长度加一
- 如果是换行符就返回长度
开始编写
返回长度
首先是实现返回长度的功能:
1 | int getline(char s[], int lim) |
这里s[]是一个临时数组,用于存储输入的值
如果不为EOF或换行符\n的话,则说明是我们想要的内容,所以一个一个加到临时数组里面,同时每加一次,长度i就加一
这时候可能就有人要问了,诶,那这里是干什么的呢?
1 | if (c == '\n') |
首先让我们思考一下什么情况下我们不会加入到数组:输入为EOF或者\n的时候便不会继续循环
那这里有人就又要问了,那这样子的话为什么不删去for语句里面的判断条件c != '\n'呢
让我们再次回想一下我们要干什么:嗯,是为了查找输入最长的那一行
那一行的末尾是什么呢?诶那就是换行符\n了
这里如果不写上c != '\n'的话,那么循环会继续下去,就不是一行了,而是多行了
所以这里也就是为什么要多这么一个判断的原因
那又有人要问了,这个是干什么的?
1 | s[i] = '\0'; |
这里需要科普的一点是,在C语言中,一个字符串结束的标志是\0
所以这里其实是在说这个字符串结束了,一个简单的收尾工作
这个东西很重要,之后还有个函数依赖于这个特性
接下来让我们思考一下,如果这里输入过多该咋办?所以需要加一个限制条件
限制条件也就是最大长度,这里可以用到传进来的参数lim
所以这里在循环条件里面加一句,只能在这个范围里面执行循环
1 | for (i = 0;i < lim - 1 && (c = getchar()) != EOF && c != '\n';++i) |
这里可能有疑惑了,诶,为什么这里要- 1啊
首先让我们提前补充一个点,一个字符串的结尾,一般为\0
这个\0代表着字符串的结束,而这里的- 1的作用是为这个特殊的标志腾出空间
因为这里如果不腾出空间的话,那么如果这个字符串有1000个字符(包括\n),那么最后一个结束的标志(\0)则会因为无法写入而导致报错,这也就是为什么要- 1的原因
复制最大的数组
接下来是复制最大的那个数组,以便在结束的时候可以输出这个最长的数组
首先让我们思考一下要怎么做
首先第一步是读取最长的那个数组
然后一步一步把这个数组复制一遍
诶,那要怎么看是不是复制完了呢?还记得上面的结尾是什么吗
对,这里的判断条件便是!= '\n'
下面是完整代码:
1 | int main() { |
这里顺带把main的一小部分截出来了,为了方便理解
首先可以发现,这里的话to[i]是复制的那个列表,而from[]是原来的列表
对应关系的话是这样子的:
1 | from[] = s[] = line |
之后每次执行循环都会使得数组的位数增加1个单位,直到到达末尾的\n
既然这里截出了main的一小块,那顺带也讲了吧
这是一个比较简单的循环,其核心在于循环的条件((len = getline(line,MAXLINE)) > 0)
这里发现调用了函数getline(),那么这个函数返回的什么呢?
返回过去看看,可以发现返回的是这个数组的长度,所以说这里的目的是检测输入的内容是否大于0,并且赋值给变量len
main
接下来讲讲main
1 |
|
while语句的内容已经在上面讲过了,接下来往下讲if语句
这里的目的是为了判断是否存在这么个最大的行,这里没有一般为没输入就直接退出了
最后输出内容为longest,也就是复制过后的to[]
完整代码
1 |
|
这里的MAXLINE指的是最大可以支持的长度,说白了就是先把这个长度的数组给准备好,之后把输入一个一个填进去就行了,超过这个长度就没位置可以填了,所以也就是最大的长度了
外部变量与作用域
接下来来讲讲这一方面
局部变量(自由变量)
首先在之前的例子中我们可以发现一个点,main()函数内部使用了一些变量,这些变量是十分特别的,因为只能在main()内部使用,所以被称为私有变量或者局部变量
由于定义这些变量的位置位于函数内部,所以其他函数是不可以访问的
相同的,这也就是为什么在上面的例子中,getline()和copy()都用到了变量名i,但是却无法通用的原因
需要注意的一点是,这些局部变量只有在调用的时候才有用,函数调用结束,这些局部变量也就消失了,这也是为什么有些语言会把局部变量成为自动变量的原因
这里可以思考一个点,自动变量会在函数结束的时候消失,那么如果我要在其他函数用到,得怎么办呢
当然是重新声明
需要注意的一点是,如果自动变量没有赋值,那么其中存放的就是无效值
当然,既然有局部变量,那肯定就有外部变量
外部变量
既然局部变量是只能在函数内部使用,那么按这个道理一推,便可以得知外部变量就是可以在任意函数中通用的变量
外部变量可以用于函数之间交换数据,而并不使用参数表,并且在执行程序的时候外部变量会一直存在
首先,外部变量需要在任何函数之前就定义完,并且只能定义一次,在每个需要访问外部变量的函数中,必须要声明这个使用的外部变量,具体声明使用到extern语句,或者上下文隐式声明
1 | extern 变量类型 变量名 |
由于C语言在C99标准后废除了隐式声明,所以这里不再提及
需要说明一点的是,在一些情况下可以省略extern声明,如果在源文件中,外部变量的定义出现在他的函数之前,那么在那个函数中便可以不使用extern声明
一般来说,所有的外部变量的定义都放在最开头,这样就可以省略掉extern声明
接下来讲讲多个文件的情况
如果你的一个变量位于文件A,而你在文件B使用到了这个变量,那么则需要使用extern声明
通常的做法是,把变量和函数的extern声明放到一个单独的文件中(一般称为头文件)
并在每个文件的开头使用#include语句把要用的这些头文件包含进来
而后缀.h是约定为头文件名的扩展名
接下来看看之前的程序有什么地方可以用到外部变量吧
使用外部变量
首先我们可以先看看getline()函数
首先由main()可以得知,最大范围是由参数传给函数的,而这里的最大参数便可以使用MAXLINE来代替原有的lim参数
接下来可以发现,原来的程序使用了一个参数line来储存句子
而这个由于这个句子使用了for循环使得每次调用的时候位置都是会重新计算(i = 0)的
这就导致了这里无论调用多少次,开始存储的位置还是一样的,所以这里的第一个参数也可以换成外部变量
最后的getline函数如下
1 | int getline(void) |
可以看到这里使用了外部变量line[]
而MAXLINE在开头的#define就定义过了,编译的时候会自动替换成后面的1000
这里需要注意的一点是,getline()的括号内部写的是void
这是因为在 ANSI C中,如果要声明空参数表,则必须使用关键字void进行显式声明
接下来是copy()
1 | void copy(void) |
通过观察main()可以知道,原先的原先数组from[]实际可以替换为line[],而输出的数组可以用到longest[]
longest[]是在一开始就定义的外部变量,可以不借助参数而选择直接写入函数内部
最后是完整的代码:
1 |
|
需要说明的一点是,虽然外部变量很好用,但是如果过度使用的可能会导致数据之间的关系变得不清,并且如果将外部变量写入程序内部,会不传入参数进函数而导致函数失去了通用性
补充内容
接下来补充几个小点
上文在讲自动变量的时候提及了以下内容:
如果自动变量没有赋值,那么其中存放的就是无效值
而如果外部变量没有初始化的话,那么编译器会自动初始化为0
此外还有有关定义和声明的区别
定义指的是:表示创建变量或者分配存储单元
而声明指的是:说明变量的性质,但并不分配存储单元
举个例子:
在外部变量中,定义如下:
1 | int max; |
而声明如下:
1 | extern int max; |
定义只能定义一次,而声明可以声明无限次