文章目录
前言
scanf和printf是两个重要的C语言输入输出函数。
scanf函数用于从stdin(标准输入设备(如键盘))读取数据,并将数据存储到指定的变量中。
printf函数用于将数据输出到stdout(标准输出设备(如屏幕))。
事实上他们是一对格式化输入输出函数,它们中含的f
便是format(格式化之意),所谓的格式化是指他们需要以固定格式的输入或输出。
一、scanf函数介绍
scanf 的功能用一句话来概括就是“通过键盘给程序中的变量赋值”。
1.函数声明:
函数声明:
# include <stdio.h> int scanf(const char *format, additional arguments);
1)该函数需要引用头文件stdio.h
2.参数:
1)const char *format:
这个参数主要包括两类:格式化输入控制符 或者 输入控制符与非输入控制符的组合
按第一类格式输入时,第一个参数只输入输入控制符,一般是%
+别的符号;
按第二类格式输入时,第一个参数可以输入自己想要的一些不用作后续操作的输入,但是需要注意,这里你在函数参数中写了什么,就要在键盘输入时输入什么,格式必须保持完全一致。
2)additional arguments:
这些参数应该是指针:为了在常规变量上执行scanf操作并存储结果,其名称前应该加上取地址操作符(&),这些参数的数量至少应该与格式说明符所存储的值的数量相同。额外的参数会被该函数忽略。
举例如下:
1.参数一 类型为格式化输入控制符
# include <stdio.h> int main(void) { int a; scanf("%d", &a); //&a 表示变量a的地址,&是取地址符 printf("a = %d\n", a); return 0; }
2.参数一 类型为输入控制符与非输入控制符的组合
# include <stdio.h> int main(void) { int a; scanf("a = %d", &a); printf("a = %d\n", a); return 0; }
例1中,你只需要输入你想输入的值,然后按下回车键即可:
9
+ENTER
键
例2中,你需要按照原样输入非输入控制符:
a = 9
+ENTER
键
3.返回值:
scanf函数返回的是int型的数据,scanf函数返回成功读入的数据项数,读入数据时遇到了“文件结束”则返回EOF
。(EOF是文件结束标志End Of Files)
例如:
scanf的返回值
scanf("%d%d", &a, &b);
若a和b都读取成功,则返回
2
。
若只有a被成功读入,返回值为1
;
若a读取失败,返回值为0;
若遇到错误或遇到end of file,返回值为EOF
。
仅从函数的定义来看这个函数的使用是非常简单的,但事实上并非如此,scanf函数在使用时有很多容易出问题的地方。正是因为scanf函数使用上的诸多不便,所以C++虽然保留了兼容C语言的scanf函数,但是也开发了自己的输入流函数cin
。
二、scanf函数初阶
1.scanf函数的输入原理
在 scanf 中,从键盘输入的一切数据,不管是数字、字母,还是空格、回车、Tab 等字符,都会被当作数据存入缓冲区。存储的顺序是先输入的排前面,后输入的依次往后排。按回车键的时候 scanf 开始进入缓冲区取数据,从前往后依次取。格式控制符是告诉计算机在缓冲区去寻找对应类型的值,在寻找对应的值的时候,会跳过不同类型的值,并将它从缓冲区释放,直至找到合适的值。未被跳过或取出的数据,系统会将它一直放在缓冲区中,直到下一个 scanf 来获取。数据未被接收
** 例 scanf的输入原理**
# include <stdio.h> int main(void) { int a; scanf("%d", &a); //&a表示变量 a 的地址,&是取地址符 printf("a= %d\n",a); return 0; }
首先我们需要知道,从键盘输入的全部都是字符。比如从键盘输入 123
,它表示的并不是数字 123
,而是字符 '1'
、字符 '2'
和字符 '3'
。这是为什么呢?
操作系统内核就是这样运作的。操作系统在接收键盘数据时都将它当成字符来接收的。这时就需要用“输入控制符”
将它转化一下。例如:%d
的含义就是要将从键盘输入的这些合法的字符转化成一个十进制数字。经过 %d 转化完之后,字符 123 就是数字 123 了。
而&
是一个取地址运算符,&
后面加变量名表示“该变量的地址”,所以&a
就表示变量 a的地址。&a
又称为“取地址a”,就相当于将数据存入以变量 a的地址为地址的变量中。
综上所述,scanf 语句的意思就是:从键盘上输入字符 123
,然后%d
将这三个字符转化成十进制数 123
,最后通过“取地址 a”找到变量 a 的地址,再将数字 123 放到以变量 a 的地址为地址的变量中,即变量 a 中,所以最终的输出结果就是a=123。
接下来我们先了解一下scanf可以从stdin
中读取什么样的数据。
1)空白字符和非空白字符
- 空白字符串:空白字符会使scanf函数在读操作中略去输入中的一个或多个空白字符。空白符可以是空格、制表符和新行符。
控制串中的空白符使 scanf() 在输入流中跳过一个或多个空白行。 本质上,控制串中的空白符使 scanf() 在输入流中读,但不保存结果,直到发现非空白字符为止。
在输入流中,如果需要读入的数据是非字符类型的多组数据,如%d
(整数类型)时,数据项必须由空格、制表符和新行符分割。空白字符是给计算机信号让它在这个地方断开去读下一个数据。
例子如下:
例1 输入一个数据
char c; scanf("%c",&c);
输入:
a
接收:a
例2 输入多个数据int a, b; scanf("%d%d",&a, &b);
输入:
20 30
接受:20 30
若输入:
2030
接收:2030
(一个数)
- 非空白字符串:一个非空白字符会使scanf()函数在读入时剔除掉与这个非空白字符相同的字符。
非空白符使 scanf() 在流中读一个匹配的字符并忽略之。例如,“%d,%d” 使 scanf() 先读入一个整数,读入中放弃逗号,然后读另一个整数。如未发现匹配,scanf() 返回。
例1 非空白字符串
int a, b; scanf("%d,%d", &a, &b);
输入:
20,30
接收:20 30
2)使用EOF进行多组输入
在scanf函数从stdin
中读到文件结束,也就是换行符ENTER
(键盘输入时每一个键都会被输入)时,会返回EOF
,可以利用这一特点输入多组数据,直到按下ENTER
例 输入多组数据
int main() { int a, b; while (scanf("%d %d", &a, &b) != EOF) { // 注意 while 处理多个 case printf("%d\n", a + b); } return 0; }
输入:
20 30 40 50 60 70
接收:三组a,b的值
3)未被接收的数据
点我跳转到:数据为什么未被接收
C编译在碰到空格,制表符,回车或非法数据(如对“%d”输入“12A”时,A即为非法数据)时即认为该数据结束。未被接收到的数据会被存储在键盘缓冲区,在下次scanf函数使用时开始接收。
例 带空格的输入
#include <stdio.h> int main() { char a[20]; scanf("%s", &a); printf("%s\n", a); }
运行结果:
上述程序并没有hello world
。因为scanf扫描到o
后面的空格就认为对str的扫描结束(空格没有被扫描),并忽略后面的world
。残留的信息 "world"是存在于stdin流中,而不是在键盘缓冲区中。如何输入带有空格的字符串
2.scanf函数格式化输入
scanf函数的格式化控制符为:"%[modifiers]type"
type
是一个字符,指定了要被读取的数据类型以及数据读取方式。具体参见表格。
类型 | 合格的输入 | 参数的类型 |
---|---|---|
%a、%A | 读入一个浮点值(仅 C99 有效). | float * |
%c | 单个字符:读取下一个字符。如果指定了一个不为 1 的宽度 width,函数会读取 width 个字符,并通过参数传递,把它们存储在数组中连续位置。在末尾不会追加空字符。 | char * |
%d | 十进制整数:数字前面的 + 或 - 号是可选的。 | int * |
%e、%E、%f、%F、%g、%G | 浮点数:包含了一个小数点、一个可选的前置符号 + 或 -、一个可选的后置字符 e 或 E,以及一个十进制数字。两个有效的实例 -732.103 和 7.12e4 | float * |
%i | 读入十进制,八进制,十六进制整数 。 | int * |
%o | 八进制整数。 | int * |
%s | 字符串。这将读取连续字符,直到遇到一个空格字符(空格字符可以是空白、换行和制表符)。 | char * |
%u | 无符号的十进制整数。 | unsigned int * |
%x、%X | 十六进制整数。 | int * |
[modifiers]
为对应的附加参数所指向的数据指定一个不同于整型(针对 d、i 和 n)、无符号整型(针对 o、u 和 x)或浮点型(针对 e、f 和 g)的大小: h :短整型(针对 d、i 和 n),或无符号短整型(针对 o、u 和 x) l :长整型(针对 d、i 和 n),或无符号长整型(针对 o、u 和 x),或双精度型(针对 e、f 和 g) L :长双精度型(针对 e、f 和 g)
附加参数 – 根据不同的 format 字符串,函数需要一系列的附加参数,每个参数包含了一个要被插入的值,替换了 format 参数中指定的每个 % 标签。参数的个数应与 % 标签的个数相同。
3.初阶使用时遇到的问题
1)输入类型与格式化字符不匹配导致stdin流的阻塞
在 printf 中,“输出控制符”的类型可以与数据的类型不一致,如:
# include <stdio.h>
int main(void)
{
int i = 97;
printf("i = %c\n", i);
return 0;
}
输出结果是:i = a
printf函数会自动在ASCII码值和数字之间切换,取决于你想打印出什么格式化类型。
但是在 scanf 中,对于从键盘输入的数据的类型、scanf 中“输入控制符”的类型、变量所定义的类型,这三个类型一定要一致,否则就是错的。虽然编译的时候不会报错,但从程序功能的角度讲就是错的,则无法实现我们需要的功能。比如:
# include <stdio.h>
int main(void)
{
int i;
scanf("%d", &i);
printf("i = %d\n", i);
return 0;
}
键盘输入:a
输出结果是:i = -858993460
我们从前面可以知道,scanf 中 %d 只识别“十进制整数”。对 %d 而言,空格、回车、Tab 键都是区分数据与数据的分隔符。当 scanf 进入缓冲区中取数据的时候,如果 %d 遇到空格、回车、Tab 键,那么它并不取用,而是跳过继续往后取后面的数据,直到取到“十进制整数”为止。
但是如果 %d 遇到字母,那么它不会跳过也不会取用,而是直接从缓冲区跳出。所以上面这个程序,虽然 scanf 进入缓冲区了,但用户输入的是字母 a,所以它什么都没取到就出来了,而变量 i 没有值,即未初始化,所以输出就是 –858993460。
这里我们可以使用getchar()
去将冲区错误输入的a给释放掉。
但如果将 %d 换成 %c,那么任何数据都会被当作一个字符,不管是数字还是空格、回车、Tab 键它都会取回。
不但如此,前面讲过,你从键盘输入 123,这个不是数字 123,而是字符 ‘1’、字符 ‘2’ 和字符 ‘3’,它们依次排列在缓冲区中。因为每个字符变量 char 只能放一个字符。所以输入“123”之后按回车,scanf 开始进入缓冲区,按照次序,先取字符 ‘1’,如果还要取就再取字符 ‘2’,以此类推。
如果都取完了还有 scanf 要取数据,那么用户就需要再输入。
有多个输入时这种情况会更严重: 导致输入流的阻塞无法输入正确的数据
看下面这个段代码
#include <stdio.h>
int main()
{
int a = 0, b = 0, c = 0, ret = 0;
ret = scanf("%d%d%d", &a, &b, &c);
printf("第一次读入数量:%d\n", ret);
ret = scanf("%c%d%d", &a, &b, &c);
printf("第二次读入数量:%d\n", ret);
return 0;
}
运行结果如下:
第一次输入 :1 b 2
与前面的情况一样%d
遇到字母,那么它不会跳过也不会取用,而是直接从缓冲区跳出,所以第一次只输入了一个数据,其他两个数据b 2
还在缓冲区,第二次键盘只输入3
,那么缓冲区里正好是b 2 3
能够被第二次格式化打印输出。
如果这个地方不处理,缓冲区里存在别的数,那么之后再输入就会造成阻塞,解决方法就是使用fflush函数,在每次输入后刷新缓冲区。
#include <stdio.h>
int main()
{
int a = 0, b = 0, c = 0, ret = 0;
ret = scanf("%d%d%d", &a, &b, &c);
fflush(stdin);
printf("第一次读入数量:%d\n", ret);
ret = scanf("%c%d%d", &a, &b, &c);
fflush(stdin);
printf("第二次读入数量:%d\n", ret);
return 0;
2)scanf()函数无法正确接收有空格的字符串
前面我们就了解到scanf函数无法接收空格的字符串,它在读到空格时会认为是要去读取下一个数据了,并不会读到空格。
那么我们如何解决这个问题,简单的方法是使用gets()函数,它可以直接输入带有空格的字符串。
需要注意的是gets函数在 C11 和 C++14已经不存在了。
例:
#include <stdio.h>
int main() {
char arr[100];
//scanf("%s", &arr);
gets(arr);
printf("%s\n", arr);
}
运行结果:
当然使用scanf也可以做到,具体做法会在下面的高阶用法讲到,学习完高阶用法可以使用scanf函数解决这个问题。空格问题具体解决方法
4.初阶使用注意事项
刚开始使用函数要注意以下几点:
a) 在 scanf 的“输入参数”中,变量前面的取地址符&不要忘记。
b) scanf 中双引号内,除了“输入控制符”外什么都不要写。
c) “输出控制符”和“输出参数”无论在顺序上还是在个数上一定要一一对应。
d) “输入控制符”的类型和变量所定义的类型一定要一致。
e) 对于字符串数组或字符串指针变量,由于数组名可以转换为数组和指针变量名本身就是地址,因此使用scanf()函数时,不需要在它们前面加上"&"操作符。
三、scanf函数进阶
1.进阶格式化输入(附加格式说明符)
scanf函数除了基础的格式输入外,还有一些更高级的使用,巧妙的使用他们可以直接对数据进行进一步处理。
scanf函数的格式化控制符为[=%[*][width][modifiers]type=]
高阶格式 | 说明 |
---|---|
width(域宽) | 这指定了在当前读取操作中读取的最大字符数。 |
* | 这是一个可选的星号,表示数据是从流 stream 中读取的,但是可以被忽视,即它不存储在对应的参数中。 |
^ | 不匹配有些字符 |
1)使用域宽读取指定长度
还记得在 printf() 中可以指定最小输出宽度吗?
就是在格式控制符的中间加上一个数字,例如,%10d
表示输出的整数至少占用 10 个字符的位置:
- 如果整数的宽度不足 10,那么在左边以空格补齐;
- 如果整数的宽度超过了 10,那么以整数本身的宽度来输出,10 不再起作用。
其实,scanf() 也有类似的用法,也可以在格式控制符的中间加一个数字,用来表示读取数据的最大长度,例如:
%2d
表示最多读取两位整数;%10s
表示读取的字符串的最大长度为 10,或者说,最多读取 10 个字符。
我们可以使用这个用法做做这个题目:
牛客网链接: 出生日期输入输出
把答案放到这里,做完之后可以参考一下:
int main() {
int year, month, day;
scanf("%4d%2d%2d", &year, &month, &day);
printf("year=%d\n", year);
printf("month=%02d\n", month);
printf("date=%02d\n", day);
return 0;
}
2)匹配特定字符
%s
可以匹配除空白符以外的所有字符,但不能读取特定的字符,比如只想读取小写字母,或者十进制数字等,%s
就无能为力。时可以使用[]
包含上需要的范围,也就是%[xxx]
:
比如:%[abcd]表示只读取字符abcd,遇到其它的字符就读取结束;
例 匹配特定字符
#include <stdio.h> int main() { char str[30]; scanf("%[abcd]", str); printf("%s\n", str); return 0; }
运行结果
使用连接符
为了简化字符集合的写法,scanf() 支持使用连字符-
来表示一个范围内的字符,例如 %[a-z]
、%[0-9]
等。
例 使用连接符匹配特定字符
#include <stdio.h> int main() { char str[30]; scanf("%[a-z]", str); printf("%s\n", str); return 0; }
运行结果
3)丢弃不需要的字符
使用*
,可以把读取到的数据直接丢弃,不往变量中存放。
例如:
%*d
表示读取一个整数并丢弃;%*[a-z]
表示读取小写字母并丢弃;
例 丢弃不需要的字符
#include <stdio.h> int main() { int n; scanf("%*d %d", &n); printf("n=%d\n", n); return 0; }
运行结果
4)不匹配特定字符
使用^
符号可以直接指定某些不能匹配的字符,在读到该字符时直接停止读取。
例如:
%[^\n]
表示匹配除换行符以外的所有字符,遇到换行符就停止读取;%[^0-9]
表示匹配除十进制数字以外的所有字符,遇到十进制数字就停止读取。
学到这里我们就可以解决输入空格的问题了:输入空格问题的解决方法
#include <stdio.h>
int main()
{
char a[20];
scanf("%[^\n]s", &a);
printf("%s\n", a);
}
运行结果:
使用键盘输入时我们最后需要敲ENTER
键也就是\n
才会让scanf开始读取所以我们需要使用%[^\n]
让它读取换行符之前的数据。
2.进阶使用的注意事项
- 在高版本的 Visual Studio 编译器中,scanf 被认为是不安全的,被弃用,应当使用scanf_s代替 scanf。
但是这样做的话代码的兼容性不好,不可移植,所以笔者并不建议这么写。 - scanf函数中没有类似printf的精度控制。
如:scanf(“%5.2f”,&a); 是非法的。不能企图用此语句输入小数为2位的实数。
总结
scanf函数是一个非常常用的函数,很多人只知道它的简单用法,在学习过高阶的使用后,可以解决很多在使用的过程中遇到的问题也可以更好地使用scanf函数去处理数据、应用更多场景。