标准输入输出你吃透了吗?

  • 了解标准输入输出
  • printf()
  • scanf()
  • getchar()和putchar()
  • gets()和puts()
  • 其它IO函数

了解标准输入输出

所谓标准输入输出,我们先理解为通过控制台来输入和显示内容,只要是个操作系统都会有控制台,这是最基本的人机交互方式,后面会详细讲解标准输入输出的概念。标准输入输出是C语言初学者第一个要越过的门槛,特别是printf()和scanf()这两个格式化输入输出函数,由于格式定义复杂导致很多人望而却步,其实标准输入输出函数并不是C语言定义的标准,而是stdio库中定义的函数,每个编译器实现不尽相同,但差别不大,掌握它们之后再学习文件的输入输出就很简单了,它们的思路是一样的。

printf()

对于格式化输入输出来说,printf()相对简单,因为没有涉及底层硬件,可以将焦点放在格式定义上,下面一起来看看printf()函数的定义:
printf(“格式”, 参数1,参数2…)
如果没有格式控制符,printf()直接输出内容,例如:printf(“Hello World\n”),在格式中使用%开头的字母引用后面的变量,称为格式符,格式符是格式输入输出函数的核心,首先来看看整数:
%d十进制整数 %o八进制整数 %x十六进制整数 %u无符号整数
printf()并未提供八进制和十六进制的有符号输出,也就是说不同进制都是无符号的,另外short用%hd表示,long用%ld表示,long long用%lld表示 long double用lf%表示,如果类型不匹配printf()会自动进行进行类型转换,转换失败会报错。通常用%d来输出short和long不会导致错误,但是会出现警告。%f可以表示单精度和双精度浮点,因为printf()会设法输出足够的长度,不过养成类型严格匹配是一个好习惯。浮点默认有效数字为单精度7位,如果是双精度值会被四舍五入,printf()未提供单独的双精度类型,使用"%lf","%lg"输出精度仍然为7位,但可以通过定义浮点精度解决,格式为%m.nf,m为整数位数,n为小数位数,由于双精度值小数位数不会超过15位,整数不会超过20位,因此20.15%f足以表示双精度。现在我们使用下面代码输出不同的数值类型:

#include<stdio.h>int main()
{short a=1;int b=2;long c=3;unsigned d=4;double e=3.141592654;printf("%hd,%d,%ld,%u,%f,%1.2f,%20.15f",a,b,c,d,e,e,e);return 0;
}

bool是新的类型,之前printf()并未定义,因此使用%d输出,ture输出1,false输出0,下面代码输出bool类型的数值:

#include<stdio.h>
#include<stdbool.h>int main()
{bool a=true;bool b=false;printf("a=%d,b=%d",a,b);return 0;
}

字符类型使用%c,字符串类型使用%s,使用下面代码测试输出字符和字符串:

#include<stdio.h>
#include<stdbool.h>int main()
{char a='a', b='b', c='c';char s[10]="Hello";printf("%c%c%c %s",a,b,c,s);return 0;
}

内存地址通常用十六进制表示,而且长度相同,因此printf()提供了一个新的格式符”%p”,它使用固定的长度输出内存地址,这与某些十六进制文本编辑器显示的结果相同,请看下面的代码:

#include <stdio.h>int a=1;
int main()
{printf("%x\n",&a);printf("%p",&a);return 0;
}

显示结果为:
403010
0000000000403010
通过%p可以看出是64位系统下的内存地址,因为64位系统使用8个字节标记内存地址,在十六进制中FF值为256,因此两位数表示一个字节,8字节一共16位数字。其实这里看到的地址也是虚拟地址,并不等于数据在物理内存中的真实地址,虚拟内存地址是现代计算机因内存管理的需要才提出的概念,一个程序的内存实际上由多个物理内存碎片组成,还包括在磁盘中的虚拟内存,虚拟内存使得应用程序认为它拥有连续的内存空间,这样有助于代码调试。

ANSI C确定了最基本的格式控制符,C99和C11在此基础上增加了一些,所有格式符如下表:
所有数据修饰符如下表:

可以看到,printf()不能输出二进制,通过数字指定的宽度是最小宽度而不是绝对宽度,因此我们可以使用printf(“%06x”, 0x00FF);输出一个字节的十六进制整数,但是不能输出二进制整数,二进制必须使用itoa()转化为字符串输出。printf()具有返回值,如果成功输出返回输出字符个数,如果输出错误会返回一个负值。

