C中PRINTF和SCANF的一些知识

暑假在家看《C Primer Plus》,有一部分对 printf 和 scanf 函数讲得比较细,所以想要写一篇笔记记录一下这些知识。

一、关于 printf 函数

printf 和 scanf 函数是 C 语言中的输入/输出函数(I/O 函数),可以使用这两个函数来实现程序和用户的交互。也是 C 语言初学者所接触到的最早的两个函数了,printf 函数用于格式化输出信息,我们最早会用这个函数来编写“Hello world”程序,想必对于 C 语言的学习者来说是最熟悉不过的函数了。

1.函数的参数格式

printf 函数与一般我们定义的函数不同,它是没有固定参数个数的。它使用“格式字符串+参数列表”的形式获取参数,即“格式字符串”是 printf 的第一个参数,其中包含了后续参数列表中的参数个数和输出信息的格式等信息。如:

1
printf("My name is %s, %d years old.", name, age);

上面的“My name is %s, %d years old.”就是格式字符串,而后面跟着的 name 和 age 是需要打印其值的变量,这两个变量的值分别替换字符串中的“%s”和“%d”,然后 printf 函数将替换后的文本打印到控制台中的当前位置。由此可见,printf 函数可以通过格式字符串中的“%s”等(转换说明)获取到参数的数量并加以处理。

格式字符串后面的参数列表中的参数可以是变量、常量或者是需要在打印之前先计算其值的表达式。格式字符串中应该包含“实际需要打印的字符”和“转换说明”两种信息。若格式字符串中没有转换说明,那么也可以没有参数列表,如:

1
printf("My name is Ming, 16 years old");

这样即表明格式字符串中都是实际需要打印的字符,没有需要替换的转换说明,printf 将会把格式字符串中的字符串原样打印到控制台的当前位置。

2.函数的转换说明

上一段中提到了“转换说明”,就是在格式字符串中类似于“%s”的字符组合。它在格式字符串中起到了“占位符”的作用,即标记了待插入内容将要插入到字符串中的位置,并表明了待插入内容的数据格式。printf 函数将转换说明和后面参数列表中的参数一一对应,一个转换说明对应一个参数,使用转换说明表明的数据格式读取参数(变量或者常量)中的值,将以二进制格式存储在计算机中的数据转换成一系列字符串,并将转换说明用读取出来的值替换掉,再进行下一个参数的读取和插入,直到转换说明都被替换掉为止。

即在上一个例子中,

1
printf("My name is %s, %d years old.", name, age);

printf 先读取到了格式字符串,了解到了在格式字符串后面应该有两个参数用于替换转换说明,“%s”表明了后面参数列表中的第一个参数是字符串,所以 printf 函数用字符串的数据格式将 name 变量中的值读取出来,并用这个值替换了“%s”。按照这样的方式也替换了“%d”,如果还有更多的转换说明,那么以此类推都替换掉。

如果上面的 name 变量的值为“Ming”,age 的值为 16,那么被替换过的字符串就变成了“My name is Ming, 16 years old.”并被打印到控制台的当前位置。

有很多不同的转换说明支持转换不同类型的数据(这里“转换”的意思就是将以二进制格式存储在内存中的数据“翻译”成字符串并插入格式字符串中),常用的有以下一些:

转换说明 输出
%a 浮点数、十六进制数和 p 计数法
%c 单个字符
%d 有符号十进制数
%f 浮点数,十进制记数法
%e 浮点数,e 计数法
%o 无符号八进制数
%p 指针
%s 字符串
%u 无符号十进制数
%x 无符号十六进制数(小写)
%% 打印一个百分号

另外,格式字符串中的转换说明一定要与后面的参数数据类型相匹配,若转换说明个数和参数个数不同或者转换说明与参数类型不匹配,会导致不确定的后果(如打印出无意义的值或者程序停止运行等)。和转换说明一起使用的还有“转换说明修饰符”,用于修饰转换说明,常用的几个如下:

修饰符 含义
数字 最小字段宽度
.数字 精度,%e或%f表示小数点后面位数
h 与整型说明一起使用表示short类型
hh 与整型说明一起使用表示char类型
l 与整型说明一起使用表示long类型
ll 与……一起使用表示long long类型
L 与浮点说明使用表long double类型
z 与整型说明一起使用表示size_t类型
- 待打印项左对齐
+ 显示正负号
空格 正数显示空格,负数显示负号
# 使用能表明数据进制的形式显示
0 对于数值,使用0代替空格填充宽度

3.函数的打印机制和其返回值

有时候即使用对了大部分转换说明也可能会出现下面这种情况:

一段程序

这段程序中 printf 虽然用对了 n3 和 n4 的转换说明,但是 n3 和 n4 还是没有被正确打印出来。其中的原因就在 printf 的参数传递过程和打印机制中。

以上代码在调用 printf 函数的时候,计算机根据程序中声明的变量类型将传递给函数的参数变量 n1, n2,n3,n4存入一个被称为“栈(stack)”的内存区域内,每个变量在存储时在内存中都是相邻的,如图:

stack 示意图

在栈中,n1 和 n2 各占用 8 字节,n3 和 n4 各占用 4 字节,4 个变量相邻地存储在栈中。

当 printf 开始按照所给的转换说明在栈中读取变量时,它先按照第一个转换说明“%ld”在栈中从头开始读取了 4 字节(n1 的完整数据应该是 8 字节),即读取了 n1 变量的一半(第一个方块)按照 long 类型解释,于是显示了一个错误的值。

接着 printf 按照第二个转换说明接着读取 4 字节的内容(第二个方块),再次错误地翻译了二进制数据并显示了;接下来根据第三个转换说明从当前栈中的位置继续读取 4 字节内容,又错误地显示了……

