C语言学习第二天-输入输出

显示数据的函数

在C语言中,有三个函数可以用来在显示器上输出数据,它们分别是:

puts():只能输出字符串,并且输出结束后会自动换行。

putchar():只能输出单个字符。

printf():可以输出各种类型的数据。

%-9d中,d表示以十进制输出,9表示最少占9个字符的宽度,宽度不足以空格补齐,-表示左对齐。

printf()高级用法

printf()格式控制符的完整形式如下:

%[flag][width][.precision]type

[]表示此处的内容可有可无,是可以省略的。

1. type表示输出类型,比如%d、%f、%c、%lf,type就分别对应d、f、c、lf。

type这一项必须有,这意味着输出时必须要知道是什么类型。

2. width表示最小输出宽度,也就是至少占用几个字符的位置,例如,%-9d中width对应9,表示输出结果最少占用9个字符的宽度。

当输出结果的宽度不足width时,以空格补齐(如果没有指定对齐方式,默认会在左边补齐空格);当输出结果的宽度超过width时,width不再起作用,按照数据本身的宽度来输出。

3. .precision表示输出精度,也就是小数的位数。

当小数部分的位数大于precision时,会按照四舍五入的原则丢掉多余的数字;

当小数部分的位数小于precision时,会在后面补0。

4. flag是标志字符。例如,%#x中flag对应#,%-9d中flag对应-。

scanf()函数

scanf是scan format的缩写,意思是格式化扫描,也就是从键盘获得用户输入,和printf的功能正好相反。

用法:scanf("%d %d", &a, &b);

scanf的变量前要带一个&符号。&为取地址符,也就是获取变量在内存中的地址。scanf会根据地址把读取到的数据写入内存。

%p是一个新的格式控制符,它表示以十六进制的形式(带小写的前缀)输出数据的地址。如果写作%P,那么十六进制的前缀也将变成大写形式。

连续输入

从本质上讲,我们从键盘输入的数据并没有直接交给scanf(),而是放入了缓冲区中,指导我们按下回车键,scanf()才到缓冲区中读取数据。如果缓冲区中的数据符合scanf()的要求,那么就读取结束;如果不符合要求,那么就继续等待用户输入,或者干脆读取失败。

对读取字符串的说明

字符串定义的两种定义形式:

char str1[] = "Hello world!";

char *str2 = "阿巴阿巴";

第一种形式的字符串所在的内存既有读取权限又有写入权限,第二种形式的字符串所在的内存只有读取权限,没有写入权限。printf(),puts()等字符串输出函数只要求字符串由读取权限,而scanf(),gets()等字符串输入函数要求字符串由写入权限,所以,第一种形式的字符串既可以用于输出函数又可以用于输入函数,而第二种形式的字符串只能用于输出函数。

另外,对于第一种形式的字符串,在[]里面要指明字符串的最大长度,如果不指明,也可以根据=后面的字符串来自动推算。

scanf()读取数据时需要的是数据的地址,整数、小数、单个字符都要加&取地址符。对于字符串,可以加&也可以不加&,这是因为,字符串的名字会自动转换为字符串的地址,所以不用再多此一举加&了。

输入单个字符

输入单个字符当然可以使用scanf()这个通用的输入函数,对应的格式控制符为%c。还有三个专用的字符输入函数,包括getchar()、getche()、getch(),它们具有某些scanf没有的特性,是scanf()不能代替的。其中,getche()和getch()函数只能在windows系统中使用。

1. getchar()

最容易理解的字符输入函数是getchar(),它就是scanf("%c", c)的替代品。或者说,getchar()是scanf()的一个简化版本。

2. getche()

getche()没有缓冲区,输入一个字符后会立即读取,不用等待用户按下回车键,这是它和scanf()、getchar()的最大区别。

3. getch()

getch()也没有缓冲区,输入一个字符后会立即读取,不用按下回车键,这一点和getche()相同。getch()的特别之处是它没有回显,看不到输入的字符。所谓回显,就是在控制台上显示出用户输入的字符;没有回显,就不会显示用户输入的字符,就好像根本没有输入一样。

回显在大部分情况下是非常有必要的,它能够与用户及时交互,让用户清楚地看到自己输入的内容。但在某些特殊情况下,我们却不希望有回显,例如输入密码,有回显是非常危险的,容易被偷窥。