scanf()

scanf()比printf()难掌握是因为它要与键盘进行交互,而且涉及到键盘缓冲区的硬件知识,还有按变量地址传入的方式涉及到指针,控制台均以字符串形式传入,因此又要将字符串转换为对应的数据类型。我们首先看看scanf()的格式,再从最简单的问题入手。scanf()函数的格式为:
scanf (“格式”, 地址1,地址2…)
首先scanf()要求传入变量地址而不是变量名,为什么呢?这是因为与printf()不同的是scanf()是赋值操作,也就是需要修改变量的值,而变量可能是数值也可能是字符串,按值传递还是按引用传递不确定,因此统一传入地址。输入整型写为:scanf(“%d”, &a); 输入字符串为:scanf(%s, arr);

第二个问题是内容匹配,在scanf()中输入的格式要和定义的格式完全一致,例如scanf(“a=%d”,&a),必须输入a=1才行,否则会导致scanf()获取不到正确信息进入死循环或宣告失败。当有多个变量需要赋值时,当匹配第一个格式符遇到空格、制表符这样的间隔符或者因为类型不同匹配失败后会继续给下一个匹配,如果缓冲区已没有数据则要求用户继续输入而不退出。例如scanf(“%d%d%d”, &a,&b,&c)可以通过输入1 2 3或1 2 3等来连续赋值,当匹配第一个%d时遇到空格会导致匹配终止,接着匹配下一个%d,在匹配下一个整数时如果遇到空格会自动忽略,直到读取到数字。这里不能输入1,2,3,因为逗号被看作非法字符,当给a赋值完后遇到逗号会停止匹配,而转入下一个%d的匹配,而匹配下一个%d时第一个读取的是逗号,这个非法字符不会像空格那样被忽略,又会导致匹配失败而进入下一个匹配,直到找不到匹配的内容宣告失败,结果只给a进行了赋值操作。如果你只输入了1后回车,当a赋值完后发现缓冲区已没有数据,scanf()会继续让用户输入,当输入2后开始匹配第二个%d,此时键入的回车会被当做空白忽略而正确读到数字2,同样接着输入3可以完成所有的赋值操作,因此输入多个数值时可以使用空白也可以使用回车作为空白间隔符,如果你一定要使用逗号作为间隔符需要改为:scanf(“%d%,d,%d”, &a,&b,&c)。

第三个问题是格式转换问题,标准输入输出操作的都是字符,控制台将所有类型转化为字符后进行输出,键盘键入的内容也是字符,因此在匹配类型的过程中本身就存在数据类型转换。scanf()可以识别包含回车键在内的所有字符,空格、制表符和回车会被当做空白间隔符,printf()是将所有类型转换为字符输出,scanf()是将字符解析为数值,而将字符转化为数值相对困难,出错的机率更高。例如要求输入整数但你输入一个小数,小数点会被当做非法字符导致匹配结束。因此scanf()不仅要求输入内容的格式与给出的格式完全相同,还要求数据类型、甚至小数精度都严格匹配,比如short必须使用%hd,长整数必须使用%ld,双精度必须使用%lf,地址必须使用%p。即便这样,也可能产生意外结果,那就是对于字符串输入,可能你认为输入一个带空格的字符串是一件正常的事情,但却和你想象的不同,scanf()遇到空格就认为字符串输入结束了,因此scanf()是不能接受带空格的字符串的。

