1.引言
最近在重新学习C语言,发现有很多初学小伙伴在写scanf时一直不喜欢在每个输入间加空格,而我平时都是会加上空格的,虽然对于单次输入的程序没有问题,那么多次输入的程序还能正常运行吗?抱着刨根问底的精神,我查阅了很多资料,然后从空格到底应不应该加这个小问题说开来,尝试从底层来谈谈输入函数的机制。
2.scanf函数
在讲缓冲区之前我们先来讲讲输入函数,因为有输入,缓冲区才会有读写,所以缓冲区的使用跟输入函数的关系是非常密切的
scanf函数往往是我们学到第一个输入函数,也是用的最多函数,所以学好scanf函数是非常有必要的
scanf函数是标准I/O库中的函数,使用时要先调用<stdio.h>库函数,这里插一句,我也是重学的时候听老师讲才知道<stdio.h>的意思是standard input/output . head, 标准输入/输出库函数头文件,以前还偶尔写成studio,明白含义之后就再也不会写错了。
基本用法:
scanf("格式控制字符串",输入项地址列表);
2.1.格式控制字符串
我们先来说说格式控制字符串,其中格式控制字符串有如下几种常用输入方法:
格式说明符 | 格式 |
%d或%i | 有符号十进制整型 |
%c | 接收单个字符 |
%s | 接收字符串(变量后面不加取址符) |
%f | 浮点数 |
%lf | 长精度浮点数 |
这里要说明一点,%d前面是可以指定域宽的(但不能指定精度,即不能加”.”),如%5d,scanf就会只读取前五个单位长度的数据,那么后面没读取的呢?没读取的数据会被放在缓冲区内(这一点我会在后文详细解释),并不会消失。
指定域宽有什么用呢?当然有用了,这就意味这我们可以在一步的输入中,同时做到对输入数的分割
比如我们现在输入一个学号,20010203,20代表入学年号,01代表学院,02代表班级,03代表个人编号,写出输入函数表达式
#include <stdio.h>
#include <stdlib.h>
int main()
{
char year[3], academy[3], pclass[3], pernum[3];
int c;
scanf("%2s%2s%2s%2s", &year, &academy, &pclass, &pernum);
printf("年级为%s,学院为%s,班级为%s,个人编号为%s\n", year, academy, pclass, pernum);
c=atoi(academy);
printf("这是数字%d", c);
return 0;
}
因为0放在一个大于0的整数前是没有意义的,所以以整数型输入,再输出时并不会显示02前面的0,因此我们以字符串格式输入输出,后续有必要可以再调用<stdlib.h>库函数中的atoi()函数进行字符串到整数的强制转化
输入20010203运行结果如下

数据分类之后可以用来构造字典
2.2.输入数据流分隔
scanf函数会根据格式字符的含义从输入中取得数据,当输入的数据类型与格式字符要求不同时,就会认为这一个%输入存储结束,进入下一个%输入存储
#include <stdio.h>
int main()
{
int a;
float b;
char c;
scanf("%d%c%f", &a, &c, &b);
printf("%d\n%c\n%.2f\n", a, c, b);
return 0;
}
这里我们输入123×456.789,即一串整型+字符型+浮点型的数据组合

可以看到scanf是会自动结束不同类型的输入存储并开始下一段存储
2.3.输入项地址列表
然后我们来谈谈输入项地址列表,输入项地址列表,一般是取地址符号& + 变量,多个变量之间用”,”隔开,每个变量前都要加上&
取地址符&的含义取出后面变量的地址,scanf的作用就是将输入的数据,根据地址存到地址所指向的空间内,那地址有什么用呢?地址的作用就是指向变量,告诉你这个变量在哪,地址和变量是两个完全不同的东西,地址是指向变量的一系列数,变量是存放在存储空间内的数
举个栗子,假如你去故宫玩,进到故宫了(输入),你想去看看某某文物(变量),这个文物放在某一个房间(存储空间)里,但是故宫有几千间,如果一间一间找,或者随机找,那么会浪费很多时间,这时候如果你有一张地图(地址),地图上的路指向了你想要去看的某个房间的某个文物,你所要做的就是跟着地图走就能通过最短路径直接找到房间,然后进到这个房间就可以成功看到了文物了(输入的值存到存储空间,输入结束)。
其实scanf函数的工作流程也就是这样,输入->指向->存储。
2.4.注意事项
1.如果在多个输入控制符%之间加入了其他字符,在输入时就要输入相应的字符,如加入逗号
#include <stdio.h>
int main()
{
int a, b;
scanf("%d,%d", &a, &b);
printf("a的值为%d\nb的值为%d", a, b);
return 0;
}

