c语言 - 输入与输出

c语言 - 输入与输出,gcc分步编译,输入缓冲区,以及如何清空输入缓冲区

GCC 编译步骤

GNU 编译器套装(英语:GNU Compiler Collection,缩写为 GCC),指一套编程语言编译器,以 GPL 及 LGPL 许可证所发行的自由软件,也是 GNU 项目的关键部分,也是 GNU 工具链的主要组成部分之一。GCC(特别是其中的 C 语言编译器)也常被认为是跨平台编译器的事实标准。1985 年由理查德·马修·斯托曼开始发展,现在由自由软件基金会负责维护工作。

原名为 GNU C 语言编译器(GNU C Compiler),因为它原本只能处理 C 语言。GCC 在发布后很快地得到扩展,变得可处理 C++。之后也变得可处理 Fortran、Pascal、Objective-C、Java、Ada,Go 与其他语言。

许多操作系统,包括许多类 Unix 系统,如 Linux 及 BSD 家族都采用 GCC 作为标准编译器。苹果电脑预装的 Mac OS X 操作系统也采用这个编译器。

GCC 将 C 源码文件编译成最终的可执行文件要经历四个步骤:预处理 -> 编译 -> 汇编 -> 链接

  • 预处理(Preprocessing)gcc -E hello.c -o hello.i,处理 C 源码文件中的预处理指令,处理之后依旧是 C 源码文件。如果省略 -o 选项,则默认将预处理结果输出到屏幕。
  • 编译(Compilation)gcc -S hello.i -o hello.s,将 C 源码文件编译为平台相关的汇编语言源代码,如果省略 -o 选项,则默认输出文件名为 hello.s。
  • 汇编(Assembly)gcc -c hello.s -o hello.o,将汇编语言源代码转换为可重定位目标文件(二进制),如果省略 -o 选项,则默认输出文件名为 hello.o。
  • 链接(Linking)gcc hello.o -o hello,将 hello.o 以及一些其它必要的可重定位目标文件(printf() 函数)组合起来,创建可执行目标文件 hello,如果省略 -o 选项,则默认输出文件名为 a.out。

经历了这四个步骤后,hello.c 源码文件变成了 hello 可执行目标文件,然后我们就可以在 shell 中使用./hello来执行它。当然,实际上我们完全可以使用gcc -o hello hello.c来一并执行这四个步骤。

C 语言输出函数

C 语言输出函数主要有 3 个:putcharputsprintf,它们都包含在stdio.h头文件中。
puts会自动添加换行符\n,而printf不会,printf完全可替代putcharputs,且printf支持格式化输出。

printf 函数原型
int printf(const char *format, ...)

  • format输入参数,字符常量指针,即指向字符常量的指针,格式化字符串就写在这里
  • ...输入参数,可变参数,可以有任意个(零个或多个),根据 format 的格式化参数个数而定
  • 成功则返回写入的字符个数,失败则返回负数

printf 格式化参数
格式占位符的一般形式:%[n$][flags][width][.prec]type,方括号表示可选。

  • 每个格式占位符均以%开头,然后以type数据类型结束。如需输出%本身,请使用%%
  • n$,表示当前 type 所在的参数位置(从 1 开始),如果有一个格式占位符使用了n$,则全部都要使用n$,否则 gcc 会产生Wformat警告。
  • flags,标志位,可以有多个。具体的 flag 如下:
    • #规范输出o八进制值前面自动添加数字0xX十六进制值前面自动添加0x0XaAeEfFgG浮点值始终包含小数点,而gG则尾部的 0 始终保留。
    • 0零填充。对于数值类型(整型、浮点型),使用 0 填充左边的空白,而非使用默认的空格填充。
    • -左对齐。默认为右对齐。
    • 符号位-占位。如果为正数则使用空格占位,如果为负数则被忽略。
    • +符号位-显示。如果为正数则使用+前缀符,如果为负数则被忽略。
  • width,字段最小宽度,如果参数的宽度(一个字符占有的宽度为 1)小于 width,则使用空格或 0 填充字段左边或右边(具体要看 flags),如果参数的宽度大于 width,则该 width 被忽略。width 为一个十进制数值,除了静态指定外,我们还可以使用**n$(n 从 1 开始)来动态指定 width 值。
  • .prec,字段输出精度,以.开头,然后接一个十进制数字表示精度。除了静态指定外,我们还可以使用**n$(n 从 1 开始)来动态指定 prec 值。对于d, i, o, u, x, X整型,表示输出的最小位数,不足用 0 填充;对于a, A, e, E, f, F浮点型,表示输出的小数位数;对于g, G浮点型,表示最大有效数;对于s, S字符串,表示输出的最大字符数。
  • type,字段数据类型。具体的 type 如下:
    • d, i有符号十进制数字,即signed int
    • o, u, x, X无符号N进制数字,即unsigned int,分别代表:八进制、十进制、十六进制(x 使用小写 abcdef 表示,X 使用大小 ABCDEF 表示)。
    • f, F浮点型-一般表示法,即double,输出的格式为:[-]ddd.ddd,默认精度为 6。
    • a, A浮点型-十六进制数,即double,输出的格式为:[-]0xh.hhhhp±,如果 type 为 A,则输出大写的 0X、ABCDEF、P。
    • e, E浮点型-科学记数法,即double,输出的格式为:[-]d.ddde±dd,默认精度为 6。如果 type 为 E,则输出大写的 E。
    • g, G浮点型-自适应格式,即double,根据浮点数的大小自动的选择%f、%e%g)或%F、%E%G)。
    • c无符号字符型,即unsigned char
    • s字符常量指针,即const char *,如果没有指定精度,则默认输出到\0终止字符(不包括终止字符)。
    • p无类型的指针,即void *,打印指针的地址,使用%#x输出格式。
  • 对于d, i, o, u, x, or X type,它还存在以下 length modifier 长度修饰符(位于 type 之前):
    • hhsigned char、unsigned char
    • hsigned short、unsigned short
    • lsigned long、unsigned long
    • llsigned long long、unsigned long long
    • Llong double
    • zsize_t、ssize_t