最后一个问题是缓冲区,这个问题最频繁也最复杂,因为它涉及到硬件的工作原理,如果不明白缓冲区运行机制,随时会被一些莫名其妙的问题缠住,要解答这个问题,我们只能从硬件知识入手。缓冲区是为解决输入输出速度不对等的性能问题而设置的区域,由于不同的硬件运行速度不同,并且有些硬件访问需要等待的时间开销很大,响应缓慢,缓冲区既可以改善硬件之间的速度不对等问题,又可以减少硬件的访问次数,还可以实现异步工作。速度不对等的交流比较典型的是内存和硬盘,硬盘读写速度远远小于内存,特别是机械硬盘,平时没有读写时处于睡眠状态,当需要访问时要经过请求访问、电机加速、磁头读写、再由主板南桥芯片负责将数据传输到内存等一系列复杂的操作,可以直接导致程序卡顿,例如将休眠的硬盘唤醒读取数据时非常明显。如果在内存中开辟一个缓冲区,一次读取多个数据,由于后续的数据已经在缓冲区,就不必再从硬盘取出了,直接从缓冲区取出,解决了响应慢的问题,这就是所谓的预读。如果需要写入数据,将内容先写入缓冲区,等待缓冲区填满再一次性写入硬盘,这样可以大大减少写入次数。一些读写异常缓慢的硬件干脆采取异步传输,比如打印机,它依赖缓冲区储存临时数据,下载和打印时cpu可以做其它事情从而减少等待时间,这些异步硬件是不会造成计算机卡顿的。现在的计算机硬件,如显卡、声卡、网卡等几乎都会在内存中开辟自己的缓冲区,硬件内部也有缓冲区,而且还将缓冲区分级,例如cpu、硬盘内部都按照速度不同分为一二三级缓存,它们在硬件架构内部自成体系。在工作时硬件内部的缓冲区对接内存中的缓冲区,大大降低了硬件访问次数,缓解了输入输出不对等的问题。我们再来看看软件,其实软件的运行也离不开缓冲区,抛开操作系统在底层给我们创造的缓冲区,我们自行编写软件时也会将频繁读写的数据放到内存中,或者将可能用到数据提前从硬盘取出,或者在程序空闲时将缓冲区的数据写入硬盘,这么一来缓冲区就分为硬件缓存、系统缓存和应用缓存,随处可见。在C语言中缓冲区也频繁出现,很多表示内存地址的变量都使用buff这样的名称,它表示在内存中创建一段缓冲区。

了解缓冲区的作用后我们将焦点移到标准输入输出,标准输入输出只是一个概念,指通过缺省设备交流,标准输出设备称为stdout,标准输入设备称为stdin,在硬件上标准输出设备通常指显示器,输入设备通常指键盘,在软件上通常借助于系统自带的控制台,也可以不使用系统控制台,例如IDE自带的控制台。stdout和stdin定义了标准输入输出缓冲区大小、起始地址、读写指针、有效数据等内容,在C中它们是同一个结构体中定义的不同的缓存,对于支持访问stdout和stdin的编译器,可以直接输出它们的成员,如下:

#include<stdio.h>int main()
{puts("stdout");printf("%p\n",stdout); //直接输出stdoutprintf("%p\n",stdout->_base); //缓冲区起始地址printf("%d\n",stdout->_bufsiz); //缓冲区大小printf("%d\n",stdout->_cnt); //有效数据字节数printf("%p\n",stdout->_ptr); //读写指针puts("stdin");printf("%p\n",stdin); //直接输出stdinprintf("%p\n",stdin->_base); //缓冲区起始地址printf("%d\n",stdin->_bufsiz); //缓冲区大小printf("%d\n",stdin->_cnt); //有效数据字节数printf("%p\n",stdin->_ptr); //读写指针
}

结果为:
stdout
00007FFAB232FA30
00000000001B68C0
4096
4050
00000000001B68F3
stdin
00007FFAB232FA00
00007FFAB2335B80
4096
0
00007FFAB2335B80
由起始地址可以看到,输入和输出是两个不同缓冲区,它们的大小都是4k字节,读写指针开始为0,随着读写进行不断移动。对于stdin来说有效缓冲数据大小对我们测试代码来说非常重要,请记住这个_cnt属性。由于硬件不同、需求不同,缓冲也分为两种运行机制,一种是全缓冲,即当缓冲区填满才保存,例如硬盘;一种是半缓冲,也叫行缓冲,典型的设备是键盘,当遇到空格或回车时读取全部或一部分,这导致不同的C函数运行方式不同,我们自己编写代码时也可以选择全缓冲和半缓冲。