可以看到,我们输入时没有加入逗号,b就读到了错误的值,并且,全角和半角的逗号不同也会报错,所以不建议在不必要时加入除空格外的其他字符
2.scanf 中 %d 只识别“十进制整数”。对 %d 而言,空格、回车、Tab 键都是区分数据与数据之间的分隔符。当 scanf 进入缓冲区中取数据的时候,如果 %d 遇到空格、回车、Tab 键,那么它并不取用,而是跳过继续往后取后面的数据,直到取到“十进制整数”为止。对于被跳过和取出的数据,系统会将它从缓冲区中释放掉。未被跳过或取出的数据,系统会将它一直放在缓冲区中,直到下一个 scanf 来获取。
3.gets函数
gets函数可以用来输入字符串,它能从输入流中读取字符串,直到接收到换行符停止,换行符不会最为被读取的内容,而是会被替换为”\0″空字符,来结束字符串
gets函数可以无限读取,不会判断上限是多少,所以如果输入的值大于已经分配的空间,就会造成溢出,如果溢出,多出来的字符将被写入到堆栈中,这就覆盖了堆栈原先的内容,破坏一个或多个不相关变量的值。
但和scanf函数相比,gets函数在输入时不会被空格干扰,在scanf函数中,空格被认为是分隔符,意味着字符串的结束,而在gets函数中,只会以回车作为结束的标志。
#include <stdio.h>
int main()
{
char a[10], b[10];
gets(a);
gets(b);
printf("a为%s\nb为%s", a, b);
return 0;
}

输入a空格b空格,c空格d空格,可以发现空格可以输入在字符串中,并且空格并没有影响后续的输入,即没有存入b中
4.缓冲区读取问题
为什么我会说gets输入函数在输入时上一个空格没有存入下一个变量中呢,其实这些输入的数据都放在缓冲区内,但由于使用的输入函数的不同,会导致特殊符号的读取影响多个变量之间的存储。我们先来说说缓冲区,再说这个问题会更好理解一些
缓冲区又叫缓存,是内存空间的一部分,即在内存空间中预留出来的一定大小的空间,可以用来缓冲输入或者输出的数据,所以叫做缓冲区
那么为什么需要缓冲呢?这是因为先把数据放在缓冲区,计算机在缓冲区读取数据,读完了再去读磁盘,可以减少磁盘I/O次数,又可以节省读取花费的时间,这就像是把零钱放在钱包里,小花销开支零钱直接在钱包操作就行,大面额操作再去银行,钱从钱包拿出来远比从银行取要快
在缓冲区中,遵循先进先出的规则,我们用输入函数输入的数据就是被存放在缓冲区的,读取的时候按输入顺序读取,当完成一个格式控制符的读取时,缓冲区剩余的数据会留给下一个格式控制符的读取
这里我们可以通过程序验证
#include <stdio.h>
int main()
{
char c1[10], c2[10];
scanf("%2s%s", c1, c2);
printf("c1为%s\nc2为%s", c1, c2);
return 0;
}
我们让前一个控制符只读取两个长度的字符,输入abcdef

可以看到只输入一组数据,读取一次ab后剩下的字符被第二个格式控制符读取了
举个栗子,我们把缓冲区比作一辆公交车,把一次输入的操作看作上车,把一次输出的操作(把数据从缓存区拿出来给指向的变量)看作下车,把上车的每个人看作一个长度的数据,那么先上车的人往里走,后上车的人排在先上车的人都后面,前面的人下车之后才轮到后面的下车(理想模型,排除后面的人先到站下车的可能),每一个站点都有相应的下车人数(控制格式符读取的数据长度),那么缓冲区的运作过程就像是上下车的过程一样,先进先出,有序排列。
理解了缓冲区的原理,我们再来谈谈输入函数中的空格问题
先来试试整型变量,scanf两个输入控制符号%d之间不加空格
#include <stdio.h>
int main()
{
int c1,c2;
scanf("%d%d", &c1, &c2);
printf("c1为%d c2为%d\n", c1, c2);
scanf("%d%d", &c1, &c2);
printf("c1为%d c2为%d\n", c1, c2);
return 0;
}
两次都输入1空格2

