C语言安全输入:scanf函数

输入函数scanf()

函数名: scanf
功 能: 执行格式化输入
用 法: int scanf(char *format[,argument,…]);

format 是一个字符串,通过这个字符串限制了scanf的数据匹配格式,
argument 指的是后面的需要进行匹配赋值的参数,
… 指后面的可变参数

例如:

    int a, b;
    // 这里有两个变量 我们可以通过scanf对他进行赋值
    scanf("%d%d", &a, &b); // 正确用法 如果要通过函数修改变量,应当将他的地址传给这个函数
                           // 否则只会修改他的值得拷贝
    // scanf("%d%d", a, b);   // 错误用法
    printf("%d %d\n", a, b);

在上面的程序中 我们通过scanf对a,b两个变量赋值,
其中的format参数的值是 “%d%d” 其中使用到的%d是格式转换符

格式转换说明符

格式转换说明符功能描述
%d输入一个十进制整数
%f输入一个单精度实数
%lf输入一个双精度实数
%c输入一个字符
%s输入一个字符串
%o输入一个八进制整数
%x输入一个十六进制整数
%*表示本输入项只是读入,但不赋给相应变量

在上个例子中,我们使用格式转换说明符匹配了两个整形变量,但是值得注意的是,这个格式说明符· 中间没有分隔,程序是怎么知道要分开两个数的呢?
实际上,程序通常有三种条件进行下一个格式说明符的匹配

1、空白字符(空白字符是指空格键,tab键,回车键)

2、遇宽度结束

3、遇非法输入(结束scanf)

4、缓冲区被读取完毕(结束scanf)

在实际运行过程中,我输入的是"12 34"通过一个空格(空白字符)隔开两个值,接下来我们来看第二种情况

在格式转换符的帮助下我们已经可以输入一个小数了,但是现实条件下我们通常会限制数据的长度
此时可以用到%nd (n为一个十进制数)

    int a, b;
    // 这里有两个变量 我们可以通过scanf对他进行赋值
    scanf("%2d%2d", &a, &b); // 通过限制获取两位
    printf("%d %d\n", a, b);

在现在的这个例子中,我们只需要输入"1234",不用分隔符也能匹配上两个数。
我们来分析一下,程序先使用"%2d"匹配了前两个字符"12",然后将其赋值给变量a,然后继续使用下一个"%2d"匹配接下来的"34",赋值给变量b。

注意!!!! 在这个过程中程序在匹配了一个字符之后,转而进行变量的赋值,此时"%2d%2d"的匹配被打断了,等待赋值完成后,程序继续进行匹配,

这代表程序实际上是有一块缓冲的地方给字符串存储的。

缓冲区

实际上由于cpu处理速度远高于我们IO输入的速度,所以在计算机设计过程中,有专门的缓冲区来缓冲输入,输出同样也有。

缓冲区用来缓存“输入数据”的ASCII码,而scanf()每次从缓冲区中读取一个字符(ASCII码),在运行过程中,如果缓冲区为空,scanf会要求用户输入数据到缓冲区,然后scanf进行读取直到缓冲区为空。如果缓冲区为非空状态,则下次执行scanf()不会要求输入,如果键盘缓冲区为空,执行scanf()则会等待用户的输入。

下面我们来看scanf第三种结束条件
我们尝试给一个int和一个char赋值 我们输入"A A",使用一个空格(空白字符)隔开两个值

    int a;
    char c;
    // 这里有两个变量 一个int 和一个char 我们可以通过scanf对他进行赋值
    scanf("%d%c", &a, &c); // 通过限制获取两位
    printf("%d %c\n", a, c);

我们期待的结果是
a的值为65 即’A’的ASCII码的值
c的值为’A’ 即’A’被成功赋值

在这里插入图片描述
但是实际运行结果是 "0 "(两个括号 一个是printf内的空格字符,一个是c的字符值)

这意味着在实际过程中 scanf在使用"%d"给’A’匹配时发生了错误,他并没有给a赋值,而且直接结束了匹配。
那么剩下还有一个的’A’在哪呢 我们来看看缓冲区还剩什么

 int a;
    char c;
    // 这里有两个变量 一个int 和一个char 我们可以通过scanf对他进行赋值
    scanf("%d%c", &a, &c); 

    char arr[10]; // 用数组获取缓冲区
    scanf("%c%c", &arr[0], &arr[1]);
    printf("%c %c\n", arr[0], arr[1]);

在这里插入图片描述
一共进行了两次scanf,第一次scanf由于匹配失败,在缓冲区留下了一个’A’
第二次scanf的执行并没有等待我输入,他直接从缓冲区读取到了上一个scanf留下的那个’A’

程序出现了开发者不希望出现得到情况,第二个scanf直接破坏了整个程序的运行逻辑

再比如一个很多人可能遇见的情况,没遇见过的同学可以拷贝去尝试运行一下

    int a;
    char c;
    // 这里有两个变量 一个int 和一个char 我们可以通过scanf对他进行赋值
    printf("input one integer:");
    scanf("%d", &a);
    printf("input one char:");
    scanf("%c", &c);
    printf("input end\n");
    printf("%d %c\n", a, c);