现在轮到我们讨论scanf()了,它是一个标准输入函数,并采用半缓冲机制,现在带着缓存的概念诠释它的运行规则如下:

  • 如果缓冲区有内容就直接取出

  • 如果没有,就等待用户输入。

  • 用户从键盘输入的每个字符都会暂时保存到缓冲区,直到按下回车键,产生换行符\n才结束输入,从缓冲区中读取数据,开始匹配格式符并赋值给变量。

  • 如果能够匹配整个格式字符串,就直接从缓冲区中读取,不用等待用户输入了。

  • 如果缓冲区中剩余的所有数据只能匹配前半部分控制符,那就等待用户输入剩下的数据。

  • 如果不能匹配,scanf() 还会尝试忽略一些空白符,例如空格、制表符、换行符等,这常常发生在解析数值时

  • 如果这种尝试成功,那么再重复以上的匹配过程。

  • 如果这种尝试失败就尝试匹配下一个格式控制符。

  • 如果所有的格式控制符都尝试完后还不能正确匹配就宣告失败,运行结束

  • 当解析字符串时遇到空格、制表符、换行自动结束

  • 允许只匹配不给变量赋值,也就是说需要赋值的变量数量可以少于格式符,甚至没有变量,当变量数量和格式控制符不一样时编译器可能会给出警告,即使没有变量,匹配时仍然会将字符从缓存中取出。

  • 支持简单的正则表达式修改匹配规则
    其实只要一个格式符未匹配成功整个赋值就算失败了,即便scanf()尝试继续赋值,因为结果是无法预料的。如果对上面的规则掌握不全面也不了解缓存,在使用时肯定会遇到很多坑,一起来看看这些坑:

  1. 如果调用scanf()之前键盘缓冲区中有内容,可能导致失败,需要在调用前清空缓存。
  2. 当输入回车时本身也把\n放入缓存中,如果全部匹配成功,这个’\n’很可能还留在缓存中;如果只匹配了一部分,未匹配成功的字符和’\n’都会留在缓存中;如果匹配失败,留在缓存中的字符会更多。
  3. 匹配字符串时遇到空格会认为结束,剩下的字符串会留在缓存中。

缓存不空是最频繁的一个问题,我们可以选择调用scanf()前清空缓存和调用scanf()后清空缓存,我倾向于每次调用scanf()后清空缓存,这样后面无论通过scanf()还是其它输入函数都不会碰到问题。如果能确定缓存中只剩下一个’\n’,那么运行完后调用getchar()或者用在scanf()中添加一个不赋值的%c,例如scanf(“%d%c”, &a),这种形式即可顺便清除最后回车键入的换行符。有些编译器为了安全起见不允许格式符没有赋值的变量,例如VS会给出警告。如果调用了一个别人的写的函数,不确定留在缓存中的字符数量,就只能清空所有缓存了,标准库提供了一个清除缓存的函数fflush(stdin),但并非所有操作系统都支持,这也是C代码移植头疼的问题,winidows支持这个函数,其它操作系统未必,通过以下代码测试你的平台支持情况:

#include<stdio.h>int main()
{scanf("%s");printf("%d\n",stdin->_cnt);fflush(stdin);printf("%d\n",stdin->_cnt);return 0;
}

当我们输入abc efg后,scanf()遇到空格停止匹配字符串,空格后面字符都留在缓冲区中,通过stdin检测缓存中的字符个数来比较调用fflush()前后的变化,如果有变化那么恭喜你可以直接使用fflush()清理缓存,如果不能只能采用其它方法,下面代码是一个笨方法:

#include <stdio.h>
int main()
{scanf("%s");
printf("%d\n",stdin->_cnt);while(getchar() != '\n');printf("%d\n",stdin->_cnt);return 0;
}

不断取出字符直到最后取出的字符为换行符,其实将代码稍微修改下就能输出缓存中的字符:

#include <stdio.h>
int main()
{scanf("%s");printf("%d\n",stdin->_cnt);while(putchar(getchar()) != '\n');printf("%d\n",stdin->_cnt);return 0;
}

方法虽笨但有助于查看缓冲区内容,例如我们通过下面代码查看部分匹配成功后缓冲区中剩下的内容:

#include <stdio.h>
int main()
{int a;scanf("%d",&a);if (stdin->_cnt)while(putchar(getchar()) != '\n');return 0;
}

输入123abc,因为匹配到字符a时会因为类型不符而停止匹配,缓存中剩下abc和一个’\n’,输出结果也是如此,当我们使用scanf()发生意外错误时,可以通过此方法检测缓存来查找原因。后面我们学了正则表达式后也可以通过正则表达式清除缓存,但无法查阅缓存内容,这部分内容我们放到后面讲解。

