目录
1 scanf 介绍
如下图所示,程序员能够为程序输入数据,经程序处理后会产生输出。在 C 语言里,借助函数库能读取标准输入(stdin,一般为键盘输入),然后通过相应函数处理把结果展示在屏幕上。
此前我们学习了 printf 函数,了解到可以通过此函数将结果输出至屏幕。接下来重点阐述标准输入函数 scanf 。
scanf 函数作为 C 标准库中极为重要的输入函数,用于从标准输入(stdin,通常为键盘输入)获取格式化的输入。它能够让程序依照指定的格式字符串来剖析输入的数据,并把解析所得的数据存储到指定的变量当中。
2 scanf 格式语法
#include <stdio.h>
int scanf(const char *format, ...);
- format:一个格式字符串,指定了后续参数应该如何被读取和转换。格式字符串由普通字符(直接复制到输出流中)、格式说明符(指定了如何解析输入数据)以及空白字符(如空格、制表符、换行符和换页符,用于分隔输入中的值)组成。
- ...:表示可变数量的参数,这些参数是指向变量的指针,用于存储 scanf 解析出的数据。
3 scanf 格式说明符
scanf 函数和 printf 函数在 C 语言中用于输入和输出,它们都使用格式说明符来指定如何处理数据。下面是 scanf 函数常用格式说明符的一个表格:
scanf 格式说明符 | 含义 | 示例 |
---|---|---|
%d | 读取十进制整数 | int intVar; scanf("%d", &intVar); |
%i | 与 %d 相同,读取十进制整数(有的编译器也支持十六进制和八进制) | int intVar; scanf("%i", &intVar); |
%u | 读取无符号十进制整数 | unsigned int unsignedIntVar; scanf("%u", &unsignedIntVar); |
%f | 读取一个浮点数(单精度 float 类型) | float floatVar; scanf("%f", &floatVar); |
%lf | 读取一个浮点数(双精度 double 类型) | double doubleVar; scanf("%lf", &doubleVar); |
%s | 读取字符串(直到遇到空白字符为止) | char strVar[50]; // 假设字符串不会超过49个字符加上一个空字符 scanf("%s", strVar); |
%c | 读取单个字符 | char charVar; scanf(" %c", &charVar);//注意%c前面的空格是为了跳过任何之前的空白 |
%x 或 %X | 读取十六进制整数 | int hexVar; scanf("%x", &hexVar); 或 scanf("%X", &hexVar); |
%o | 读取八进制整数 | int octalVar; scanf("%o", &octalVar); |
%ld | 读取 long int 类型的整数 | long int longIntVar; scanf("%ld", &longIntVar); |
与 printf 不同,scanf 的格式说明符和变量之间必须使用 & 操作符来获取变量的地址,因为 scanf 需要知道在哪里存储输入的数据。然而,对于字符串数组(即字符数组用作字符串),数组名本身就代表了数组首元素的地址,因此在 scanf 中读取字符串时不需要使用 &。
总结:
- 在 printf 中,对于 float 、 double 通常都使用 %f。
- 在 scanf 中,对于 float 应该使用 %f,对于 double 必须使用 %lf(scanf 需要明确知道它应该读取多少字节的数据来存储到指定的变量中,而 %lf 告诉 scanf 需要读取足够的字节来填充一个 double 类型的变量)。
4 scanf 原理剖析
4.1 输入缓存
当 scanf 函数读取标准输入时,如果还没有输入任何内容,那么 scanf 函数会被卡住(专业用语为阻塞,等待用户输入数据)。当用户通过键盘输入数据时,这些数据首先被存储在输入缓冲区中,这些数据最初以字符(包括字母、数字、标点符号等)的形式被读取并存储在输入缓冲区中。这些字符在输入缓冲区中组成了一个字符串。
输入缓冲区是一个临时的存储区域,用于存放从键盘输入的数据,直到遇到特定的触发条件(如回车键)才会将数据传递给程序。
按下回车键后,输入的字符串(包括回车符,通常会被转换为换行符 \n)被送入缓冲区,等待程序处理。
4.2 格式化解析
scanf 函数根据提供的格式字符串来解析输入缓冲区中的数据。格式字符串指定了期望的输入格式,包括数据类型和可能的分隔符(如空格、制表符、换行符等)。
scanf 会按照格式字符串的要求,从输入缓冲区中依次读取数据,并将其转换为指定的数据类型,然后存储到对应的变量中。
4.3 读取与存储
在读取数据时,scanf 会根据格式字符串中的格式说明符来识别输入的数据类型,并尝试将输入的字符序列转换为相应的数据类型。
4.3.1 对于数值类型
对于数值类型(如 %d、%i、%u、%f、%lf、%o、%x、%ld 等),会自动跳过前面的空白字符(如空格、制表符或换行符),直到遇到第一个不能解析为数值的字符,然后尝试根据格式说明符读取数值。例如:
#include <stdio.h>
int main() {
int num;
scanf("%d", &num); // %d 会自动跳过前面的空白字符
printf("你输入的整数是: %d\n", num);
return 0;
}
1、通过键盘输入 4 个整数,每个整数使用空格隔开,第一个整数前面故意留有多余的空格,%d 会忽略掉空白字符,输出结果如下图所示:
解释:
在上述代码中,当输入 11 22 33 44 时,这些输入会首先被存储在输入缓冲区中。
scanf("%d", &num) 会从输入缓冲区中读取数据。%d 格式说明符的工作原理是它会跳过输入前面的空白字符,然后尝试读取一个有效的十进制整数。
在输入缓冲区中的数据 11 22 33 44 中,scanf 首先遇到的是数字 11 ,它将其识别为一个整数并将其存储到变量 num 中。
一旦 11 被读取并处理,scanf 就完成了这次读取操作。
此时,输入缓冲区中还剩下 22 33 44 未被处理,但由于本次 scanf 只要求读取一个整数,所以后续的这些数据会留在缓冲区中,等待下一次的输入操作来处理。
最后,printf("你输入的整数是: %d\n", num); 输出了 你输入的整数是: 11 ,即刚刚通过 scanf 读取并存储到 num 中的值。
2、通过键盘输入多个数据,各数据之间使用空格隔开,但第一个数据为浮点型,输出结果如下图所示:
解释:
当输入 20.22 30.33 44 55 时,这些数据首先被完整地存储在输入缓冲区中,等待 scanf 函数处理。
scanf 函数使用 "%d" 格式说明符,它期望读取一个整数。
开始处理:scanf 从输入缓冲区的开始位置(即 20.22 的第一个字符 '2' )开始处理。
整数解析:scanf 会尝试按照整数的格式来解析遇到的字符序列。在这个例子中,它首先看到了 '2' 和 '0',这两个字符是有效的整数部分。然而,紧接着它遇到了小数点 '.',这在整数格式中是不被接受的。
停止读取并确定结果:在遇到第一个无法解析为整数部分的字符(这里是 .)时,scanf 会停止读取更多字符,并基于它到目前为止已经成功解析的字符(即 '2' 和 '0')来确定结果。因此,它确定整数为 20,并将这个值存储在变量 num 中。
剩余输入:由于 scanf 已经完成了它的任务(读取一个整数),并且遇到了一个不能继续解析为整数的字符(.),它会停止处理并留下剩余的输入(.22 30.33 44 55)在输入缓冲区中。这些剩余的输入不会自动被清除或忽略,除非有其他输入操作(如另一个 scanf 调用)来读取它们。
输出结果:最后,printf 函数输出读取到的整数 20。
3、通过键盘输入多个数据,各数据之间使用空格隔开,但第一个数据既不是整数也不是浮点数,输出结果如下图所示:
解释:
当输入 a abc @#$%^&* 123 4.5 后,它们首先会被存储在输入缓冲区中,等待 scanf 函数进行处理。
scanf 函数使用 "%d" 格式说明符来尝试从输入缓冲区中读取一个整数。
开始处理:scanf 从输入缓冲区的开始位置(即 'a' )开始处理。
不匹配检查:scanf 期望找到一个可以解析为整数的字符序列。然而,它遇到的第一个字符是 'a',这不是一个有效的整数开头字符(整数应该以数字 0-9 开始)。
停止读取:由于 'a' 不匹配整数格式,scanf 会立即停止读取更多字符,并且不会将任何值赋给指定的变量 num。此时,num 变量的值将保持不变,如果它在之前没有被显式初始化,那么它的值将是未定义的(即可能包含任何垃圾数据)。
剩余输入:剩余的输入(abc @#$%^&* 123 4.5)仍然保留在输入缓冲区中,等待后续可能的输入操作(如另一个 scanf 调用)来处理。
返回值:重要的是要注意,scanf 函数会返回一个整数,表示成功读取并赋值的输入项的数量。在这个例子中,由于 scanf 没有成功读取任何整数,它会返回 0。这个返回值可以用来检查 scanf 调用是否成功读取了预期数量的输入项。
4.3.2 对于字符类型
对于字符类型(如 %c),不会跳过前面的空白字符,而是直接读取遇到的第一个字符,包括空格、制表符或换行符(如果之前没有使用其他方式来消耗这些字符)。
为了避免这个问题,通常会在 %c 之前放置一个空格,以跳过前面的任何空白字符。或者使用 fflush(stdin); 用于刷新标准输入缓冲区,将缓冲区内的数据清空并丢弃。
虽然 fflush(stdin); 在某些情况下看起来像是可以用来清空输入缓冲区并丢弃其中的数据,但实际上 fflush 函数是定义用于输出流的,其行为对于输入流(如 stdin)是未定义的。
因此,在标准 C 中,不应使用 fflush(stdin); 来尝试清空输入缓冲区。C 标准并没有定义 fflush 函数对输入流(如 stdin)的行为,因此它的使用是不可移植的,也不被推荐。
#include <stdio.h>
int main() {
char ch1,ch2;
scanf("%c", &ch1); // 没在 %c 前面加空格
printf("你输入的字符是: %c\n", ch1);
scanf("%c", &ch2); // 没在 %c 前面加空格
printf("你输入的字符是: %c\n", ch2);
return 0;
}
如果按照上面的代码写法,输入a b,输出结果如下图所示:
当然也可以直接输入ab,但这样输入不够清晰,很容易看成是一个字符串,每一位数据之间最好还是使用间隔符隔开,输出结果如下图所示:
下面,将上面的代码进行修改,在 %c 前面加上空格,或使用 fflush 函数:
#include <stdio.h>
int main() {
char ch1,ch2;
scanf(" %c", &ch1); // 注意这里的空格,它会跳过前面的空白字符
printf("你输入的字符是: %c\n", ch1);
//fflush(stdin); // 刷新标准输入缓冲区,将缓冲区内的数据清空并丢弃
scanf(" %c", &ch2); // 注意这里的空格,它会跳过前面的空白字符
printf("你输入的字符是: %c\n", ch2);
return 0;
}
这样就可以解决 %c 读取到无用的空白符,输出结果如下图所示:
解释:
正确地使用了 scanf(" %c", &ch); 这种形式来读取字符,其中 %c 前的空格告诉 scanf 在尝试读取字符之前,要忽略(即“跳过”)任何前导的空白字符(包括空格、制表符、换行符等)。
当然也可以打开 fflush(stdin); 这一行注释,刷新标准输入缓冲区,将缓冲区内的数据清空并丢弃,达到相同的效果,但不推荐!
4.3.3 对于字符串类型
对于字符串类型(如 %s),不会跳过输入缓冲区中的空白字符,而是从第一个非空白字符开始读取字符串,并继续读取直到遇到下一个空白字符为止,这意味着字符串内部不能包含空白字符。为了跳过前面的空白字符,可以在 %s 之前的格式字符串中放置一个或多个空格,这样 scanf 就会先跳过任何空白字符,再开始读取字符串。
它不会自动在读取的字符串末尾添加空字符(\0)作为字符串的结束符;这个空字符是由 scanf 在内部添加的,前提是目标数组有足够的空间来存储它。
注意事项:
- 一定要确保目标数组有足够的空间来存储整个字符串(包括空字符 \0)。
- 使用 fgets(后续章节讲解) 而不是 scanf 来读取字符串,因为 fgets 允许你指定读取的最大字符数(包括空字符 \0),从而避免了缓冲区溢出的风险。
#include <stdio.h>
//%s 从第一个非空白字符开始读取字符串,并继续读取直到遇到下一个空白字符为止
int main() {
char str1[8],str2[8];
scanf("%s", &str1); // 不用加空格了
printf("你输入的字符是: %s\n", str1);
scanf("%s", &str2); // 不用加空格了
printf("你输入的字符是: %s\n", str2);
return 0;
}
输出结果如下所示:
5 scanf 返回值
scanf 函数会返回成功匹配并读取的输入项的数量。如果输入项的数量少于预期,或者在尝试读取数据时遇到错误或文件结束符(EOF),它会返回 EOF(通常是 -1)。
#include <stdio.h>
int main() {
int a, b;
int result;
// 尝试读取两个整数
result = scanf("%d %d", &a, &b);
// 检查 scanf 的返回值
if (result == 2) {
// 成功读取了两个整数
printf("成功读取两个整数: %d 和 %d\n", a, b);
} else if (result == 1) {
// 只成功读取了一个整数
printf("只成功读取了一个整数: %d\n", a);
// 注意:此时 b 的值是未定义的,因为它没有被成功读取
} else if (result == 0) {
// 没有读取到任何整数(输入可能不匹配)
printf("没有读取到任何整数\n");
} else if (result == EOF) {
// 遇到了文件结束符或读取错误
printf("遇到了文件结束符或读取错误\n");
} else {
// 理论上不应该到达这里,因为 scanf 的返回值应该是 0, 1, 2, 或 EOF
printf("意外的 scanf 返回值: %d\n", result);
}
return 0;
}
输入两个整数,输出结果如下图所示:
输入一个整数,输出结果如下图所示:
输入零个整数,输出结果如下图所示:
6 多种数据类型混合输入
在 C 语言中,使用 scanf 函数混合读取多种类型的数据时,特别是包含字符型(%c)时,确实需要格外小心处理空格、换行符等字符。这是因为 %c 格式说明符会读取任何字符,包括空格、制表符和换行符,这与 %d、%f 等类型不同,后者会自动跳过空白字符(空格、制表符、换行符)直到找到可以转换的数据。
下面展示一个错误例子:
#include <stdio.h>
int main() {
int i, ret;
char c;
float f;
double d;
//多种数据类型混合输入,%c 前面不加空格,会出错
ret = scanf("%d%c%f%lf", &i, &c, &f,&d);
printf("i = %d, c = %c, f = %5.2f, d = %5.2f, ret = %d\n", i, c, f,d,ret);
return 0;
}
输出结果如下所示:
解释:
当输入 100 a 98.2 99.9 时,
整数(%d):100 被成功读取并存储在变量 i 中。
字符(%c):紧接着整数之后的是空格字符(' ')。因为 %c 指示符会读取任何字符,包括空格,所以它会读取并存储这个空格到变量 c 中。
浮点数(%f):由于 %c 已经读取了空格,scanf 现在试图从 a 开始读取一个浮点数,但这显然失败了,因为 a 不是一个有效的浮点数开头。因此,f 保持未定义,并且 scanf 在这一点上停止读取更多的输入项。
双精度浮点数(%lf):由于之前的读取失败,scanf 不会尝试读取这个值。所以,最终只成功读取了两个数据(整数 100 和空格字符),ret = 2。
所以,为了避免发生上面那种错误,一般在 %c 前面加一个空格。
以下是一个正确的示例程序,展示了如何正确地混合读取整数(int)、浮点数(float)和字符(char)类型的数据。在这个例子中,我们在 %d 和 %c 之间加入了一个空格,以确保 %c 读取的是非空白字符。
#include <stdio.h>
int main() {
int age;
float height;
char gender;
// 提示用户输入
printf("请输入年龄、身高(浮点数)和性别(M/F):");
// 读取数据,注意 %f 和 %c 之间的空格
// 这个空格的作用是告诉 scanf 跳过前面的所有空白字符(包括空格、制表符和换行符)
if (scanf("%d %f %c", &age, &height, &gender) == 3) {
// 检查返回值确保读取了三个值
printf("年龄:%d, 身高:%.2f, 性别:%c\n", age, height, gender);
} else {
// 如果读取失败(即返回值不等于3),则打印错误信息
printf("输入错误!\n");
}
return 0;
}
输出结果如下图所示:
7 解决 printf 和 scanf 显示顺序问题
下面有这样的代码:
#include <stdio.h>
int main() {
int a;
printf("请输入一个整数:\n");
scanf("%d", &a);
printf("输入的整数是:%d",a);
return 0;
}
正常来看输出结果应该是:先输出打印请【输入一个整数: 】,再输入一个数据,再输出打印【输入的整数是:输入的数据】。
然而当我们在 CLion 中执行的时候,看到的却是先输入一个数据,再一起输出打印【输入一个整数: 】【输入的整数是:输入的数据】。如下所示:
注意:这只是显示的顺序跟理想的顺序不一致,不要误以为是程序执行顺序。上面这段代码是顺序结构,执行顺序是从上到下,可以在调试模式查看程序执行顺序。
7.1 fflush 函数
现在我们在 scanf 之前调用 fflush(stdout); 来确保之前的输出被即时发送到目标设备(这里是屏幕),修改代码如下:
#include <stdio.h>
int main() {
int a;
printf("请输入一个整数:\n");
printf("可以使用 fflush(stdout); 清空(或刷新)标准输出缓冲区\n");
printf("下面使用试试看:\n");
fflush(stdout); //在输出后调用fflush(stdout);来确保输出被即时发送到目标设备。
scanf("%d", &a);
printf("输入的整数是:%d",a);
return 0;
}
输出结果如下图所示:
可先,现在控制台的输入输出顺序就趋于正常了。
7.2 fflush(stdout); 的作用
fflush(stdout); 在 C 语言中是一个用于 清空(或刷新)标准输出(stdout)缓冲区的函数调用。
标准输出缓冲区是 C 语言运行时环境为了优化输出性能而设置的一个内存区域,用于暂存输出数据。当程序向标准输出(stdout,通常是屏幕或终端)输出数据时,这些数据首先被写入到这个缓冲区中,而不是直接发送到目标设备。一旦缓冲区满了,或者程序显式地要求刷新缓冲区(例如,通过调用 fflush 函数),或者程序正常终止时,缓冲区中的数据才会被发送到目标设备并显示出来。
- 清空标准输出缓冲区:调用此函数后,stdout 缓冲区中积累的所有待输出数据都会被发送到其对应的目标设备(如屏幕、文件等)。如果缓冲区为空,则此调用不会产生任何效果。
- 确保即时输出:在某些情况下,可能希望程序立即显示输出,而不是等待缓冲区满或程序结束。这时,可以在输出后调用 fflush(stdout); 来确保输出被即时发送到目标设备。
回忆:对于上文提到的 fflush(stdin); 其目的是尝试清空输入缓冲区(即丢弃缓冲区中已存在的输入数据),以便后续的输入操作不会受到之前残留数据的影响。但是,需要注意的是,C 标准并没有定义 fflush 函数对输入流(如 stdin)的行为,因此它的使用是不可移植的,所以不被推荐使用。
8 调试之变量未初始化不触停断点
在 C 语言编程中,变量的初始化是一个重要的概念。然而,在调试过程中,可能会发现,对于未初始化的变量,即使你在其声明行上设置了断点,调试器通常也不会在该位置停留。这是因为变量的声明(特别是没有初始化器的声明)本身并不执行任何操作,它只是告诉编译器在内存中为变量预留空间。
以下是一个具体的例子来说明这一点:
#include <stdio.h>
int main() {
int num; // 在这里设置断点通常不会停留
scanf("%d", &num); // 在这里设置断点会在执行到这一行前停留
printf("你输入的整数是: %d\n", num);
return 0;
}
点击调试按钮后,初始状态如下所示:
在上面的代码中,我们声明了一个整型变量 num 并试图通过 scanf 函数从用户那里读取一个整数来初始化它。如果在这两行代码上都设置了断点:
- 在 int num; 上设置的断点:调试器通常不会在这里停留,因为这只是一个简单的变量声明,没有执行任何操作。
- 在 scanf("%d", &num); 上设置的断点:调试器会在这里停留,因为这是一个函数调用,它涉及到实际的数据读取和存储操作。
需要注意的是,未初始化的变量 num 在被读取之前其值是未定义的。这意味着它可能包含任何值,这取决于内存在该时刻的状态。因此,在使用未初始化的变量之前,最好显式地给它赋一个初始值,以避免潜在的问题。
9 小节判断题
1、scanf 读取标准输入,%d 用来匹配 int 整型,%f 匹配 float 类型,%c 匹配字符 ?
A. 正确 B. 错误
答案:A
解释:正确的,这个需要记住。
2、有如下代码,int i; scanf("%d", i); 想读取一个数据到变量 i 中,是否正确 ?
A. 正确 B. 错误
答案:B
解释:通过 scanf 读取标准输入时,我们需要对变量 i 进行取地址,代码是 scanf("%d", &i) ,因为 scanf 函数是把对应的数据放入变量所在的空间中,因此需要对应变量的地址。
3、scanf("%d", &i) ,当我们输入 10 回车后,i 读取到了 10 ,那么标准缓冲区中已经空了 ?
A. 正确 B. 错误
答案:B
解释:这时标准缓冲区中并没有空,里边还有 “\n” 字符。
4、int i; char c; float f; scanf("%d %c %f", &i, &c, &f); 当混合读取时,因为 %c 不能忽略空格和 “\n” ,所以需要在其前面加一个空格 ?
A. 正确 B. 错误
答案:A
解释:这种操作要记住,对于做 OJ 的题目,考研机试非常重要。