C语言-输入函数与缓冲区
C语言-输入函数与缓冲区

C语言-输入函数与缓冲区

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代表个人编号,写出输入函数表达式

  1. #include <stdio.h>
  2. #include <stdlib.h> 
  3.  
  4. int main()
  5. {
  6. 	char year[3], academy[3], pclass[3], pernum[3];
  7. 	int c;
  8.  
  9. 	scanf("%2s%2s%2s%2s", &year, &academy, &pclass, &pernum);
  10. 	printf("年级为%s,学院为%s,班级为%s,个人编号为%s\n", year, academy, pclass, pernum);
  11.  
  12. 	c=atoi(academy);
  13. 	printf("这是数字%d", c);
  14.  
  15. 	return 0;
  16. }

因为0放在一个大于0的整数前是没有意义的,所以以整数型输入,再输出时并不会显示02前面的0,因此我们以字符串格式输入输出,后续有必要可以再调用<stdlib.h>库函数中的atoi()函数进行字符串到整数的强制转化

输入20010203运行结果如下

数据分类之后可以用来构造字典

2.2.输入数据流分隔

scanf函数会根据格式字符的含义从输入中取得数据,当输入的数据类型与格式字符要求不同时,就会认为这一个%输入存储结束,进入下一个%输入存储

  1. #include <stdio.h>
  2.  
  3. int main()
  4. {
  5. 	int a;
  6. 	float b;
  7. 	char c;
  8.  
  9. 	scanf("%d%c%f", &a, &c, &b);
  10. 	printf("%d\n%c\n%.2f\n", a, c, b);
  11.  
  12. 	return 0;
  13. }

这里我们输入123×456.789,即一串整型+字符型+浮点型的数据组合

可以看到scanf是会自动结束不同类型的输入存储并开始下一段存储

2.3.输入项地址列表

然后我们来谈谈输入项地址列表,输入项地址列表,一般是取地址符号& + 变量,多个变量之间用”,”隔开,每个变量前都要加上&

取地址符&的含义取出后面变量的地址,scanf的作用就是将输入的数据,根据地址存到地址所指向的空间内,那地址有什么用呢?地址的作用就是指向变量,告诉你这个变量在哪,地址和变量是两个完全不同的东西,地址是指向变量的一系列数,变量是存放在存储空间内的数

举个栗子,假如你去故宫玩,进到故宫了(输入),你想去看看某某文物(变量),这个文物放在某一个房间(存储空间)里,但是故宫有几千间,如果一间一间找,或者随机找,那么会浪费很多时间,这时候如果你有一张地图(地址),地图上的路指向了你想要去看的某个房间的某个文物,你所要做的就是跟着地图走就能通过最短路径直接找到房间,然后进到这个房间就可以成功看到了文物了(输入的值存到存储空间,输入结束)。

其实scanf函数的工作流程也就是这样,输入->指向->存储

2.4.注意事项

1.如果在多个输入控制符%之间加入了其他字符,在输入时就要输入相应的字符,如加入逗号

  1. #include <stdio.h>
  2.  
  3. int main()
  4. {
  5. 	int a, b;
  6.  
  7. 	scanf("%d,%d", &a, &b);
  8. 	printf("a的值为%d\nb的值为%d", a, b); 
  9.  
  10.  
  11. 	return 0;
  12. }

可以看到,我们输入时没有加入逗号,b就读到了错误的值,并且,全角和半角的逗号不同也会报错,所以不建议在不必要时加入除空格外的其他字符

2.scanf 中 %d 只识别“十进制整数”。对 %d 而言,空格、回车、Tab 键都是区分数据与数据之间的分隔符。当 scanf 进入缓冲区中取数据的时候,如果 %d 遇到空格、回车、Tab 键,那么它并不取用,而是跳过继续往后取后面的数据,直到取到“十进制整数”为止。对于被跳过和取出的数据,系统会将它从缓冲区中释放掉。未被跳过或取出的数据,系统会将它一直放在缓冲区中,直到下一个 scanf 来获取。


3.gets函数

gets函数可以用来输入字符串,它能从输入流中读取字符串,直到接收到换行符停止,换行符不会最为被读取的内容,而是会被替换为”\0″空字符,来结束字符串