输入字符串

输入字符串当然可以使用scanf()这个通用的输入函数,对应的格式控制符为%s。除此之外,还有gets()这个专用的字符串输入函数,它拥有一个scanf()不具备的特性。

gets()是有缓冲区的,每次按下回车键,就代表当前输入结束了,gets()开始从缓冲区中读取内容,这一点和scanf()是一样的。gets()和scanf()的主要区别是:

scanf()读取字符串时以空格为分隔,遇到空格就认为当前字符串结束了,所以无法读取含有空格的字符串。

gets()认为空格也是字符串的一部分,只有遇到回车键时才认为字符串输入结束,所以,不管输入了多少个空格,只要不按下回车键,对gets()来说就是一个完整的字符串。

也就是说,gets() 能读取含有空格的字符串,而 scanf() 不能。

缓冲区

缓冲区又称为缓存,是内存空间的一部分。也就是说,计算机在内存中预留了一定的存储空间,用来暂时保存输入或输出的数据,这部分预留的空间就叫做缓冲区(缓存)。

有时候,从键盘输入的内容,或者将要输出到显示器上的内容,会暂时进入缓冲区,待时机成熟,再一股脑将缓冲区中的所有内容“倒出”,我们才能看到变量的值被刷新,或者屏幕产生变化。

有时候,用户希望得到最及时的反馈,输入输出的内容就不能进入缓冲区。缓冲区是输入输出的“命门”所在。

为什么要引入缓冲区(缓存)

缓冲区是为了让低俗的输入输出设备和高速的用户的程序能够协调工作,并降低输入输出设备的读写次数。

用户程序的执行速度可以看做CPU的运行速度,如果没有各种硬件的阻碍,理论上它们是同步的。例如,我们都知道硬盘的速度要远低于 CPU,它们之间有好几个数量级的差距,当向硬盘写入数据时,程序需要等待,不能做任何事情,就好像卡顿了一样,用户体验非常差。计算机上绝大多数应用程序都需要和硬件打交道,例如读写硬盘、向显示器输出、从键盘输入等,如果每个程序都等待硬件,那么整台计算机也将变得卡顿。

但是有了缓冲区,就可以将数据先放入缓冲区中(内存的读写速度也远高于硬盘),然后程序可以继续往下执行,等所有的数据都准备好了,再将缓冲区中的所有数据一次性地写入硬盘,这样程序就减少了等待的次数,变得流畅起来。

缓冲区的另外一个好处是可以减少硬件设备的读写次数。其实我们的程序并不能直接读写硬件,它必须告诉操作系统,让操作系统内核(Kernel)去调用驱动程序,只有驱动程序才能真正的操作硬件。从用户程序到硬件设备要经过好几层的转换,每一层的转换都有时间和空间的开销,而且开销不一定小;一旦用户程序需要密集的输入输出操作,这种开销将变得非常大,会成为制约程序性能的瓶颈。

这个时候,分配缓冲区就是必不可少的。每次调用读写函数,先将数据放入缓冲区,等数据都准备好了再进行真正的读写操作,这就大大减少了转换的次数。实践证明,合理的缓冲区设置能成倍提高程序性能。缓冲区其实就是一块内存空间,它用在硬件设备和用户程序之间,用来缓存数据,目的是让快速的 CPU 不必等待慢速的输入输出设备,同时减少操作硬件的次数。

缓冲区的类型

根据不同的标准,缓冲区可以有不同的分类。

根据缓冲区对应的是输入设备还是输出设备,可以分为输入缓冲区和输出缓冲区。

根据数据刷新(也可以称为清空缓冲区,就是将缓冲区中的数据“倒出”)的时机,可以分为全缓冲、行缓冲、不带缓冲。

1. 全缓冲

在这种情况下,只有当缓冲区被填满以后才进行真正的输入输出操作。缓冲区的大小都是有限制的,比如1KB、4MB等,数据量达到最大值时就清空缓冲区。

在实际开发中,将数据写入文件后,打开文件并不能立即看到内容,只有清空缓冲区,或者关闭文件,或者关闭程序后,才能在文件中看到内容。这种现象,就是缓冲区在作怪。

2. 行缓冲

在这种情况下,当在输入或者输出的过程中遇到换行符时,才执行真正的输入输出操作。行缓冲的典型代表就是标准输入设备(也即键盘)和标准输出设备(也即显示器)。

