第一章

第一章主要通过华氏温度转换器来讲解例子

首先需要注意的一个点是这个

1
printf("%d\t%d\n",fahr,celsius);

这里可以等价Python中的

1
print(f"{fahr} {celsius}")

每个百分号表示其他的参数,这里的话就是后面的两个参数fahrcelsius

\t代表留一个制表符的位置

其他内容与Python没什么区别(例如while)

这就是一开始的代码,在之后我们会一点点完善:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

main()
{
int fahr, celsius;
int lower, upper, step;

lower = 0;
upper = 300;
step = 20;

fahr = lower;
while (fahr <= upper) {
celsius = 5 * (fahr - 32) / 9;
printf("%d\t%d\n", fahr, celsius);
fahr += step;
}

}

首先是第一个问题,我们在运行之后可以发现数据并不是右对齐的,所以不是很美观

这时候我们可以在%d中间加上数字来指定打印宽度

例如:

1
2
printf("%3d\t%6d\n", fahr, celsius);
printf("%3d\t%3d\n", fahr, celsius);

其输出如下:

1
2
3
4
5
6
  0        -17
0 -17
150 65
150 65
300 148
300 148

可以看到,在指定打印宽度后,相对应的数据就会随之对齐,同时可以发现%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
2
3
for(第一个式子; 第二个式子; 第三个式子){
表达式;
}

第一个式子

For语句中,括号内第一个式子指的是初始化变量

例如:

1
2
3
for(fahr = 0; 第二个式子; 第三个式子){
表达式;
}

需要注意的一点是这个式子可以省略,但必须保留分号:

1
2
3
for(; 第二个式子; 第三个式子){
表达式;
}

但在这个时候初始化的工作就必须在For循环外面实现

第二个式子

第二个式子的作用是判断是否继续执行循环,如果为真(不为0),则继续执行

如果为假(0),则结束循环

例如:

1
2
3
for(; fahr <= 300; 第三个式子){
表达式;
}

这个式子同样可以省略,但如果省略会导致无限循环,除非有break语句退出循环

与上面一样,如果省略同样需要保留分号

第三个式子

第三个式子指的是每次循环后执行的语句,一般用于更新循环变量的值

相应例子如下:

1
2
3
for( ; ; fahr += 1){
表达式;
}

第三个式子与前面一样,可以省略不写,但如果不在循环体内更新变量的话,可能会导致无限循环


下面通过一个例子来说明具体的用法

1
2
3
for(fahr = 0; fahr <= 300; fahr += 150){
printf("%3.1f\n", fahr);
}

首先这个循环先定义变量fahr为0,之后开始执行循环,每次循环打印一次fahr

同时在每次训话循环结束后,变量fahr的值增加150,直到fahr的值大于300的时候结束循环


符号常量

符号常量指的是将一个符号定义为一个字符串

例如,可以将300定义为UPPER

在C语言中,使用符号常量的办法是利用#define

具体操作方法如下:

1
#define 符号常量名称 要替换的文本

例如:

1
#define UPPER 300

利用这个小方法,我们之前的例子可以改写为:

1
2
3
4
5
6
7
8
9
#define LOWER 0
#define UPPER 300
#define STEP 150
int main() {
float fahr;
for (fahr = LOWER; fahr <= UPPER; fahr += STEP) {
printf("%3.1f \t %6.1f\n", fahr, (5.0 / 9.0) * (fahr - 32));
}
}

相较于之前的直接在main()里面定义变量,这种方法会更加直观(前提是你的符号常量是明确的,无歧义的)

另外,细心的人可能发现了,用#define定义符号常量的时候并不需要在末尾加分号

还需要注意一点的是,符号常量的名字一般来讲是大写,为了与其他用小写的变量名作区分

拓展内容

以下内容K&R书中在第一章未提及

#define 的本质
#define 的本质为文本替换,也就是说在编译前的预处理阶段,会把整个程序里面出现的符号变量的名字(不包括在引号里面的)替换为替换文本的东西