gets函数可以无限读取,不会判断上限是多少,所以如果输入的值大于已经分配的空间,就会造成溢出,如果溢出,多出来的字符将被写入到堆栈中,这就覆盖了堆栈原先的内容,破坏一个或多个不相关变量的值。

但和scanf函数相比,gets函数在输入时不会被空格干扰,在scanf函数中,空格被认为是分隔符,意味着字符串的结束,而在gets函数中,只会以回车作为结束的标志。

  1. #include <stdio.h>
  2.  
  3. int main()
  4. {
  5. 	char a[10], b[10];
  6.  
  7. 	gets(a);
  8. 	gets(b);
  9. 	printf("a为%s\nb为%s", a, b); 
  10.  
  11.  
  12. 	return 0;
  13. }

输入a空格b空格,c空格d空格,可以发现空格可以输入在字符串中,并且空格并没有影响后续的输入,即没有存入b中


4.缓冲区读取问题

为什么我会说gets输入函数在输入时上一个空格没有存入下一个变量中呢,其实这些输入的数据都放在缓冲区内,但由于使用的输入函数的不同,会导致特殊符号的读取影响多个变量之间的存储。我们先来说说缓冲区,再说这个问题会更好理解一些

缓冲区又叫缓存,是内存空间的一部分,即在内存空间中预留出来的一定大小的空间,可以用来缓冲输入或者输出的数据,所以叫做缓冲区

那么为什么需要缓冲呢?这是因为先把数据放在缓冲区,计算机在缓冲区读取数据,读完了再去读磁盘,可以减少磁盘I/O次数,又可以节省读取花费的时间,这就像是把零钱放在钱包里,小花销开支零钱直接在钱包操作就行,大面额操作再去银行,钱从钱包拿出来远比从银行取要快

在缓冲区中,遵循先进先出的规则,我们用输入函数输入的数据就是被存放在缓冲区的,读取的时候按输入顺序读取,当完成一个格式控制符的读取时,缓冲区剩余的数据会留给下一个格式控制符的读取

这里我们可以通过程序验证

  1. #include <stdio.h>
  2.  
  3. int main()
  4. {
  5. 	char c1[10], c2[10];
  6.  
  7. 	scanf("%2s%s", c1, c2);
  8. 	printf("c1为%s\nc2为%s", c1, c2);
  9.  
  10. 	return 0;
  11. }

我们让前一个控制符只读取两个长度的字符,输入abcdef

可以看到只输入一组数据,读取一次ab后剩下的字符被第二个格式控制符读取了

举个栗子,我们把缓冲区比作一辆公交车,把一次输入的操作看作上车,把一次输出的操作(把数据从缓存区拿出来给指向的变量)看作下车,把上车的每个人看作一个长度的数据,那么先上车的人往里走,后上车的人排在先上车的人都后面,前面的人下车之后才轮到后面的下车(理想模型,排除后面的人先到站下车的可能),每一个站点都有相应的下车人数(控制格式符读取的数据长度),那么缓冲区的运作过程就像是上下车的过程一样,先进先出,有序排列。

理解了缓冲区的原理,我们再来谈谈输入函数中的空格问题

先来试试整型变量,scanf两个输入控制符号%d之间不加空格

  1. #include <stdio.h>
  2.  
  3. int main()
  4. {
  5.     int c1,c2;
  6.  
  7.     scanf("%d%d", &c1, &c2);
  8.     printf("c1为%d c2为%d\n", c1, c2);
  9.  
  10.     scanf("%d%d", &c1, &c2);
  11.     printf("c1为%d c2为%d\n", c1, c2);
  12.  
  13.     return 0;
  14. }

两次都输入1空格2

似乎没有问题,那我们把第二个输入函数换成字符输入%c试试

  1. #include <stdio.h>
  2.  
  3. int main()
  4. {
  5.     int c1,c2;
  6.  
  7.     scanf("%d%d", &c1, &c2);
  8.     printf("c1为%d c2为%d\n", c1, c2);
  9.  
  10.     scanf("%c%c", &c1, &c2);
  11.     printf("c1为%d c2为%d\n", c1, c2);
  12.  
  13.     return 0;
  14. }