3. 不带缓冲

不带缓冲区,数据就没有地方缓存,必须立即进行输入输出。

getche()、getch()就不带缓冲区,输入一个字符后就立即执行了,根本不用按下回车键。

windows下的printf()也不带缓冲区,不管最后有没有换行符\n,都会立即输出。

对于类似的输出代码,错误信息输出函数perror()也没有缓冲区。错误信息必须刻不容缓、立刻马上显示出来,缓冲区将会增加捕获错误的时间,这是毫无理由的。

缓冲区的刷新(清空)

刷新缓冲区就是将缓冲区中的内容送达到目的地,缓冲区的刷新遵循以下的规则:

不管是行缓冲还是全缓冲,缓冲区满时会自动刷新;

行缓冲遇到换行符\n时会刷新;

关闭文件时会刷新缓冲区;

程序关闭时一般也会刷新缓冲区,这个是由标准库来保障的;

使用特定的函数也可以手动刷新缓冲区。

缓冲区位于用户程序和硬件设备之间,用来缓存数据,目的是让快速的CPU不必等待慢速的输入输出设备,同时减少操作硬件的次数。对于IO密集型的网络应用程序,比如网站、数据库、DNS、CDN等,缓冲区的设计至关重要,它能十倍甚至百倍地提高程序性能。

结合缓冲区谈scanf()函数

scanf()是带有缓冲区的。遇到scanf()函数,程序会先检查输入缓冲区中是否有数据:

如果没有,就等待用户输入。用户从键盘输入的每个字符都会暂时保存到缓冲区,直到按下回车键,输入结束,scanf()再从缓冲区中读取数据,赋值给变量。

如果有数据,哪怕是一个字符,scanf()也会直接读取,不会等待用户输入。

scanf()匹配到想要的数据后,会将匹配到的数据从缓冲区中删除,而没有匹配到的数据仍然会留在缓冲区中。

在控制台中输入的任何内容本质上都是字符串,都会被%s所匹配。

缓冲区引发的问题

当用户按下回车键时,回车换行符也会被保存到缓冲区,只是大多数情况下 scanf() 会忽略。但是当控制字符串不是以 %xxx 开头时,回车换行符就起作用了,scanf() 会对它进行匹配,只是匹配失败而已。匹配回车换行符失败之后,既不等待用户输入,也不给变量赋值。

清空缓冲区

四种方法实现清空缓冲区

1. 使用fflush()函数

fflush()函数冲洗流中的信息,该函数通常用语处理磁盘文件。清除读写缓冲区,需要立即把输出缓冲区的数据进行物理写入。fflush()函数包含在stdio.h头文件中。

函数原型:int fflush(FILE *stream),在这里的stream就是索要清除缓存区的文件。

函数返回值:当进行刷新成功返回0,失败返回EOF。没有缓冲区或者只读打开时也返回0值。如果fflush返回EOF,数据可能由于写错误已经丢失。

用法示例:fflush(stdin)刷新标准输入缓冲区,fflush(stdout); 刷新标准输出缓冲区。printf()函数后面加fflush(stdout); 可以提高打印效率。

2. 使用while((ch = getchar()) != '\n' && ch != EOF); 语句

这种方法是最好的方法,可以成为万能清空缓存区语句。

3. 使用setbuf()函数

setbuf()函数是linux中的C函数,主要用于打开和关闭缓冲机制,包含在头文件stdio.h中。

setbuf()函数有打开和关闭缓冲机制,为了带缓冲进行I/O,参数buf必须指向一个长度为bufsiz的缓冲区。通常在此之后该流就是全缓冲的,但是如果该流与一个终端设备相关,那么某些系统也可以将其设置为行缓冲。为了关闭缓冲,可以将buf参数设置为NULL。

函数原型:void setbuf(FILE *stream, char *buf); 一个参数是文件流,一个参数是buf指向的缓冲区长度,这个长度就是在stdio.h中定义的宏bufsiz所决定的。当定义buf为空时,setbuf函数将使得文件I/O不带缓冲。

4. 使用scanf("%*[^\n]"); scanf("%*c"); 语句

scanf()高级用法

1. 指定读取长度

类似printf(),scanf()也可以在格式控制符的中间加一个数字,用来表示读取数据的最大长度,例如:%2d表示最多读取两位整数;%10s表示读取的字符串的最大长度为10,或者说,最多读取10个字符。