%n$的用法举例,printf("%1$s, %1$s\n", "www.zfl9.com"),将输出www.zfl9.com, www.zfl9.com

*的用法举例,我要打印两个数字,分别是 999 和 1,但是我想输出的结果为999001而非9991,该怎么做呢?
第一种,静态指定字段宽度,即printf("%d%03d\n", 999, 1),每个格式说明符都与传入的参数一一对应
第二种,动态指定字段宽度,即printf("%d%0*d\n", 999, 3, 1)%0*d*占用参数3d占用参数1
第三种,结合%n$,即printf("%1$d%2$0*3$d\n", 999, 1, 3),开头的n$表示 type 的位置,后面的n$则为*的位置

转义序列
此处的转移序列仅针对 ASCII,ASCII 字符集只定义了 128 个字符,这 128 个字符使用 [0, 127] 区间的一个唯一数字表示,该数字称为码点(code point),因为 128 个字符仅需要 7-bit 空间,因此,我们可以使用一个 char 变量存储一个 ASCII 字符,而第一个比特位为 0。

在 C 语言的字符字符串字面量中,除了可以使用字面字符来表示一个字符外,还可以直接用它的码点值来表示,码点值必须使用八进制十六进制数字表示,如果想了解 ASCII 的码表,可以查看 - 7 Bit ASCII Codes 在线码表查询

  • 八进制值,\n\nn\nnn,其中 n、nn、nnn 为 ASCII 字符对应的码点值,最小值为\0,最大值为\177
  • 十六进制值,\xh\xhh,其中 h、hh 为 ASCII 字符对应的码点值,最小值为\x0、最大值为\x7F

在 ASCII 字符集中,码点区间 [0, 31](十进制)的字符均为控制字符,因为这些字符无法直接书写,因此必须使用\nnn\xhh来表示。不过由于码点值太难记,C 语言又定义了其简写形式,完整的列表如下(引自维基百科):

转移序列 十六进制值 字符的意义
\a 07 BEL 警铃符
\b 08 BS 退格符
\t 09 HT 水平制表
\v 0B VT 垂直制表
\r 0D CR 回车符
\n 0A LF 换行符
\f 0C FF 换页符
\e 1B ESC 退出符
\\ 5C 反斜线
\' 27 单引号
\" 22 双引号
\? 3F 问号
\nnn - 八进制值代表的 ASCII 字符
\xhh - 十六进制值代表的 ASCII 字符
\uhhhh - Unicode BMP 码点值(十六进制)
\Uhhhhhhhh - Unicode 码点值(十六进制)

Unicode 码点在线查询 -> Unihan data(中日韩越统一表意文字),提醒一下,\uhhhh\Uhhhhhhhh分别占用 2 字节、4 字节,因此无法在 char 变量中存储!

C 语言输入函数

C 语言输入函数主要有getchargetchegetchgetsscanf,其中getchegetch是 VS 中的函数(在 conio.h 头文件),GCC 中没有,gets在 GCC、VS2017 中已经不能使用了。在代码中,最好使用getcharscanf(后面主要也是讨论它们)。

无论是 getchar 还是 scanf,都是从输入缓冲区中提取(剪切)数据的,当我们输入一行字符串时,这些数据其实还未进入它的输入缓冲区,直到我们按下回车键,shell 才会将这行字符串以及换行符(其实就是我们刚才按下的回车键,只不过被 shell 给转换为了换行符)送入到它的输入缓冲区。