由于scanf()总是受到缓存的干扰,并且还有一个缺点是不能返回输入字符的内容,必须声明一个变量来赋值,有没有其它便利的方法呢?有的,标准库提供了一些更简洁的函数来获取字符和字符串:
getchar() 获取一个字符,并返回它
putchar() 输出一个字符,并返回它
gets() 获取一个字符串,并返回它
puts() 输出一个字符串,并返回它
c=getchar()相当于scanf(“%c”, c),使用起来更加方便。当我们想在屏幕上输出一个字符串时,通常会使用printf(“字符串\n”)来输出,每次都要加上\n来换行显得麻烦,此时可以使用puts(“字符串”),它会自动添加换行符。当我们想输入带空格的字符串时,scanf(“%s”,str)是不能获取完整内容的,此时可以使用gets(str),这个函数可以保留字符串中的空格,还能自动将缓冲区的回车取出来丢弃,不会对接下来输入的内容造成影响,一举两得,因此获取字符和字符串推荐使用这些便捷函数。有人会问那为什么不提供获取数值的便捷方法呢?其实坐下来想想就不难发现,只要转换字符串为数值就行,例如:

#include <stdio.h>
#include <stdlib.h>int main()
{char str[20]="";int a = atoi(gets(str));long b = atol(gets(str));float c = atof(gets(str));printf("%d,%ld,%f\n",a,b,c);puts(itoa(a,str,20));puts(ltoa(b,str,20));puts(gcvt(c,3,str));return 0;
}

stdlib库中提供了数值和字符串互相转换的函数,但是调用两个函数来输入输出未必比printf()和scanf()简洁,实际上printf()和scanf()也是调用这些函数进行数值转换的。当输入输出数值时我还是偏向使用printf()和scanf()。printf()和scanf()也有它高效的地方,那就是一次输出和输出多个变量,如果需要格式化输入输出也只能选择printf()和scanf()。

掌握scanf()运行机制和缓存的秘密后很多问题都迎刃而解了,现在轮到我们利用scanf()的优点来高效输入数据了,第一个用途是输入数组元素,例如:

#include <stdio.h>
int main()
{int a[5];scanf("%d,%d,%d,%d,%d",a,a+1,a+2,a+3,a+4);printf("%d,%d,%d,%d,%d",a[0],a[1],a[2],a[3],a[4]);
}

上面代码必须输入逗号分隔数组元素,当数组元素不多时,使用这种方式很方便,但是当数组元素较多时就需要使用循环了,如果你使用for(i=0;i<5;i++) scanf(“%d”,a+i)来输入一个数组就必须每输入一个元素按空格或回车键,能否像上面一样通过逗号来输入呢?掌握了scanf()后不难写出:

#include <stdio.h>
int main()
{int a[5];int i;for(i=0;i<5;i++) scanf("%d%c",a+i);for(i=0;i<5;i++) printf("%d ",a[i]);
}

这里每读取一个元素后,用%c清除间隔符,不管是用空格还是逗号当间隔符都可以,最后一个%c清除缓存中的回车。

第二个用途是初始化结构体,可以发现即便数据类型不同,同样的方法也适合初始化结构体,下面是一个例子:

#include<stdio.h>struct Student
{char name[20];int age;char sex;
};int main()
{struct Student s;scanf("%s%c", s.name);scanf("%d%c", &s.age);scanf("%s%c", &s.sex);puts(s.name);printf("%d\n",s.age);printf("%d\n",s.sex);
}

当我们输入Tom 20 0,或Tom 20 man,或者Tom, 19,0时都可以正确解析,因为空格、逗号或者其它分隔符均被%c清除,最后一个回车也被%c清除。

第三个用途是限制输入长度,和printf()一样,scanf()可以在%前面添加数字来限制读取长度,多于的字符会被抛弃,例如:

#include<stdio.h>int main()
{char str[11]="";scanf("%10s",str);//只取10个字符scanf("%*[^\n]");scanf(“%*c”); //清除多余缓存puts(str);
}

str的容量只有10个字符,如果输入超过10个字符就会导致数组溢出,从而导致程序崩溃,这也是被黑客攻击的漏洞之一,这也是为什么一些编译器改用scanf_s()的原因。使用scanf()将输入内容限制为10个字符能够堵住这个漏洞,最后不要忘记清除缓存。现在你已经看到使用正则表达式清除缓存的语句了,马上我们就会讲解正则表达式的用法。

