Bootstrap

2.4 详解 scanf:格式、原理、返回值与混合数据处理

目录

1 scanf 介绍 

2 scanf 格式语法

3 scanf 格式说明符

4 scanf 原理剖析

4.1 输入缓存

4.2 格式化解析

4.3 读取与存储

4.3.1 对于数值类型

4.3.2 对于字符类型

4.3.3 对于字符串类型

5 scanf 返回值

6 多种数据类型混合输入

7 解决 printf 和 scanf 显示顺序问题

8 未初始变量不触停断点

9 判断题


1 scanf 介绍 

如下图所示,程序员可为程序输入数据,程序处理后会给出输出。C 语言借助函数库读取标准输入键盘输入),再通过相应函数处理将结果显示于屏幕。此前我们学习了 printf 函数,知晓可通过该函数将结果输出至屏幕。接下来详细阐释标准输入函数 scanf 。

scanf 函数是 C 标准库中的一个非常重要的输入函数,用于从标准输入(通常是键盘输入)读取格式化的输入。它允许程序根据指定的格式字符串来解析输入的数据,并将解析出的数据存储到指定的变量中。


2 scanf 格式语法

#include <stdio.h>  //  C 标准库
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读取浮点数(floatdouble
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 使用 %f,对于 double 也通常使用 %f(尽管技术上 %lf 可能在某些编译器上被接受,但这不是标准行为)。
  • 在 scanf 中,对于 float 使用 %f,对于 double 必须使用 %lf

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个整数,中间空格隔开,输出结果如下图所示:

解释:

  1. 输入处理:当输入  11  22  33  44并按下回车键时,这个字符串(包括换行符)被放入了输入缓冲区。
  2. scanf 调用:scanf("%d", &num); 被调用时,它会开始从输入缓冲区中读取数据。
  3. 跳过空白字符%d 格式说明符会首先跳过任何前导的空白字符
  4. 读取整数:接下来,scanf 会尝试将输入缓冲区中的字符转换为整数。在这个例子中,它成功地读取了 11 并将其存储在变量 num 中。
  5. 停止读取一旦 scanf 读取到一个与 %d 不匹配的字符(在这个例子中是空格,它位于 11 和 22 之间),它就会停止读取并返回。此时,scanf 已经成功读取了一个整数,并返回了 1,表示它成功读取了一个与格式说明符匹配的项。
  6. 剩余输入:由于 scanf 只会读取一个整数,并且已经返回,输入缓冲区中剩余的 22 33 44(以及换行符)仍然留在那里,等待未来的输入操作。
  7. 输出结果:最后,程序输出:你输入的整数是: 11。

2、通过键盘输入第一个数为浮点数等数据,中间空格隔开,输出结果如下图所示:

解释:

scanf 尝试读取一个整数。它成功地从 20.22 字符串中读取了整数部分 20,因为 . 不是整数部分的有效字符。在读取到 . 时,scanf 停止读取,因为它已经找到了一个整数

3、通过键盘输入第一个数既不是整数也不是浮点数等数据,中间空格隔开,输出结果如下图所示:

解释

scanf 会尝试从当前位置开始读取一个整数。但是,它遇到的第一个字符是 'a',这不是一个数字字符,因此不能作为整数的一部分。当 scanf 遇到与格式字符串不匹配的字符时,它会停止读取,并保留已经读取(但在这个情况下是零个字符)的数据。同时,它会将目标变量(这里是 num)的值设置为未定义(如果之前没有被初始化的话,也可能是垃圾数据)。

4.3.2 对于字符类型

对于字符类型(如 %c),不会跳过前面的空白字符,而是直接读取遇到的第一个字符,包括空格、制表符或换行符(如果之前没有使用其他方式来消耗这些字符)。为了避免这个问题,通常会在%c之前放置一个空格在 scanf 的格式字符串中,以跳过前面的任何空白字符。或者使用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 不会尝试读取这个值。

所以,为了避免发生上面那种错误,一般在 % c前面加一个空格。

以下是一个正确的示例程序,展示了如何正确地混合读取整数(int)、浮点数(float)和字符(char)类型的数据。在这个例子中,我们 %d 和 %c 之间加入了一个空格,以确保 %c 读取的是非空白字符。

#include <stdio.h>

int main() {
    int age;
    float height;
    char gender;

    // 提示用户输入
    printf("请输入年龄、身高(浮点数)和性别(M/F):");

    fflush(stdout);

    // 读取数据,注意%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 中执行的时候,看到的却是先输入一个数据,再一起输出打印【输入一个整数:    】【输入的整数是:输入的数据】。

这只是显示的顺序跟理想的顺序不一致,不要误以为是程序执行顺序。上面这段代码是顺序结构,执行顺序是从上到下,可以在调试模式查看程序执行顺序。

现在 在输出后调用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;
}

输出结果如下图所示:

fflush(stdout); 在C语言中是一个用于 清空(或刷新)标准输出缓冲区(stdout)的函数调用。标准输出缓冲区是 C语言运行时环境为了优化输出性能而设置的一个内存区域,用于暂存输出数据。当程序向 stdout(通常是屏幕或终端)输出数据时,这些数据首先被写入到这个缓冲区中,而不是直接发送到目标设备。一旦缓冲区满了,或者程序显式地要求刷新缓冲区(例如,通过调用 fflush函数),或者程序正常终止时,缓冲区中的数据才会被发送到目标设备并显示出来。

fflush(stdout);的具体作用如下:

  • 清空标准输出缓冲区:调用此函数后,stdout 缓冲区中积累的所有待输出数据都会被发送到其对应的目标设备(如屏幕、文件等)。如果缓冲区为空,则此调用不会产生任何效果。
  • 确保即时输出:在某些情况下,可能希望程序立即显示输出,而不是等待缓冲区满或程序结束。这时,可以在输出后调用 fflush(stdout); 来确保输出被即时发送到目标设备。

对于前文提到的 fflush(stdin),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 的题目,考研机试非常重要。

;