因为 printf 每次从栈中读取数据都是从当前位置读取,所以当第一个转换说明错误以后,printf 在第一次读取数据时读错了字节,再次读取时就“错位”了,因此也就不能正确的显示哪怕是正确的转换说明了。

printf 函数返回成功打印字符的个数,这个特性可以用来检查是否成功打印字符。若输出错误,printf 函数会返回一个负值。

4.打印较长的字符串

如果 printf 要打印较长的字符串,会导致语句太长不方便阅读。因为在 C 语言中使用空白字符(空格、制表符和换行符等)分隔不同的代码部分时,编译器会忽略它们,所以一条 printf 语句可以写成多行的形式。如参数与参数之间换行:

1
2
printf("Something long to print, something %s long to print, %s",
sth1, sth2);

但是不能在格式字符串中间换行,这样会导致字符串中包含非法字符。

《C Primer Plus》中写道如果想要在字符串中换行,有以下三种方法:

  • 使用多个 printf 语句;
  • 在字符串中使用“\”反斜杠加 Enter 键换行;
  • ANSI C 引入的字符串连接,使用两个带双引号字符串之间带空白隔开,编译器会将它们链接为一个字符串。

例:

printf 长字符串三种形式

二、关于 scanf 函数

C 语言包含很多输入函数,除了从标准输入输入,还有能够从文件读入数据的函数等。但在控制台和用户交互时,scanf 是比较通用的一个,因为它可以格式化地读取用户输入的数据。scanf 函数的功能是将读取的字符串转换成整数、浮点数、字符或字符串等,与 printf 的功能相反。

1.函数的参数格式和转换说明

scanf 函数的参数格式和 printf 函数相似,也使用“格式字符串+参数列表”的形式。格式字符串中也使用转换说明,用法和 printf 类似,但是用一点需要说明的是参数列表中的参数是变量的地址,scanf 会将读取的数据写入参数列表中提供的地址。

格式字符串中除了转换说明外都是“需要用户实际输入的字符”,scanf 函数会跳过格式字符串中转换说明以外的字符,在转换说明所在的位置读取用户输入的内容(空字符不是严格匹配,除了“%c”,其他转换说明都会忽略输入项前面的所有空白字符)。同时转换说明还为 scanf 函数表明了需要将用户输入的字符串转换成何种类型的数据,这样 scanf 函数就能用正确的格式编码输入的字符串,并将其存储到指定的相应类型变量中了。

scanf 函数的转换说明和 printf 函数基本相同,需要说明的一点是对于 double 类型的转换说明,printf 函数使用“%f”,而 scanf 使用“%lf”。

2.读取用户输入的过程

按照《C Primer Plus》中的例子,我们假设 scanf 根据“%d”转换说明读取一个整数,从这里开始看它的读取过程。

1
scanf("%d", &a);

用户在输入区输入了“ 1A”( 1 前面有三个空格)并按下了回车,按下回车的时候,系统将输入区用户输入的字符串一并发送给 scanf 函数,函数开始处理字符串,从字符串的开头开始,一次读取一个字符,跳过空白字符,即对用户输入的三个空格不予理会直接跳过。接着读取到了一个“1”字符,发现它是数字,继续读取下面一个字符“A”,发现“A”不是数字了。于是将 1 转换成整数的格式传入了 a 变量,并将无法处理的字符“A”放回到用户的输入区。

如果用户直接输入了“A”怎么办呢?

和上面的过程相似,但是当 scanf 读取到“A”的时候将“A”放回到用户输入区,无法给变量 a 赋值。若用户再次运行这句语句,并接着“A”输入了一个数字的话,scanf 还是不能读取到“A”后面的数字,因为被放回到输入区中的“A”再次被发送给 scanf 读取到并又放回了输入区中。如图:

scanf 读取过程

因为 scanf 会忽略用户输入的空白字符,所以我们可以用空白字符来分隔不同的数据,如:

1
scanf("%d%d", &num1, &num2);

输入“123 234”,空格会将 123 和 234 分成两个数字并分别读入 num1 和 num2 中,或者将空格换成回车也有同样的效果。

但是若使用“%c”,空白就不会不忽略了,如:

1
scanf("%d%c", &num, &chara);

若用户输入了“3<回车>”,那么 3 会被读入 num 中,换行符会被读入 chara 中;或者输入“3 A”,那么3会被读入 num 中,空格会被读入 chara 中,“A”会被 scanf 函数放回输入区。

3.函数的返回值

scanf 函数返回成功读取的项数,若没有成功读取则返回 0(如转换说明是一个“%d”,但用户却输入了一个字符,这就不算成功读取),当 scanf 读取到文件结尾时会返回 EOF。(使用 scanf 读取文件?)

通常我们可以使用 scanf 的返回值来判断用户是否进行了正确的输入,从而判断是否结束循环、处理不合理的输入或者进行其他操作等。如:

使用函数返回值判断是否结束循环

上图就是一个要求用户输入整数来循环的例子,用户可以通过输入一个字母等来退出循环。

三、printf 和 scanf 的 * 修饰符

在 printf 函数中,“”修饰符可以替代转换说明的修饰符,并用参数列表中相应位置的参数来替换“”修饰符,从而可以使用程序中的变量来修改输出数据的格式。如:

printf 的 * 修饰符

在上面的程序中, printf 中格式字符串里的第一个“”会被 width 变量的值替换,第二个“”会被 precision 的值替换,即变为“%8.2f”,再进行 data 的处理。

使用“*”修饰符可以让用户来决定数据的输出格式,更增加了程序的实用性和方便性。

以上。


C中PRINTF和SCANF的一些知识
https://maphical.cn/2017/09/something-about-scanf-in-c/
作者
MaphicalYng
发布于
2017年9月9日
许可协议