第四个用途是使用正则表达式匹配内容,scanf()支持简单的正则表达式,当使用正则表达式时scanf()会改变规则,例如读取字符串时不再是遇到空格停止,而是不能满足正则表达式条件时停止。scanf()中的正则表达式有两种运行方式,匹配和丢弃,我们先看看匹配是怎么运行的。
scanf(“%[a-z]”, str) 表示从左到右匹配小写字母,遇到非法字符立即停止匹配,然后将匹配的结果写入str,未匹配的内容留在缓冲区,举个例子:

#include<stdio.h>int main()
{char str[20]="";scanf("%[a-z]",str);puts(str);scanf("%s%c",str);puts(str);
}

当输入door901num时,匹配结果为door,scanf()并不支持全局匹配,遇到数字9后停止运行,剩下的字符留在缓存中,最后两句话输出缓冲区中的内容并清除缓存。现在我们可以改变匹配规则,利用正则表达式输入带空格的字符串了:
scanf(“%[^\n]”, str)
可以匹配除了换行符之外的所有字符,只到读到换行符才会停止,例如:

#include<stdio.h>int main()
{char str[20]="";scanf("%[^\n]",str);getchar();puts(str);
}

输入一段包含空格的字符串,就可以看到结果。

正则表达式另外一个运作方式是丢弃字符,在%的后面加上一个号,变成从左到右遇到符合规则的字符会丢弃,遇到不符合规则的字符则停止丢弃,例如:
scanf(“%
[0-9]%s”, str)
丢弃所有数字,这里*不用作通配符,而是表示忽略内容,这是和常规正则表达式的区别,当遇到非数字时停止丢弃,然后剩余缓存的内容匹配%s,我们可以编写一段代码测试结果:

#include<stdio.h>int main()
{char str[11]="";scanf("%*[0-9]%s%c", str);puts(str);
}

