C语言中scanf详解

9.2 scanf

当我们有了变量,我们需要给变量输⼊值就可以使⽤的时候可以使⽤scanf 函数 ,如果需要将变量的值输出在屏幕上prinf 函数,下⾯看⼀个例⼦:

画图来演示这一流程:
标准输入一般就是指键盘,标准输出一般就是屏幕

9.2.1 scanf基本⽤法

scanf() 函数⽤于读取用户的键盘输⼊。

程序运⾏到这个语句时,会停下来,等待⽤⼾从键盘输⼊。
用户输⼊数据、按下回⻋键后,scanf() 就会处理用户的输⼊,将其存⼊变量 。它的原型定义在头⽂件<stdio.h>中 。

scanf() 的语法跟printf() 类似。

cpp 复制代码
int a = 0;
scanf("%d",&a);

它的第⼀个参数是⼀个格式字符串,⾥⾯会放置占位符 (与printf()的占位符基本一致,告诉编译器如何解读用户的输⼊,需要提取的数据是什么类型。

这是因为C语⾔的数据都是有类型的,scanf() 必须提前知道⽤⼾输⼊的数据类型,才能处理数据。它的其余参数就是存放⽤⼾输⼊的变量,格式字符串⾥⾯有多少个占位符,就有多少个变量

上⾯⽰例中,scanf() 的第⼀个参数i 。%d ,表⽰⽤⼾输⼊的应该是⼀个整数。%d就是⼀个占位符,%是占位符的标志,d表示整数.第二个参数&a表示将用户从键盘输入的整数存放入变量a之中.

注意:

(1). 变量前面必须加上&运算符(指针变量除外),因为scanf()传递的不是值而是地址,即将变量a的地址指向用户输入的值.

(2).如果这里的变量是指针变量(比如字符串变量),就不需要加&运算符.因为指针变量里储存的就是地址.(指针变量后面详细讲解).

下⾯是⼀次将键盘输⼊读⼊多个变量的例⼦。

cpp 复制代码
scanf("%d%d%f%f",&i,&j,&x,&y);

上面的示例中,格式字符串%d%d%f%f,表示用户输入的前两个数是整数,后两个是浮点数.比如输入: 1 -10 5.5 2.0e3.那么这四个值会被依次放入i,j,x,y四个变量中.

scanf() 处理数值占位符时,会⾃动过滤空⽩字符,包括空格、制表符、换⾏符等。 所以,⽤⼾输⼊的数据之间,有⼀个或多个空格不影响scanf() 解读数据。另外,用户使⽤回⻋键,将输⼊分成⼏⾏,也不影响解读。如:

上⾯⽰例中,⽤⼾分成四⾏输⼊,得到的结果与⼀⾏输⼊是完全⼀样的。每次按下回⻋键以后, scanf() 就会开始解读,如果第⼀⾏匹配第⼀个占位符,那么下次按下回⻋键时,就会从第⼆个占 位符开始解读。

输入原理:

scanf() 处理用户输⼊的原理是,用户的输⼊先放⼊缓存,等到按下回⻋键后,按照占位符对缓存 进⾏解读。

解读用户输⼊时,会从上⼀次解读遗留的第⼀个字符开始,直到读完缓存,或者遇到第⼀个不符合条件的字符为⽌.

具体事例如下:

讲解: 上⾯⽰例中,scanf() 读取⽤⼾输⼊时,%d 占位符会忽略起⾸的空格,从处开始获取数据,读取到-13停下来,因为后⾯的.不属于整数的有效字符。这就是说,占位符%d 会读到-13 .

第⼆次调⽤scanf() 时,就会从上⼀次停⽌解读的地⽅,继续往下读取。这⼀次读取的⾸字符是.,由于对应的占位符是%f,会读取到.45e12 ,这是采⽤科学计数法的浮点数格式。后⾯的# 不属于浮点数的有效字符,所以会停在这⾥。

由于scanf() 可以连续处理多个占位符,所以上⾯的例⼦也可以写成下⾯这样:

cpp 复制代码
scanf("%d%f", &x, &y);

9.2.2 scanf 的返回值

scanf() 的返回值是⼀个整数,并不是从键盘输入的数据,而是表⽰成功读取的变量个数。

如果没有读取任何项,或者匹配失败,则返回0。

如果在成功读取任何数据之前,发⽣了读取错误或者遇到读取到⽂件结尾,则返回常量EOF(-1)。
EOF-endoffile ⽂件结束标志.

cpp 复制代码
#include <stdio.h>
int main()
{
    int a = 0;
    int b = 0;
    float f = 0.0f;
    int r = scanf("%d %d %f", &a, &b, &f);
    printf("a=%d b=%d f=%f\n", a, b, f);
    printf("r = %d\n", r);
    return 0;
}

输⼊输出测试:

如果输⼊2个数后,按ctrl+z ,提前结束输⼊,在VS环境中按3次ctrl+z ,才可以结束了输⼊,我们可以看到r是2,表⽰正确读取了2个数值:

如果⼀个数字都不输⼊,直接按3次ctrl+z ,输出的r是-1,也就是EOF.

9.2.3 占位符

9.2.3.1 常见scanf()占位符

scanf() 常⽤的占位符如下,与printf() 的占位符基本⼀致。

9.2.3.2 %s占位符特性

下面特别说一下**%s占位符** ,,它其实不能简单地等同于字符串。

1. 特性一:

**核心读取规则:「非空开始,空白结束」
%s 是专门用于读取字符串的占位符,但它有严格的扫描边界,分为两个阶段:

  1. 起始条件:跳过前导空白,从「第一个非空白字符」开始.
    scanf() 遇到 %s 时,会自动跳过开头所有空白字符(空格、回车 \n、Tab \t),直到找到第一个非空白字符,才开始正式读取。
  2. 终止条件:遇到「第一个空白字符」就停止.
    读取过程中,一旦再次遇到任意空白字符(空格、回车、Tab),立即停止读取,并将空白字符之前的所有内容作为字符串存入变量。**

举个例子:

cpp 复制代码
char str[20];      //定义一个字符数组
scanf("%s", str);   //输入字符串

因为 %s 的终止条件是空白字符,而空格 / 回车正是多个单词的分隔符,所以⽆法⽤来读取多个单词,除⾮多个 %s ⼀起使⽤。这也意味着, scanf("%s") 仅适用于读取「无空格的简单字符串」(如单词、用户名、编号),完全不适合处理书名、歌曲名、句子等含空格的内容.

2. 特性二:.

scanf("%s") 成功读取字符串后,会在存储的字符末尾自动添加一个 \0(空字符)作为字符串的结束标志。如下代码:

风险提醒:如果输入字符数超过数组长度(如数组长度为 3,输入abcd),%s 会直接缓冲区溢出,覆盖后续内存,导致程序崩溃。

9.2.4 五个大坑

接下来这里介绍scanf使用中的五个大坑.

1. 遇到空白字符就停止,只能读取一个单词 / 数字

这个上面提到过,直接看案例:

可见 name 中只存了 "Li"," Hua" 被留在缓冲区.

解决方法有两个:

  • fgets() 替代 scanf("%s"),可以完整读取带空格的整行输入.
  • %[^\n] 扫描集,指定「读到换行符为止」.
cpp 复制代码
scanf("%[^\n]", name); // 读取到换行前的所有字符,包含空格

2.scanf()读完后把 \n 留在缓冲区,污染后续输入

原理: scanf() 读取数字 / 字符串时,不会消费输入末尾的回车符 \n(不仅是 \n,所有空白符(空格、Tab)都可能残留,尤其是用%d/%s读取后,缓冲区会残留输入时的换行符 ),会把它留在输入缓冲区里。如果后面用 getchar()、fgets()、scanf("%c") 等函数,会直接读到这个残留的 \n,导致逻辑错误。

解决方法:

cpp 复制代码
// 方法1:用 getchar() 循环吃掉所有残留字符
while (getchar() != '\n');
// 方法2:用 scanf("%*[^\n]") 跳过所有非换行字符,再读走换行
scanf("%*[^\n]"); getchar();

3.不检查数组长度,直接导致缓冲区溢出

原理:scanf("%s", arr) 和被废弃的 gets() 一样,完全不检查目标数组的长度,如果用户输入的字符数超过数组容量,会直接越界写入内存,导致程序崩溃、数据被篡改,甚至产生安全漏洞。

解决方法:

(1). 给 %s 加长度限制,指定最多读取的字符数.

cpp 复制代码
char buf[5];
scanf("%4s", buf); // 最多读4个字符,自动补 '\0',永远不会溢出

(2).直接使用fgets,它会严格限制读取长度,更安全.

4. 输入类型不匹配,程序直接「卡死」

scanf() 对输入类型匹配要求极严格:如果格式串要求 %d(整数),但用户输入了字母 / 符号,匹配会直接失败,并且不会从缓冲区移除错误数据,导致后续所有 scanf() 都直接失败,程序陷入死循环。

cpp 复制代码
int num;
while (1)
{
    scanf("%d", &num); // 输入 abc
    // 匹配失败,abc 留在缓冲区,下一轮 scanf 继续读 abc,永远失败
    printf("输入:%d\n", num);
}

解决方法:

  • 检查 scanf() 的返回值:scanf() 会返回成功匹配的参数个数,用它判断输入是否合法.
  • 类型不匹配时,清空缓冲区,重新输入:
cpp 复制代码
int num;
while (scanf("%d", &num) != 1) 
{
    // 匹配失败,清空缓冲区
    while (getchar() != '\n');
    printf("输入非法,请重新输入整数:");
}

5. %c 会读取空白字符,导致误读

原理:scanf() 中,只有 %c 不会自动跳过空白字符,会直接读取缓冲区里的空格、回车、Tab。如果前面有 scanf() 残留的 \n%c 会直接把它读走,而不是等待用户输入新字符.

cpp 复制代码
int age;
char op;
scanf("%d", &age); // 输入 18 回车
scanf("%c", &op);  // op 直接读到了残留的 '\n',不是用户输入的字符

解决方法:

(1). 在 %c 前面加一个空格 ,让 scanf() 先跳过所有空白字符,再读取有效字符.

cpp 复制代码
scanf(" %c", &op); // 注意 %c 前面的空格!

(2).同样可以用「先清空缓冲区」的方法,避免残留字符干扰.

9.2.4.1 总结:

内容太多,我们来做一个总结:

9.2.5 赋值忽略符

有时,用户的输⼊可能不符合预定的格式。如下代码:

cpp 复制代码
#include <stdio.h>
int main()
{
    int year = 0;
    int month = 0;
    int day = 0;
    scanf("%d-%d-%d", &year, &month, &day);
    printf("%d %d %d\n", year, month, day);
    return 0;
}

上⾯⽰例中,如果⽤⼾输⼊ 格式,⽐如 2020-01-01 ,就会正确解读出年、⽉、⽇。问题是⽤⼾可能输⼊其他 2020/01/01 ,这种情况下, scanf() 解析数据就会失败。

为了避免这种情况,scanf() 提供了⼀个赋值忽略符 (assignmentsuppressioncharacter) * 。 只要把 * 加在任何占位符的百分号后⾯,这个占位符只负责读取匹配的字符,但不会把值赋给任何变量,读取后直接丢弃.

简而言之,其作用是跳过指定位置的分隔符.

cpp 复制代码
#include <stdio.h>
int main()
{
    int year = 0;
    int month = 0;
    int day = 0;
    scanf("%d%*c%d%*c%d", &year, &month, &day);
    return 0;
}

上⾯⽰例中,%*c就是在占位符的百分号后⾯,加⼊了赋值忽略符*,表⽰这个占位符没有对应的变量,解读后不必返回。

相关推荐
Scott9999HH7 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
码智社8 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海8 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖9 小时前
JDK 26 新特性详解
java·开发语言
马优晨9 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区11 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大12 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai12 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
এ慕ོ冬℘゜12 小时前
前端基础:什么是时间戳?JS获取时间戳三种方法与实战用途
开发语言·前端·javascript
执明wa12 小时前
LayoutInflater详解: XML是如何变成View的?
android·xml·开发语言·android studio