第一次调用 getchar/scanf 时,因为输入缓冲区中没有数据,因此该调用被阻塞,直到缓冲区中有数据流入而被唤醒。被唤醒后,getchar/scanf 开始从输入缓冲区中抽取数据(剪切数据),并且是有多少(至少有数据)取多少。然后当下一次调用 getchar 和 scanf 时,如果输入缓冲区中还有未读完的数据,则该调用不会被阻塞,而是直接读取剩下的数据(有多少读多少);如果输入缓冲区中没有数据了,则该调用会被阻塞,直到输入缓冲区中流入了数据,以此类推。

getchar 单字符

输入任意字符(串)并回车,getchar()只会获取第一个字符,剩下的字符(如果有的话)则还在缓冲区中。

编译并运行,输入 5 个字符

getche 单字符/有回显

获取单个字符,有回显,编译环境是 VS2017

输入a

getch 单字符/无回显

获取单个字符,没有回显,编译环境是 VS2017

输入a

scanf 格式化扫描

scanfprintf是一对,用法也很相似,scanf 是格式化扫描,printf 是格式化打印,并且它们的 format 格式很相似,基本通用,除了 scanf 多出%[^sep](设置分隔符为 sep)、%*type(读取输入,但是不保存它)。

scanf 和 getchar 一样,都不是直接从键盘获取字符,而是当我们按下回车键,将输入数据送入了它们的输入缓冲区后才开始从这个输入缓冲区中获取输入数据的(scanf 和 getchar 共享一个输入缓冲区)。

scanf 中存在所谓的分隔符的概念,默认的分隔符为空白符(空格、制表、回车、换行等)。scanf 在处理输入序列时,先将控制权交给第一个格式占位符(从左到右),格式占位符会忽略所有的前导空白,当此格式占位符无法继续匹配或者遇到了分隔符时,就会让出控制权,将其交给下一个格式占位符,以此类推,直到整个 format 格式字符串匹配完毕。

因此,默认情况下,%s无法获取一整行的字符串,因为会被中间的空白符截断。不过我们可以使用%[^sep]来改变默认的分隔符为 sep,注意不能少了前面的 ^,如果没有了 ^,则表示除 sep 外的任意字符都为分隔符,很显然不是我们想要的。现在,我们就可以使用scanf("%[^\n]", str)来获取一整行字符串了(不包括换行符,因为它是我们指定的分隔符)。

说了这么多,我们先来感性的认识一下 scanf 格式化扫描函数:

可以发现,scanf对于空白符的数量并不要求严格匹配。我们也可以多次调用 scanf 来读完输入缓冲区的剩余数据。

我们来看看如何用scanf录入字符串,以及如何录入带有空格的字符串(即获取一整行字符串):

因为scanf遇到分隔符就停止扫描了,所以只保存了第一个单词my,那怎么输入带有空白符(默认分隔符)的字符串呢?我们可以用%[^\n]来指定scanf的分隔符为\n换行符,这样就可以录入完整的一行字符串了。

清空输入缓冲区

我们先来看这段代码:

这个程序用来打印输入的字符的 ASCII 码,但是很不幸,由于输入缓冲区的存在,并不能达到我们要的结果:

我们只输入了第一个字符,就直接显示了结果,为什么会这样呢?仔细观察可以发现,输入 a,然后回车,直接打印了 97 和 10,查看 ASCII 码表,发现对应的 ASCII 字符为 a 和换行符。

其实很好理解,因为输入缓冲区的存在,导致第二次调用 getchar 时直接从输入缓冲区中读取了剩余的字符,而未给我们机会来输入下一个字符。

那要如何实现我们想要的结果呢?只要清空输入缓冲区就行。Windows 用fflush(stdin)rewind(stdin),Linux 则要用setbuf(stdin, NULL),这三个函数都在stdio.h头文件中。

非阻塞式键盘监听

输入函数被执行的时候,线程因为等待 IO 会被挂起,只有等我们输入完成后,线程才会被唤醒,继续执行后面的代码,这种就是阻塞式键盘监听(scanf、getchar)。

现在我们设计一个小程序,该程序每隔 500ms 打印一行字符串,只有当我们按下 ESC 键后程序才会退出。如果用之前的方法,需要每次都按键才能使得程序继续运行,显然达不到我们的要求,这时候就需要非阻塞式键盘监听了(仅限 Windows)。

kbhit()函数用来检测键盘缓冲区是否有按键被按下,如果有则返回非 0 值,没有则返回 0,kbhit()不会读取缓冲区的字符,字符还在缓冲区。

注意,下面的程序只能在 Windows 下(VS2017)运行,因为 GCC 没有conio.h头文件。

按下ESC键时,会跳出循环