C中PRINTF和SCANF的一些知识
暑假在家看《C Primer Plus》,有一部分对 printf 和 scanf 函数讲得比较细,所以想要写一篇笔记记录一下这些知识。
一、关于 printf 函数
printf 和 scanf 函数是 C 语言中的输入/输出函数(I/O 函数),可以使用这两个函数来实现程序和用户的交互。也是 C 语言初学者所接触到的最早的两个函数了,printf 函数用于格式化输出信息,我们最早会用这个函数来编写“Hello world”程序,想必对于 C 语言的学习者来说是最熟悉不过的函数了。
1.函数的参数格式
printf 函数与一般我们定义的函数不同,它是没有固定参数个数的。它使用“格式字符串+参数列表”的形式获取参数,即“格式字符串”是 printf 的第一个参数,其中包含了后续参数列表中的参数个数和输出信息的格式等信息。如:
1 |
|
上面的“My name is %s, %d years old.”就是格式字符串,而后面跟着的 name 和 age 是需要打印其值的变量,这两个变量的值分别替换字符串中的“%s”和“%d”,然后 printf 函数将替换后的文本打印到控制台中的当前位置。由此可见,printf 函数可以通过格式字符串中的“%s”等(转换说明)获取到参数的数量并加以处理。
格式字符串后面的参数列表中的参数可以是变量、常量或者是需要在打印之前先计算其值的表达式。格式字符串中应该包含“实际需要打印的字符”和“转换说明”两种信息。若格式字符串中没有转换说明,那么也可以没有参数列表,如:
1 |
|
这样即表明格式字符串中都是实际需要打印的字符,没有需要替换的转换说明,printf 将会把格式字符串中的字符串原样打印到控制台的当前位置。
2.函数的转换说明
上一段中提到了“转换说明”,就是在格式字符串中类似于“%s”的字符组合。它在格式字符串中起到了“占位符”的作用,即标记了待插入内容将要插入到字符串中的位置,并表明了待插入内容的数据格式。printf 函数将转换说明和后面参数列表中的参数一一对应,一个转换说明对应一个参数,使用转换说明表明的数据格式读取参数(变量或者常量)中的值,将以二进制格式存储在计算机中的数据转换成一系列字符串,并将转换说明用读取出来的值替换掉,再进行下一个参数的读取和插入,直到转换说明都被替换掉为止。
即在上一个例子中,
1 |
|
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)”的内存区域内,每个变量在存储时在内存中都是相邻的,如图:
在栈中,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 |
|
但是不能在格式字符串中间换行,这样会导致字符串中包含非法字符。
《C Primer Plus》中写道如果想要在字符串中换行,有以下三种方法:
- 使用多个 printf 语句;
- 在字符串中使用“\”反斜杠加 Enter 键换行;
- ANSI C 引入的字符串连接,使用两个带双引号字符串之间带空白隔开,编译器会将它们链接为一个字符串。
例:
二、关于 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 |
|
用户在输入区输入了“ 1A”( 1 前面有三个空格)并按下了回车,按下回车的时候,系统将输入区用户输入的字符串一并发送给 scanf 函数,函数开始处理字符串,从字符串的开头开始,一次读取一个字符,跳过空白字符,即对用户输入的三个空格不予理会直接跳过。接着读取到了一个“1”字符,发现它是数字,继续读取下面一个字符“A”,发现“A”不是数字了。于是将 1 转换成整数的格式传入了 a 变量,并将无法处理的字符“A”放回到用户的输入区。
如果用户直接输入了“A”怎么办呢?
和上面的过程相似,但是当 scanf 读取到“A”的时候将“A”放回到用户输入区,无法给变量 a 赋值。若用户再次运行这句语句,并接着“A”输入了一个数字的话,scanf 还是不能读取到“A”后面的数字,因为被放回到输入区中的“A”再次被发送给 scanf 读取到并又放回了输入区中。如图:
因为 scanf 会忽略用户输入的空白字符,所以我们可以用空白字符来分隔不同的数据,如:
1 |
|
输入“123 234”,空格会将 123 和 234 分成两个数字并分别读入 num1 和 num2 中,或者将空格换成回车也有同样的效果。
但是若使用“%c”,空白就不会不忽略了,如:
1 |
|
若用户输入了“3<回车>”,那么 3 会被读入 num 中,换行符会被读入 chara 中;或者输入“3 A”,那么3会被读入 num 中,空格会被读入 chara 中,“A”会被 scanf 函数放回输入区。
3.函数的返回值
scanf 函数返回成功读取的项数,若没有成功读取则返回 0(如转换说明是一个“%d”,但用户却输入了一个字符,这就不算成功读取),当 scanf 读取到文件结尾时会返回 EOF。(使用 scanf 读取文件?)
通常我们可以使用 scanf 的返回值来判断用户是否进行了正确的输入,从而判断是否结束循环、处理不合理的输入或者进行其他操作等。如:
上图就是一个要求用户输入整数来循环的例子,用户可以通过输入一个字母等来退出循环。
三、printf 和 scanf 的 * 修饰符
在 printf 函数中,“”修饰符可以替代转换说明的修饰符,并用参数列表中相应位置的参数来替换“”修饰符,从而可以使用程序中的变量来修改输出数据的格式。如:
在上面的程序中, printf 中格式字符串里的第一个“”会被 width 变量的值替换,第二个“”会被 precision 的值替换,即变为“%8.2f”,再进行 data 的处理。
使用“*”修饰符可以让用户来决定数据的输出格式,更增加了程序的实用性和方便性。
以上。