以上面的例子为例,在预处理阶段会扫一遍有没有单独出现的UPPER,如果有,就替换为300


文件复制

接下来来讲讲文件复制

在这里需要用到两个全新的函数getchar()putchar()

其中getchar()为获取输入,例如:

1
c = getchar();

此时变量c就被赋予了对应的输入内容

接下来是putchar(),作用为输出括号内的内容,例如:

1
putchar(c);

这样就输出了变量c的内容

接下来通过一个具体的例子来说明一下使用的方法:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int main(){
int c;

c = getchar();
while(c != EOF){
putchar(c);
c = getchar();
}
}

通过上面的代码,我们便实现了一个输出的内容为输入的内容的小工具

相当于复制后自动输出的效果

这里要说明一下,EOF指的是end of file,通过按Ctrl+Z后回车即可触发

当然,还可以把上面的这个写的更加紧凑一点:

1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main(){
int c;

;
while((c = getchar()) != EOF){
putchar(c);
}
}

接下来讲解一下

首先循环第一步,请求输入,并把输入的内容赋值给变量c,而后开始判断,如果变量c不为EOF,则继续执行,否则直接结束循环

这里要特别注意的一点是,while括号内必须这样写,才可以正常识别

这是由于在C语言中,不等于运算符!=的优先度是大于赋值运算符的=,正因如此,如果没加括号,则会变成这样子:

1
2
3
while (c = (getchar() != EOF)){
putchar(c);
}

会造成这样的原因与运算符的优先级有关,这一点会在之后讲到,这里先不进行拓展

文件复制

接下来来讲讲文件复制

在这里需要用到两个全新的函数getchar()putchar()

其中getchar()为获取输入,例如:

1
c = getchar();

此时变量c就被赋予了对应的输入内容

接下来是putchar(),作用为输出括号内的内容,例如:

1
putchar(c);

这样就输出了变量c的内容

接下来通过一个具体的例子来说明一下使用的方法:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int main(){
int c;

c = getchar();
while(c != EOF){
putchar(c);
c = getchar();
}
}

通过上面的代码,我们便实现了一个输出的内容为输入的内容的小工具

相当于复制后自动输出的效果

这里要说明一下,EOF指的是end of file,通过按Ctrl+Z后回车即可触发

当然,还可以把上面的这个写的更加紧凑一点:

1
2
3
4
5
6
7
8
#include <stdio.h>
int main(){
int c;

while((c = getchar()) != EOF){
putchar(c);
}
}

接下来讲解一下

首先循环第一步,请求输入,并把输入的内容赋值给变量c,而后开始判断,如果变量c不为EOF,则继续执行,否则直接结束循环

这里要特别注意的一点是,while括号内必须这样写,才可以正常识别

这是由于在C语言中,不等于运算符!=的优先度是大于赋值运算符的=,正因如此,如果没加括号,则会变成这样子:

1
2
3
4
5
6
7
8
#include <stdio.h>
int main() {
int c;

while (c = (getchar() != EOF)) {
printf("%d",c);
}
}

这样就变成了先判断输入的数是不是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,那么会以此判断123

这时候就又会发现,诶,输出怎么是4个1,这是因为你输入的时候还需要回车,回车是换行符\n,也需要进行一次判断,这就导致了有4个1

字符计数

接下来学习一个新的运算符++

这个运算符的作用是使得这个变量加一,具体效果如下:

1
++nc;

此时nc的值会加一

那么有什么作用呢,接下来通过一个例子来说明

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

int main(){
long nc;

nc += 0;
while(getchar() != EOF){
++nc;
}
printf("%ld",nc);
}

这段代码的大意为:每次执行循环若不为EOF,则变量nc的值加一

输入为EOF的时候,退出循环,并且打印nc的值

这里还有一个需要补充的点

可以看到这里有一个新的数据类型:long

这个类型的特点如下:

在一些机器上intlong的长度相同,但在一些机器上,int类型长度只有16位的存储单元的长度(最大值为32767)

long类型的存储单元长度有32位(差不多21亿),这就可以避免了输入过多导致报错的情况

对应的格式为%ld

此外还有另外一个数据类型:double可以储存的长度为64位

1
2
3
4
5
6
7
8
9
#include <stdio.h>

int main() {
double nc;

for (nc = 0; getchar() != EOF; nc++)
;
printf("%.0f\n", nc);
}

在这个语句中,for的循环体是空的,但由于C语言的语法规则要求必须有一个循环体,所以这里用一个单独的分号来表示

像这样的单独一个分号被称为空语句

这里需要说明的一点是,while语句for语句的一大有点就是在执行循环体之前就会对条件进行测试

如果条件不满足,则不会执行循环体

行计数

接下来来讲讲行计数,这里对应原文的1.5.3的内容

首先,为了实现行计数,我们需要用到前文提及到的几个点

  • 第一 while语句的检测是一个字符一个字符的检测
  • 第二 换行符\n虽然看不到输入,但也是实际存在的

利用这几个点,便可以写出一个行计数的小程序

接下来讲讲思路

首先第一个,要先大概明白要干什么,主体的思路大概为检测输入的内容,如果出现了换行符,则次数加一,直到结束输出次数

具体代码如下:

1
2
3
4
5
6
7
8
9
int main() {
int c, nl;

nl = 0;
while ((c = getchar()) != EOF)
if (c == '\n')
++nl;
printf("%d\n", nl);
}

这里的(c == '\n')需要注意的是必须为单引号,这是因为在C语言中这两者的作用并不相同(不跟Python一样)

单引号表示字符常量,用于表示单个字符,一般用于给char变量赋值:

1
char c = 'A';

而双引号则表示一串字符(实际上为一个以\0结尾的字符数组),一般用于定义字符串

这里要表示的是换行符\n,而不是这个字符串\n,所以应该用单引号

单词计数

接下来讲讲单词计数的内容

这部分的内容主要计数分成了几个点

  • 计数字符数
  • 计数单词数
  • 计数行数

首先讲解一下思路,由于字符是无论怎么样,只要有输入就算是有的,所以可以在每一次检测的时候加一即可

接下来是单词数,这里讲的单词指的是任何不包含空格、制表符或换行符的字符序列的字符

最后的行数,其实也就是指换行符的数量\n

这时候可能就有个大概的思路了

每次循环都加一(字符数),如果为换行符的话,则行数加一,如果不为空格、制表符等符号的话,则单词数加一

稍微思考一下便可以得出这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int main() {
int c, n_word, n_line, n_chara,isword;

isword = 1;
n_word = n_line = n_chara = 0;
while ((c = getchar()) != EOF) {
++n_chara;
if (c == ' ' || c == '\n' || c == '\t')
isword = 0;
if (isword)
++n_word;
else if (c == '\n')
{
++n_line;
isword = 1;
}
else
{
isword = 1;
}
}
printf("字符数:%d 单词数:%d 行数:%d", n_chara, n_word, n_line);
}

当然,也可以使用K&R里面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#define IN 1
#define OUT 0

int main() {
int c, nl, ns, nw, state;

nl = ns = nw = 0;
state = OUT;
while ((c = getchar()) != EOF) {
++ns;
if (c == '\n')
++nl;
if (c == ' ' || c == '\n' || c == '\t') {
state = OUT;
}
else if (state == OUT)
{
state = IN;
++nw;
}
}
printf("单词数:%d 行数:%d 字符数:%d", ns, nl, nw);
}

数组

接下来来讲讲数组的内容,这里对应的是原文的1.6的部分

首先简单介绍一下数组,数组是一个可以储存变量的容器,你可以往里面填入值

格式如下:

1
ndigit[x] = y;

其中x为你要填入的位置(默认以0开始),y表示这个位置的值

利用这个特性,我们便可以用更聪明的方式来统计东西