似乎没有问题,那我们把第二个输入函数换成字符输入%c试试
#include <stdio.h>
int main()
{
int c1,c2;
scanf("%d%d", &c1, &c2);
printf("c1为%d c2为%d\n", c1, c2);
scanf("%c%c", &c1, &c2);
printf("c1为%d c2为%d\n", c1, c2);
return 0;
}

这里就可以看出问题了,%d只识别十进制整数,对 %d 而言,空格、回车、Tab 键都是区分数据与数据的分隔符。当 scanf 进入缓冲区中取数据的时候,如果 %d 遇到空格、回车、Tab 键,那么它并不取用,而是跳过继续往后取后面的数据,直到取到“十进制整数”为止。对于被跳过和取出的数据,系统会将它从缓冲区中释放掉。未被跳过或取出的数据,系统会将它一直放在缓冲区中,直到下一个 scanf 来获取。
也就是说,对于只有整型的读取,空格并不会产生影响,scanf根本不会取读空格,最后留下的是回车符,回车符也不会对整型的读取产生影响,那么空格有什么用呢
我们再来看看字符型变量
#include <stdio.h>
int main()
{
char c1,c2;
scanf("%c%c", &c1, &c2);
printf("c1为%d c2为%d\n", c1, c2);
scanf("%c%c", &c1, &c2);
printf("c1为%d c2为%d\n", c1, c2);
return 0;
}
两次scanf中都没有空格,我们尝试输入a, b(ASCII值分别为97,98)

我们只输入了一次a,b,却有两次输出,没有进行第二次输入
仔细想想,第一次输入的时候我们按了四个键,分别是a,空格,b,回车,而空格的ASCII值为32,回车的ASCII值为10,正好对应,所以空格会被当做字符读取。如果缓冲区内已经存在了变量,那么后续的输入函数就不会执行,而直接输出
接下来我们每个输入之间加入空格
#include <stdio.h>
int main()
{
char c1,c2;
scanf("%c %c", &c1, &c2);
printf("c1为%d c2为%d\n", c1, c2);
scanf("%c %c", &c1, &c2);
printf("c1为%d c2为%d\n", c1, c2);
return 0;
}
尝试输入a,b

可以看到第一次的值正确了,而第二次的值还是不正确,仍然有回车遗留在缓冲区内,读取了回车才读取a的值
我们接下来尝试在第二个控制输入符和对应的b之后加空格
#include <stdio.h>
int main()
{
char c1,c2;
scanf("%c %c ", &c1, &c2);
printf("c1为%d c2为%d\n", c1, c2);
scanf("%c %c", &c1, &c2);
printf("c1为%d c2为%d\n", c1, c2);
return 0;
}

我们在第一次输入a,空格,b,空格,之后并没有立马打印,我们继续输入a,空格,b,结果发现两次打印一起输出,并且值是正确的
显然,这是因为我们第一次输入的回车被释放了,但连续两个回车也没有用,而是一个回车加上一个变量再加上一个回车,才会进行第一次打印,再输入一个变量,就会进行第二次打印
我们尝试把空格加在第二个输入函数的开头
#include <stdio.h>
int main()
{
char c1,c2;
scanf("%c %c", &c1, &c2);
printf("c1为%d c2为%d\n", c1, c2);
scanf(" %c %c", &c1, &c2);
printf("c1为%d c2为%d\n", c1, c2);
return 0;
}

可以看到,这样就能正常输出了,第二次输入时scanf第一个格式控制符前面的空格读取了一个结束字符,将前面的回车释放掉,这时缓冲区内没有其他数据,就不会影响后面的数据数据的输入读取了
所以空格可以理解为读取一个结束字符然后丢掉,对于普通字符没有影响
总结一下,空格在整型输入时无关紧要,对于字符型输入,scanf容易出错,可以采用gets函数输入,更加安全简便