输入123abc,得到结果abc,这样就丢弃了数字开头的部分,也可以将正则表达式改为scanf(“%*d%s%c”, str);%d表示丢弃一个整数,注意如果输入abc123导致一开始就丢弃失败会立即退出,不会继续匹配后面的%s。最后利用丢弃规则编写清空缓存的代码,如下:
scanf("%
[^\n]“); scanf(”%*c");
上面代码第一句丢弃除换行符之外所有的字符,第二句话丢弃换行符,也可以换成getchar(),我们用代码来测试下效果:

#include<stdio.h>int main()
{scanf("%s");printf("%d\n",stdin->_cnt);scanf("%*[^\n]");scanf("%*c");//getchar();scanf("%c");printf("%d\n",stdin->_cnt);return 0;
}

输入abc efg,可以看到留在缓存中的efg以及空格和换行被清除了,按道理将两句话合成一句scanf(“%*[\n]%c")或scanf("%*[\n]%*c”)也行得通,但是有些编译器在丢弃到只剩一个换行符后就不再匹配后面的格式符,这会导致清缓存失败,为了保险将它们分为两句执行。scanf()对正则表达式的支持相对较弱,需要多多测试。

最后我们来想一想,为什么scanf()读取字符串时默认遇到空格会停止,可能是想让空格也作为间隔符来给多个字符串变量赋值吧,我们用代码来测试一下:

#include<stdio.h>int main()
{char str1[20]="";char str2[20]="";scanf("%s%s",str1,str2);puts(str1);puts(str2);return 0;
}

输入abc 123,从结果可以看到str1和str2被成功赋值,这说明设计初衷很可能如此。这相当于如下代码:

#include<stdio.h>int main()
{char commandStr[20];char par1[5]="";char par2[5]="";char par3[5]="";char par4[5]="";char par5[5]="";char *parStr[5]={par1,par2,par3,par4,par5};int i=0;scanf("%s%c", commandStr);while(stdin->_cnt) scanf("%s%c",parStr[i++]);puts(commandStr);for(i=0;i<5;i++) puts(parStr[i]);return 0;
}

如果我们把使用空格分割的字符串看作输入的命令和它的参数,就可以通过上面方法解析出来,尝试输入一条linux命令ls –l –h –a,可以发现命令和参数都被正确解析,但我发现有些编译器如果参数中包含“:”有时会解析不正常,不知道是何原因。

相比printf(),scanf()的返回值更加重要,它返回成功读取的变量个数,只有返回值与赋值的变量个数完全一致结果才会正确,如果一个变量都未赋值成功返回0。

到这里所有的标准输入输出细节都讲清楚了,可能你会认为平时我们对输入要求并不高,花费这么多精力专研值得吗?可以明确的告诉你,值!因为文件的输入输出也有类似的fprint()和fscanf()函数,它们和print(),scanf()用法几乎相同,只是操作的对象不一样,将这里学到的知识用于读写文件中的内容会大大提升文件读写的技巧。

getchar()和putchar()

当从控制台输入输出单个字符时这两个函数的确比printf()和scanf()方便,因为不用编写格式控制符,特别是getchar(),对变量赋值时能返回输入字符,不需要赋值时能清空缓存,或者干脆放到末尾放置程序退出。

gets()和puts()

puts()最大的特点是自动在末尾添加换行符,虽然不能忠实反应输出内容,但是极大方便了行输出,对应的gets()会读取并自动丢弃缓存中换行符并且忽略字符串中间的空格符,极大方便了行输入。需要注意的是gets()在方便之余带来了安全隐患,如果用来储存字符串的数组长度不够就会产生溢出,破坏代码运行甚至遭受黑客攻击。C99承认这个漏洞但依然保留了这个函数,支持C99的很多编译器会给出警告,C11则完全废除了这个函数,但为了兼容过去的代码,支持C11很多编译器还是保留了这个函数。C11新增gets_s()函数代替gets(),gets_s()需要指定输入长度,例如:
gets_s(words,len);
当输入内容超过指定长度时,gets_s()返回NULL,然后清除缓存中该行内容,直到遇到换行符或文件尾,最后调用错误处理函数,也就是说gets_s()认定该行无效同时自动清除该行剩余内容。对于VS默认使用C11标准处理输入输出,使用gets()会报错,要使用gets()必须关闭SDL,如图:

VS还要求为gets_s()添加错误处理函数,如果不添加也会导致运行时报错,而其它支持C99的编译器则完全不支持gets_s()。如果使用C99又想避免溢出,可以使用scanf()配合正则表达式,例如:

#include <stdio.h>int main()
{char str[5] = "";char str1[5] = "";scanf("%4[^\n]", str);scanf("%4[^\n]", str1);puts(str);puts(str1);return(0);
}

字符数组str容量为5,最多输入4个字符,因此在scanf()中将读取字符设置为4,然后使用正则表达式[\n]代替s使得可以读取字符串中的空格。当输入字符超过4个时,scanf()不会读取后面的字符,剩余内容留在缓存中由str1读取,如果不想缓存中留有内容,可以加入代码scanf("%*[\n]“);scanf(”%*c");清除缓存。

其它IO函数

stdio.h中定义了二十多种输入输出函数,常用的已经在前面讲解,一些函数可能在工作中会用到,一起来看看。

  • ungetc()

允许将字符送回输入流,ungetc()的格式为:
ungetc(char,*fp)
C标准规定可以将一个字符送回输入流,是否能将多个字符送回输入流需要看编译器能否实现,该函数可以用于文件输入和标准输入,更多时候用于stdin,可以用下面代码测试是否能将多个字符送回标准输入流:

#include<stdio.h>
#include<stdlib.h>int main()
{char str[20] = "";ungetc('H',stdin);ungetc('HW',stdin);gets(str);puts(str);
}
  • fflush()

刷新缓冲区,格式为fflush(*fp),将缓冲区中所有内容输出到fp中,通常使用fflush(stdin)清空键盘缓存,需要编译器支持。

C语言深入标准输入输出相关推荐

  1. c语言中eof_C语言的标准 “输入输出”!今天是你学C语言的第几天?

    当你选择了一种语言,意味着你还选择了一组技术.一个社区.--Joshua Bloch # C语言标准输入输出 C语言输入输出,是指C程序从键盘.文件等外部获取数据,将数据输出到显示器.文件等外部输出设 ...

  2. 取代C语言的标准输入输出:cin 和 cout【C++标准输入输出】

    C 和 C++ 标准输入输出的区别 cin 标准输入 C++解决C cout 标准输入 C 和 C++ 标准输入输出的区别 cin 取代 scanf cout 取代 printf printf 和 s ...

  3. 学什么c语言标准,C语言的标准 “输入输出”!今天是你学C语言的第几天?

    当你选择了一种语言,意味着你还选择了一组技术.一个社区.--Joshua Bloch # C语言标准输入输出 C语言输入输出,是指C程序从键盘.文件等外部获取数据,将数据输出到显示器.文件等外部输出设 ...

  4. char类型怎么输入 c语言_C语言的标准 “输入输出”!今天是你学C语言的第几天?...

    当你选择了一种语言,意味着你还选择了一组技术.一个社区.--Joshua Bloch # C语言标准输入输出 C语言输入输出,是指C程序从键盘.文件等外部获取数据,将数据输出到显示器.文件等外部输出设 ...

  5. 趣学 C 语言(十三)—— 标准输入输出重定向

    在 C 语言中,可以使用 freopen 将输入输出重定向. C语言的标准输入输出为 stdin 和 stdout,这两个变量的类型为FILE*类型,也就是说,标准输入输出操作,其本质还是文件操作,只 ...

  6. 【C语言督学训练营 第二天】C语言中的数据类型及标准输入输出

    文章目录 一.前言 二.数据类型 1.基本数据类型 ①.整形 ②.浮点型 ③.字符型 2.高级数据类型 3.数据分类 ①.常量 ②.变量 三.标准输入输出 1.scanf 2.printf 四.进制转 ...

  7. 取代C语言标准输入输出:cin 和 cout【C++标准输入输出】

    C语言 和 C++ 标准输入输出的区别 cin标准输入 解决scanf输入字符的问题 解决C语言输入字符串的问题 cout 进制输出 域宽 左对齐 / 右对齐 填充 设置有效数字 设置浮点数精度 C语 ...

  8. C语言基础——数据输入输出

    五.数据输入输出 5.1 概述 C 语言中,所有的数据输入/输出都是由库函数完成的,都为函数语句. 在使用 C 语言库函数时,要用预编译命令 #include 将有关头文件包括到源文件中:如,使用标准 ...

  9. 汽车电子行业的 C 语言编程标准

    已剪辑自: https://mp.weixin.qq.com/s/i2e-cTbrWZTNne-3AfoWDw 前言 之前分享了一些编程规范相关的文章,有位读者提到了汽车电子行业的MISRA C标准, ...

最新文章

  1. JavaScript语言基础15
  2. C++深复制(深拷贝)、浅复制(浅拷贝)和复制构造函数(拷贝构造函数)详解+实例
  3. 基础设备----笔记
  4. AtCoder Grand Contest 017
  5. C++程序设计方法3:移动构造函数
  6. matlab求解含两个累加公式的方程,使用fsolve 函数求解含两个参数的多个方程问题,希望有会的人帮...
  7. atitit。解决 No suitable Log constructor。。NoClassDefFoundError: org/apache/log4j/Category 找不到类的
  8. win7下dynamips类模拟器桥接到本地PC,但是无法ping通的解决办法
  9. 2020年易语言调用大漠插件来制作游戏脚本教程
  10. pycharm新建python的快捷键_Pycharm超级好用的快捷键
  11. dz3 php post 登录,discuz X3用户登录uc_user_login()函数详解
  12. Django项目实战——11—(文件存储方案FastDFS、容器化方案Docker)
  13. 系统论、控制论和信息论
  14. (Java-17)简单模拟账号密码登陆
  15. uniapp onReachBottom 不触发
  16. Initramfs unpacking failed:junk in compressed archive
  17. 柯尔特python_广州市柯卫达科技有限公司
  18. 【漏洞提示】MySQL8.0.29因重大bug官网已下架
  19. TMMi测试能力成熟度模型简介
  20. 如何做好模型设计环节

热门文章

  1. JQuery 详细教程
  2. 【5G】5G源于什么?
  3. HTML5基础-新增标签+新增属性+布局案例
  4. sql server 各种等待类型-转
  5. 2020云盘点:公有云芯基建,AWS、阿里云、紫光云等创新发力
  6. matlab工具箱计算最小生成树_matlab_bgl 图论基本函数库, 相当丰富.包含工具箱、 、Dijkstra最短路径、 254万源代码下载- www.pudn.com...
  7. 4千元AI围棋机器人来了!人机大战在家随时开打,职业九段水平,聂卫平点赞...
  8. 如何解决ipad1升级没有响应的问题
  9. Java中ArrayList类的常用方法
  10. qq登录界面php修改法,怎么修改qq登录界面