回车的ASCII值为10,空格的ASCII值为32,字符1的ASCII值为49

这里就可以看出问题了,%d只识别十进制整数,对 %d 而言,空格、回车、Tab 键都是区分数据与数据的分隔符。当 scanf 进入缓冲区中取数据的时候,如果 %d 遇到空格、回车、Tab 键,那么它并不取用,而是跳过继续往后取后面的数据,直到取到“十进制整数”为止。对于被跳过和取出的数据,系统会将它从缓冲区中释放掉。未被跳过或取出的数据,系统会将它一直放在缓冲区中,直到下一个 scanf 来获取。

也就是说,对于只有整型的读取,空格并不会产生影响,scanf根本不会取读空格,最后留下的是回车符,回车符也不会对整型的读取产生影响,那么空格有什么用呢

我们再来看看字符型变量

  1. #include <stdio.h>
  2.  
  3. int main()
  4. {
  5.     char c1,c2;
  6.  
  7.     scanf("%c%c", &c1, &c2);
  8.     printf("c1为%d c2为%d\n", c1, c2);
  9.  
  10.     scanf("%c%c", &c1, &c2);
  11.     printf("c1为%d c2为%d\n", c1, c2);
  12.  
  13.     return 0;
  14. }

两次scanf中都没有空格,我们尝试输入a, b(ASCII值分别为97,98)

我们只输入了一次a,b,却有两次输出,没有进行第二次输入

仔细想想,第一次输入的时候我们按了四个键,分别是a,空格,b,回车,而空格的ASCII值为32,回车的ASCII值为10,正好对应,所以空格会被当做字符读取。如果缓冲区内已经存在了变量,那么后续的输入函数就不会执行,而直接输出

接下来我们每个输入之间加入空格

  1. #include <stdio.h>
  2.  
  3. int main()
  4. {
  5.     char c1,c2;
  6.  
  7.     scanf("%c %c", &c1, &c2);
  8.     printf("c1为%d c2为%d\n", c1, c2);
  9.  
  10.     scanf("%c %c", &c1, &c2);
  11.     printf("c1为%d c2为%d\n", c1, c2);
  12.  
  13.     return 0;
  14. }

尝试输入a,b

可以看到第一次的值正确了,而第二次的值还是不正确,仍然有回车遗留在缓冲区内,读取了回车才读取a的值

我们接下来尝试在第二个控制输入符和对应的b之后加空格

  1. #include <stdio.h>
  2.  
  3. int main()
  4. {
  5.     char c1,c2;
  6.  
  7.     scanf("%c %c ", &c1, &c2);
  8.     printf("c1为%d c2为%d\n", c1, c2);
  9.  
  10.     scanf("%c %c", &c1, &c2);
  11.     printf("c1为%d c2为%d\n", c1, c2);
  12.  
  13.     return 0;
  14. }

我们在第一次输入a,空格,b,空格,之后并没有立马打印,我们继续输入a,空格,b,结果发现两次打印一起输出,并且值是正确的

显然,这是因为我们第一次输入的回车被释放了,但连续两个回车也没有用,而是一个回车加上一个变量再加上一个回车,才会进行第一次打印,再输入一个变量,就会进行第二次打印

我们尝试把空格加在第二个输入函数的开头

  1. #include <stdio.h>
  2.  
  3. int main()
  4. {
  5.     char c1,c2;
  6.  
  7.     scanf("%c %c", &c1, &c2);
  8.     printf("c1为%d c2为%d\n", c1, c2);
  9.  
  10.     scanf(" %c %c", &c1, &c2);
  11.     printf("c1为%d c2为%d\n", c1, c2);
  12.  
  13.     return 0;
  14. }

可以看到,这样就能正常输出了,第二次输入时scanf第一个格式控制符前面的空格读取了一个结束字符,将前面的回车释放掉,这时缓冲区内没有其他数据,就不会影响后面的数据数据的输入读取了

所以空格可以理解为读取一个结束字符然后丢掉,对于普通字符没有影响

总结一下,空格在整型输入时无关紧要,对于字符型输入,scanf容易出错,可以采用gets函数输入,更加安全简便