运行效果如下
被吃掉的\n
我只输入了一次 12 但是他第二次scanf同样没有要求我输入,我们没有输入字符,但是他第二次居然获取了缓冲区的数据。灵异事件出现了!

回想一下刚才的输入过程,我们输入了一次12 然后按了回车,实际上回车这个键输入了两个字符\r\n(回车换行),scanf使用%d匹配了12,但是换行符没有人匹配,被留在了缓冲区,第二次scanf看见缓冲区有数据,读取了一个\n

为了验证这个结论我们修改下代码

 int a;
    char c;
    // 这里有两个变量 一个int 和一个char 我们可以通过scanf对他进行赋值
    printf("input one integer:");
    scanf("%d", &a);
    printf("input one char:");
    scanf("%c", &c);
    printf("input end\n");
    printf("%d %d\n", a, c);
    return 0;

缓冲区导致的不安全的scanf

那么我们怎么解决这个问题呢

这个问题有如下几种解决办法

解决方案1

1.getchar()

int getchar(void)

getchar能从缓冲区获取一个字节并返回
来看使用getchar的优化方法

    int a;
    char c;
    // 这里有两个变量 一个int 和一个char 我们可以通过scanf对他进行赋值
    printf("input one integer:");
    scanf("%d", &a);
    printf("input one char:");
    getchar();
    scanf("%c", &c);
    printf("input end\n");
    printf("%d %d\n", a, c);

使用getchar吃掉了’\n’,程序可以正常等待输入了
正常输入

解决方案2

在第二次输入之前,再使用一个

 scanf("\n");
 scanf("%c", &c);

或是

 scanf("%c") 
 scanf("%c", &c);

或是

fgetc(stdin);
scanf(“%c”, &c);

获取’\n’,使缓冲区清空,等待输入;

解决方案3

第一次输入时使用

int a;
scanf("%d ", &a); //这里有个括号可以使用他吃掉回车字符

解决方案4

scanf("%*[\n]");

这个字符串时scanf自带的正则表达式,它很有意思,让我们一一拆解一下

首先	"%[\n]" 表示  匹配 [ ] 中的字符 
		"%*[\n]"表示  匹配 [ ] 中的字符 并丢弃

扩展

在前面我们使用了如下表达式

scanf("%*[\n]");

我们来讨论一下他的用法

首先	"%[ABC]" 表示  匹配 [ ] 中的字符  即ABC
		"%[A-Z]" 表示  匹配 [ ] 中的字符  即所有大写字母
		"%[^A-Z]"表示  匹配不是[]中的字符 即除开大写字符的所有字符
		"%*[ABC]"表示  匹配 [ ] 中的字符  并丢弃

下面我们来应用这些知识

练习题: 在一串字符串(可能包含任意字符)中有两个整形数字(不确定位置),如何取出他们并赋值给a,b。
在题目中我们需要丢弃所有所有非数字字符 表达式为

	"%*[^0-9]" //读取所有 非(^) 数字字符(0-9) 并丢弃(*)

同时我们需要处理结尾出现的换行符 清除缓冲区,可以使用上文出现的方法
实现代码如下

void safeScanf()
{
    printf("safeScanf:\n");
    int a = 0, b = 0;
    scanf("%*[^0-9]"); // 读入所有非数字并丢弃
    scanf("%d", &a);
    scanf("%*[^0-9]"); // 读入所有非数字并丢弃
    scanf("%d", &b);
    scanf("%*[^\n]%*c"); // 他是下面两句的集合
    // scanf("%*[^\n]"); // 读入所有非换行符并丢弃
    // scanf("%*c");	 // 读入换行符并丢弃
    printf("a=%d b=%d\n", a, b);
}

恭喜你!! 通过在scanf函数上面添加补丁,你修复了自己程序的输入bug

  • 8
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C语言中常用的输入函数scanf、gets和fgets等。 1. scanf函数 scanf函数用于从标准输入流(stdin)中读取数据。它的语法如下: ```c scanf("format string", &var1, &var2, ...); ``` 其中,format string是格式字符串,用于指定输入变量的类型和格式;&var1, &var2, ...表示要输入的变量的地址。 例如,要输入一个整数,可以这样写: ```c int num; scanf("%d", &num); ``` 2. gets函数 gets函数用于从标准输入流(stdin)中读取一行字符串。它的语法如下: ```c char *gets(char *str); ``` 其中,str是一个指向字符数组的指针,用于存储输入的字符串。 例如,要输入一行字符串,可以这样写: ```c char str[100]; gets(str); ``` 需要注意的是,gets函数存在安全漏洞,因为它无法限制输入的字符数,可能会导致缓冲区溢出的问题。因此,建议使用更为安全的fgets函数。 3. fgets函数 fgets函数用于从指定的输入流中读取一行字符串。它的语法如下: ```c char *fgets(char *str, int n, FILE *stream); ``` 其中,str是一个指向字符数组的指针,用于存储输入的字符串;n是要读取的最大字节数;stream是输入流。 例如,要从标准输入流中输入一行字符串,可以这样写: ```c char str[100]; fgets(str, sizeof(str), stdin); ``` 在上面的代码中,我们使用fgets函数从标准输入流(stdin)中输入一行字符串,并且指定了最大读取字节数为sizeof(str)。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值