首先我们需要明白一个点,在ASCII码中0~9是连一块的

而如果我们将基准定为输入的数字(例如2

此时通过2 - 0,便可以得出位置2(对应数组里面的3号位

那么这样的话,便可以到这样的代码:

1
2
3
4
5
6
7
8
9
for (i = 0; i < 10; ++i) {
// 这里是初始化每个数字的次数
ndigit[i] = 0;
}
while ((c = getchar()) != EOF) {
if (c >= '0' && c <= '9') {
++ndigit[c - '0'];
}
}

首先第一个的for语句其作用为初始化每个数字出现的次数

而下一个while语句便是重点

首先循环体if语句的判定条件限定在了0~9,并且正如上面所讲的一样,判断内的部分为指定位置的值的增加(说白了就是增加次数到指定的位置上)

这样有什么好处呢,这样的话便不用自己去定义位置,使代码更为简洁

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
int main() {
int c, i;
int nwhite, nother;
int ndigit[10];

nwhite = nother = 0;
for (i = 0; i < 10; ++i) {
// 这里是初始化每个数字的次数
ndigit[i] = 0;
}
while ((c = getchar()) != EOF) {
if (c >= '0' && c <= '9') {
++ndigit[c - '0'];
}
else if (c == ' ' || c == '\n' || c == '\t') {
++nwhite;
}
else {
++nother;
}
}
printf("各个数字出现次数为:");
for (i = 0; i < 10; ++i) {
printf("%d", ndigit[i]);
}
printf("空格数:%d 其他字符:%d",nwhite,nother);
}

这里用else语句而不是在一开始就加1的原因是这里为其他字符,而不是所有字符

所以放到最后面来兜底

函数

接下来讲讲函数方面的内容,这里对应书中的1.7 函数

首先是一个简单的引入。在之前的例子中,我们有使用过这些内置的函数:printf()getrchar()putchar()

而这次的目的就是自己编写一个函数,并且成功调用这个函数

任务说明

在Python中,有一个运算符**其作用为进行幂计算

1
print(x**y)

这里变量x代表底数,变量y代表幂,例如:

1
2
3
print(2 ** 3)

# 输出:8(2的三次方)

而在C中,并没有类似的运算符,所以我们可以自己编写一个运算符

思路

首先幂的计算便是n个底数进行相乘,所以这里有个简单的方法就是利用for语句进行多次循环,最后返回计算好的结果

代码实现

通过上面的思路,可以比较轻松的写出下面这个函数,接下来开始逐一介绍

1
2
3
4
5
6
7
8
9
int power(int base, int n) {
int i,p;

p = 1;
for (i = 1; i <= n; ++i) {
p *= base;
}
return p;
}

首先我们可以模拟一下运算的流程,比如说2的3次方

1
2
3
第一次:2
第二次:4
第三次:8

这里其实有个很巧的点,如果n为0的话,那就是2的0次方了,根据for语句条件i <= n才会执行循环体内的内容

此时由于n为0,不满足循环条件,所以并不执行循环

但是很巧的一点是,2的0次方恰好为1

这也是设计的一个巧妙的点


接下来开始说明

首先函数power接受两个值:basen

这里的话base作为底数,n作为幂

由于循环判断条件是i <= n,而循环体内的条件为每次循环都乘底数一遍,直到不循环为止

最后循环结束,输出对应的值

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int main() {
int power(int m, int n);

printf("%d,%d", 2, power(2, 3));
}

int power(int base, int n) {
int i, p;

p = 1;
for (i = 1; i <= n; ++i) {
p *= base;
}
return p;
}

语法笔记

上面为K&R中提供的案例,接下来来讲讲K&R里面的笔记

定义函数

接下来来讲讲定义函数的部分、

首先给出定义函数的结构:

1
2
3
4
5
6
返回的类型 函数的名字(声明参数)
{
声明参数部分

函数语句
}

需要注意的一点是,函数的定义可以在一个文件的任何地方,这跟Python是截然不同的(Python需要先定义才能调用,C可以把定义的部分放到最后面)

函数的参数

每个函数声明的参数仅在这个函数内部生效,不影响其他函数

正因如此,其他函数便可以与之使用同样的参数名

通常把函数定义中出现的参数称为形式参数

而把函数调用中与形式参数对应的值称为实际参数

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int main() {
int power(int m, int n);

printf("%d,%d", 2, power(2, 3));
}

int power(int base, int n) {
int i, p;

p = 1;
for (i = 1; i <= n; ++i) {
p *= base;
}
return p;
}

上面这个例子中,形式参数为basen,实际参数为23

return语句

在调用了函数后,我们要怎么让结果输出出去呢,这时候便可以使用到return语句了

这个语句的效果是返回return后面的表达式,比如上面的例子就返回了p


如果你仔细观察的话,便可以发现main其实也是一个函数,但是这个函数可以省略return语句

而其他函数如果有指定返回值的类型但是没有给出返回值,则属于 “未定义行为”(返回的结果不可以预测)

这里的声明返回值类型指的是这里:

1
2
3
int main(){
;
}

main前面的int便是指明了返回值的类型,

函数原型

在作者的例子中还使用了函数原型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int power(int m, int n);

int main() {
int i;

for (i = 0; i < 10; ++i) {
printf("%d %d %d\n", i, power(2, i), power(-3, i));
}
return 0;
}

int power(int base, int n) {
int i, p;

p = 1;
for (i = 1; i <= n; ++i)
p *= base;
return p;
}

也就是上面这里例子中的int power(int m, int n);

这里表明了函数power有两个int类型的参数,并且返回一个int类型的值

特别需要注意的一点是,函数原型必须与函数的定义和用法一致,否则会出现报错

但函数原型和定义函数的参数名可以不相同,例如在上面这个例子中,函数原型的参数分别为mn

而定义函数的时候为basen

有趣的一点是,函数原型的参数名是可选的,也就是说你可以写成这样:

1
int power(int, int);

不过一般为了说明,不会省略参数名

参数——传值调用

接下来简单讲一下传参的问题

在C语言中,函数内部的所有参数值都是通过值传递的,传递的参数值都被传递到临时变量中,与原来的变量相隔开

正因如此,被调用的函数不能修改外部主调函数的变量值,只能修改自己函数内部的值

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int power(int, int);

int main(){
int c;

c = getchar();
printf("结果为:%d,幂为:%c\n", power(2, c),c);
return 0;
}

int power(int base ,int n){
int p;

for (p = 1; n - '0' > 0; --n) {
p *= base;
}
return p;
}

在上面这个例子中,函数power的参数由c决定,也就是函数内的n

这里可以看到,在执行函数后,原本作为参数传进去的c并没有在外部修改,仍然保持原有的值

当然,也可以让函数修改主调函数的变量,但需要用到指针,这里不展开说明

字符数组

字符数组可以将一个单词的字母储存到这个数组的单个位置里面

接下来我们通过一个例子来说明这一点

这里的例子使用了K&R中的例子:

编写一个程序,使其在结束的时候打印出最长输入的那一行

思路

首先我们先捋一捋思路,这里的关键词为 打印最长输入,既然有最长,那么肯定得用到比较大小,所以必然得用到if语句来对长度进行比较

下一个点是为了打印出最长的输出,所以这里必然得储存输入,所以可能得用到临时变量来储存

很好,来总结一下思路

  • 接受输入
  • 检查是否为最长
  • 如果是最长就储存输入,并且计算其长度用于比较
  • 如果不是最长就跳过换下一个输入
  • 最后结束返回储存的最长输入

难点

在Python中有个函数专门来统计列表的长度:len(),但是在C语言里面则没有类似的函数

这时候我们要思考怎么去编写这样的一个函数

我们不妨可以联想到之前一直用到的一个点:while循环和getchar()可以一个一个字符查看

而又因为换行符\n我们是看不到的,所以就可以用到这个思路:

  • 获取输入
  • 如果不是换行符就长度加一
  • 如果是换行符就返回长度

开始编写

返回长度

首先是实现返回长度的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int getline(char s[], int lim)
{
int c, i;

for (i = 0;(c = getchar()) != EOF && c != '\n';++i)
{
s[i] = c;
}
if (c == '\n')
{
s[i] = c;
++i;
}
s[i] = '\0';
return i;
}

这里s[]是一个临时数组,用于存储输入的值

如果不为EOF或换行符\n的话,则说明是我们想要的内容,所以一个一个加到临时数组里面,同时每加一次,长度i就加一

这时候可能就有人要问了,诶,那这里是干什么的呢?

1
2
3
4
5
if (c == '\n')
{
s[i] = c;
++i;
}

首先让我们思考一下什么情况下我们不会加入到数组:输入为EOF或者\n的时候便不会继续循环

那这里有人就又要问了,那这样子的话为什么不删去for语句里面的判断条件c != '\n'

让我们再次回想一下我们要干什么:嗯,是为了查找输入最长的那一行

那一行的末尾是什么呢?诶那就是换行符\n

这里如果不写上c != '\n'的话,那么循环会继续下去,就不是一行了,而是多行了

所以这里也就是为什么要多这么一个判断的原因

那又有人要问了,这个是干什么的?

1
s[i] = '\0';

这里需要科普的一点是,在C语言中,一个字符串结束的标志是\0

所以这里其实是在说这个字符串结束了,一个简单的收尾工作

这个东西很重要,之后还有个函数依赖于这个特性

接下来让我们思考一下,如果这里输入过多该咋办?所以需要加一个限制条件

限制条件也就是最大长度,这里可以用到传进来的参数lim

所以这里在循环条件里面加一句,只能在这个范围里面执行循环

1
2
3
4
for (i = 0;i < lim - 1 && (c = getchar()) != EOF && c != '\n';++i)
{
s[i] = c;
}

这里可能有疑惑了,诶,为什么这里要- 1

首先让我们提前补充一个点,一个字符串的结尾,一般为\0

这个\0代表着字符串的结束,而这里的- 1的作用是为这个特殊的标志腾出空间

因为这里如果不腾出空间的话,那么如果这个字符串有1000个字符(包括\n),那么最后一个结束的标志(\0)则会因为无法写入而导致报错,这也就是为什么要- 1的原因

复制最大的数组

接下来是复制最大的那个数组,以便在结束的时候可以输出这个最长的数组

首先让我们思考一下要怎么做

首先第一步是读取最长的那个数组

然后一步一步把这个数组复制一遍

诶,那要怎么看是不是复制完了呢?还记得上面的结尾是什么吗

对,这里的判断条件便是!= '\n'

下面是完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int main() {
...
while ((len = getline(line,MAXLINE)) > 0)
if (len > max)
{
max = len;
copy(longest, line);
}
...
}

void copy(char to[], char from[])
{
int i;

i = 0;
while ((to[i] = from[i]) != '\0')
++i;
}

这里顺带把main的一小部分截出来了,为了方便理解

首先可以发现,这里的话to[i]是复制的那个列表,而from[]是原来的列表

对应关系的话是这样子的:

1
2
3
from[] = s[] = line

to[] = longest

之后每次执行循环都会使得数组的位数增加1个单位,直到到达末尾的\n

既然这里截出了main的一小块,那顺带也讲了吧

这是一个比较简单的循环,其核心在于循环的条件((len = getline(line,MAXLINE)) > 0)

这里发现调用了函数getline(),那么这个函数返回的什么呢?

返回过去看看,可以发现返回的是这个数组的长度,所以说这里的目的是检测输入的内容是否大于0,并且赋值给变量len

main

接下来讲讲main

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#define MAXLINE 1000

int main() {
int len;
int max;

char line[MAXLINE];
char longest[MAXLINE];

// 开始初始化
max = 0; //初始化最大值
while ((len = getline(line,MAXLINE)) > 0)
if (len > max)
{
max = len;
copy(longest, line);
}
if (max > 0)
{
printf("%s", longest);
}
return 0;
}

while语句的内容已经在上面讲过了,接下来往下讲if语句

这里的目的是为了判断是否存在这么个最大的行,这里没有一般为没输入就直接退出了

最后输出内容为longest,也就是复制过后的to[]

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#define MAXLINE 1000

int getline(char line[], int maxline);
void copy(char to[], char from[]);

int main() {
int len;
int max;

char line[MAXLINE];
char longest[MAXLINE];

// 开始初始化
max = 0; //初始化最大值
while ((len = getline(line,MAXLINE)) > 0)
if (len > max)
{
max = len;
copy(longest, line);
}
if (max > 0)
{
printf("%s", longest);
}
return 0;
}

int getline(char s[], int lim)
{
int c, i;

for (i = 0;
i < lim - 1 && (c = getchar()) != EOF && c != '\n';
++i
)
{
s[i] = c;
}
if (c == '\n')
{
s[i] = c;
++i;
}
s[i] = '\0';
return i;
}

void copy(char to[], char from[])
{
int i;

i = 0;
while ((to[i] = from[i]) != '\0')
++i;
}

这里的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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int getline(void)
{
int c, i;
extern char line[];

for (i = 0;
i < MAXLINE - 1 && (c = getchar()) != EOF && c != '\n';
++i
)
{
line[i] = c;
}
if (c == '\n')
{
line[i] = c;
++i;
}
line[i] = '\0';
return i;
}

可以看到这里使用了外部变量line[]

MAXLINE在开头的#define就定义过了,编译的时候会自动替换成后面的1000

这里需要注意的一点是,getline()的括号内部写的是void

这是因为在 ANSI C中,如果要声明空参数表,则必须使用关键字void进行显式声明

接下来是copy()

1
2
3
4
5
6
7
8
9
void copy(void)
{
int i;
extern char line[], longest[];

i = 0;
while ((longest[i] = line[i]) != '\0')
++i;
}

通过观察main()可以知道,原先的原先数组from[]实际可以替换为line[],而输出的数组可以用到longest[]

longest[]是在一开始就定义的外部变量,可以不借助参数而选择直接写入函数内部

最后是完整的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#define MAXLINE 1000

int max;
int getline(void);
void copy(void);
char line[MAXLINE];
char longest[MAXLINE];

int main() {
int len;
extern int max;
extern char longest[MAXLINE];

max = 0;
while ((len = getline()) > 0)
if (len > max)
{
max = len;
copy();
}
if (max > 0)
{
printf("%s", longest);
}
return 0;
}

int getline(void)
{
int c, i;
extern char line[];

for (i = 0;
i < MAXLINE - 1 && (c = getchar()) != EOF && c != '\n';
++i
)
{
line[i] = c;
}
if (c == '\n')
{
line[i] = c;
++i;
}
line[i] = '\0';
return i;
}

void copy(void)
{
int i;
extern char line[], longest[];

i = 0;
while ((longest[i] = line[i]) != '\0')
++i;
}

需要说明的一点是,虽然外部变量很好用,但是如果过度使用的可能会导致数据之间的关系变得不清,并且如果将外部变量写入程序内部,会不传入参数进函数而导致函数失去了通用性

补充内容

接下来补充几个小点

上文在讲自动变量的时候提及了以下内容:

如果自动变量没有赋值,那么其中存放的就是无效值
而如果外部变量没有初始化的话,那么编译器会自动初始化为0

此外还有有关定义声明的区别

定义指的是:表示创建变量或者分配存储单元

而声明指的是:说明变量的性质,但并不分配存储单元

举个例子:

在外部变量中,定义如下:

1
int max;

而声明如下:

1
extern int max;

定义只能定义一次,而声明可以声明无限次