限制读取数据的长度在实际开发中非常有用,最典型的一个例子就是读取字符串:我们为字符串分配的内存是有限的,用户输入的字符串过长就存放不了了,就会冲刷掉其它的数据,从而导致程序出错甚至崩溃;如果被黑客发现了这个漏洞,就可以构造栈溢出攻击,改变程序的执行流程,甚至执行自己的恶意代码,这对服务器来说简直是灭顶之灾。

在用 gets() 函数读取字符串的时候,有一些编译器会提示不安全,建议替换为 gets_s() 函数,就是因为 gets() 不能控制读取到的字符串的长度,风险极高。

2. 匹配特定的字符

%s控制符会匹配除空白符以外的所有字符,它有两个缺点:

%s不能读取特定的字符,比如只想读取小写字母,或者十进制数字等,%s就无能为力;

%s读取到的字符串中不能包含空白符,有些情况会比较尴尬,例如,无法将多个单词存放到一个字符串中,因为单词之间就是以空格为分隔的,%s遇到空格就读取结束了。

要想解决以上问题,可以使用scanf()的另外一种字符匹配方式,就是%[xxx]。[]包围起来的是需要读取的字符集合。例如,%[abcd]表示只读取字符abcd,遇到其它的字符就结束;这里并不强调字符的顺序,只要字符在abcd范围内都可以匹配成功。

使用连接符

为了简化字符集合的写法,scanf()支持使用连字符-来表示一个范围内的字符,例如%[a-z]、%[0-9]等。连字符左边的字符对应一个ASCII码,连字符右边的字符也对应一个ASCII码,位于这两个ASCII码范围以内的字符就是要读取的字符。连字符左边的ASCII码要小于右边的,如果反过来,那么它的行为是未定义的。

常用的连字符举例:

%[a-z]表示读取 abc...xyz 范围内的字符,也即小写字母;

%[A-Z]表示读取 ABC...XYZ 范围内的字符,也即大写字母;

%[0-9]表示读取 012...789 范围内的字符,也即十进制数字。

你也可以将它们合并起来,例如:

%[a-zA-Z]表示读取大写字母和小写字母,也即所有英文字母;

%[a-zA-Z0-9]表示读取所有的英文字母和十进制数字;

%[0-9a-f]表示读取十六进制数字。

不匹配某些字符

scanf()允许我们在%[]中直接指定某些不能匹配的字符,具体方法就是在不匹配的字符前面加上^。

%[^\n]表示匹配除换行符以外的所有字符,遇到换行符就停止读取;

%[^0-9]表示匹配除十进制数字以外的所有字符,遇到十进制数字就停止读取。

丢弃读取到的字符

scanf()允许把读取到的数据直接丢弃,不往变量中存放,具体方法就是在%后面加一个*。

%*d表示读取一个整数并丢弃;

%*[a-z]表示读取小写字母并丢弃;

%*[^\n]表示将换行符以外的字符全部丢弃。

scanf()高级用法

scanf()格式控制符的完整形式如下:

%[*][width]type

[]表示此处的内容可有可无,是可以省略的。

1. type表示读取什么类型的数据,例如%d、%s、%[a-z]、%[^\n]等,type必须有。

2. width表示最大读取宽度,可有可无。

3. *表示丢弃读到的数据,可有可无。

键盘监听

键盘监听就是用户按下某个键时系统做出相应的处理。

阻塞式键盘监听用于用户输入时一般没有任何问题,用户输入完数据再执行后面的代码往往也符合逻辑。然而在很多小游戏中,阻塞式键盘监听会带来很大的麻烦,用户要不停按键游戏才能进行,这简直就是灾难,所以在小游戏中一般采用非阻塞式键盘监听:用户输入数据后程序可以捕获,用户不输入数据程序也可以继续执行。

在 Windows 系统中,conio.h头文件中的kbhit()函数就可以用来实现非阻塞式键盘监听。

用户每按下一个键,都会将对应的字符放到输入缓冲区中,kbhit() 函数会检测缓冲区中是否有数据,如果有的话就返回非 0 值,没有的话就返回 0 值。但是 kbhit() 不会读取数据,数据仍然留在缓冲区,所以一般情况下还要结合输入函数将缓冲区种的数据读出。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值