编程开发

C语言-深入浅出scanf

2020-09-26  本文已影响0人  QuietHeart

这里将对C语言中的 scanf() 函数进行尽可能详细的讲解,内容主要参考自: man scanf

函数声明

C语言程序读取用户输入的常用的是 scanf 族系列库函数, 其声明如下:

#include <stdio.h>
int scanf(const char *format, ...);
int fscanf(FILE *stream, const char *format, ...);
int sscanf(const char *str, const char *format, ...);

#include <stdarg.h>
int vscanf(const char *format, va_list ap);
int vsscanf(const char *str, const char *format, va_list ap);
int vfscanf(FILE *stream, const char *format, va_list ap);

这里将重点介绍其中的 scanf() 函数。

scanf 族函数根据 format 参数来获取输入的。 format 包括了 conversion specifications (暂译作转换规则), 转换规则会将转换的结果存放在 format 参数后面的相应指针地址参数中。每一个指针参数必须与转换规则返回的值对应,如果转换规则转换的结果数目大于指针参数的数目,那么最后的结果是没有定义的;如果指针参数的数目比转换规则返回的值数目大,那么超出来的指针会被估值,但是一般会被忽略。

注:这也意味着,格式字串的转换标记不能多于参数个数,由于可变参机制会根据格式子串解析后面的参数,所以不能解析多了参数不够就危险了。

格式控制

format 参数字符串用来控制输入的格式由 directives 序列组成,而 directives 中最重要的部分是转换规则,负责对输入进行格式转换存于后面指定的参数变量。(通过对C语言中可变参数的工作方式,可理解为什么 scanf 要用 format 来指明后面参数的类型转换,即需要通过对format的内容提取各个规则并解析,然后根据解析结果将对应规则的输入内容转换至合适的可变参数中)。

directives 序列

format 参数字符串包含了 directives 序列,用于描述输入字符序列处理的方式。如果处理 directive 失败,那么不会有任何输入会被读取,同时 scanf() 会退出。

失败的情况有两种:

  1. 输入失败,也就是说输入的字符是不可用的。
  2. 匹配失败,也就是说遇到了不合适的输入。

directives 包括如下情况:

转换规则

format 字符串参数中的 directives 序列,可能是转换规则。

语法表示

每一条转换规则 (conversion specification )都以 % 或者 %n$ 开始,接着:

format 字符串参数中的转换规则主要有两种形式:以 % 开始,或者以 %n$ 开始。两种形式不能够在同样的 format 字符穿参数中同时出现,除非包含 %n$ 转换规则的字符串可以包含 %%%*

  1. 如果 format 包含 % 转换规则,那么这些规则都会依次与接下来的指针参数相对应。
  2. %n$ 形式的转换规则中(这个形式是在 POSIX.1-2001提出的,而非C99),n是一个十进制整数,指明被转换的输入应当被存放在 format 参数后面第n个指针参数引用的地址中。

标记字符

类型修改字符

下面的类型修改字符可以在转换规则中出现:

转换规则标记

下面是可用的转换规则

返回值

成功的时候, scanf() 将会返回被成功匹配以及赋值的输入的项数(而非输入的字符数目),如果在完成前出现了匹配失败的情况,返回值可以比提供的少,甚至是0。

如果在第一个成功的转换或者匹配失败前输入结束,则返回 EOF 。在读取出现错误的时候也会返回 EOF, 这时候流错误指示符号将会被设置, errno 将会表示相应的错误(参见 ferror(3) )。

举例

下面是对scanf的举例,针对如下转换规则:

代码路径: [01_scanfTest/main.c](file:///home/miracle/mygitrepo/quietheart/codes/trunk/codes/c_cppDemo/00_miscellaneous/clib_functions/01_scanfTest/main.c)

#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
        char *whole_str = NULL;
        char *part_str = NULL;
        int v1,v2,v3,v4;
        char a,b,c;
        char A[6]={'1','2','3','4','5','6'};
        //char *A;
        int sc_ret = -1;

        /* 对于%n$规则,输入时,现象:
         * 通过n指定参数的位置,参数位置从1开始(其实可以理解位置0是format字符串本身)
         * %3$c表示将输入传给第三个参数的char*变量。
         * 这样实现了读取输入并“跳跃式”赋值,而非用%方式的读取输入并顺序赋值。
         * */
        printf("scan format:%%3$c%%2$c%%1$c\n");
        sc_ret=scanf("%3$c %2$c %1$c",&a,&b,&c);
        printf("a:%c,b:%c,c:%c\n",a,b,c);
        printf("return: %d\n",sc_ret);

        /* 对于%[规则,输入时,现象
         * 对应的参数必须是char*类型
         * 如果是指针类型,指针不能为NULL,否则不做转换直接返回
         * 如果指针不是预定义数组,那么需要分配空间,存放输入字符
         * 如果自动为对应参数的char*分配空间,比如%m,那么输出的字符串会被存放,但是会在打印的时候打印乱码,像是并没有为字符串结束符号分配空间
         * 如果不使用最大长度限制如%[,那么只能通过不匹配的输入结束此参数的读取。
         * 如果定义为固定大小长度的数组,建议大小要比期望输入的字符多一个,会自动将数组最后一个元素后面追加字符串结束符。
         * 综上,最好的方式是定义长度大于最大长度的数组,用最大长度限制输入的长度如:%5[表示限制为5,则数组长度为6。
         * */
        printf("scan format:%%5[^]0-9-]%%c%%c\n");
        //char *A;
        //sc_ret=scanf("%m[^]0-9-]%c%c",A,&b,&c);//A为NULL没有来得及输入直接返回0,没有转换;A为非空,则一直要求输入,不会停止,除非遇到不匹配的字符,这时候直接返回3并段错误A为乱码。
        //sc_ret=scanf("%5m[^]0-9-]%c%c",A,&b,&c);//使用m分配,输入字符串连同空白一并存储至A;剩余字符输入包括空白传给bc,但是打印的A却是乱码,貌似没有为字符串结束符号分配空间导致。
        //sc_ret=scanf("%m[^]0-9-]%c%c",A,&b,&c);//使用m分配,输入字符串连同空白一并存储至A;剩余字符输入包括空白传给bc,但是打印的A却是乱码,貌似没有为字符串结束符号分配空间导致。
        //
        //sc_ret=scanf("%[^]0-9-]%c%c",&a,&b,&c);//0,1,2不匹配字符集,直接返回0并将0,1,2给下次scanf使用,输入a,b,c很多次,也没有结束输入
        //sc_ret=scanf("%[^]0-9-]%c%c",A,&b,&c);//A为NULL没有来得及输入直接返回0,没有转换;A为非空,则一直要求输入,不会停止,除非遇到不匹配的字符,这时候直接返回0,并段错误。
        sc_ret=scanf("%5[^]0-9-]%c%c",A,&b,&c);//限制输入长度5,数组预先定义长度需要是6(因为会在输入字符之后再追加一个空字符到对应的A中),输入字符串连同空白一并存储至A;剩余字符输入包括空白传给bc。
        printf("A:%s,b:%c,c:%c\n",A,b,c);
        printf("return: %d\n",sc_ret);

        /* 对于%n规则,输入时,现象
         * 必须在参数中提供对应标记位三个的参数,可以输入两个参数:相当于要求参数不变,少要求一个输入。
         * 输入后,第一个n被无视,但是对应输入的1没有被忽略,而是转存至参数v2
         * 虽然由于n导致少要求一个输入,但是如果显示多提供一个参数v4,那个输入的3也不会存入v4。
         * 虽然有%n的存在,却不会忽视*/
        printf("scan format:%%n%%d%%d\n");
        //sc_ret=scanf("%n%d%d",&v1,&v2,&v3);//v1:0,v2:1,v3:2
        sc_ret=scanf("%n%d%d",&v1,&v2,&v3,&v4);//v1:0,v2:1,v3:2
        //sc_ret=scanf("%n%d%d",&v2,&v3);//段错误,无论是输入两个,还是三个参数
        printf("v1:%d,v2:%d,v3:%d,v4:%d\n",v1,v2,v3,v4);
        printf("return: %d\n",sc_ret);//成功返回2,表示有两个参数被转换成功存储。

        /* 对于%*d规则,输入时,现象
         * 必须输入类似1,2,3这样三个参数,可以提供两个参数:相当于要求输入不变,少要求一个参数。
         * 由于*的存在导致第一个%d被无视,输入的1同样被无视,但对应位置的参数v1却不会被忽视
         * 后面参数数目可以是无视后的两个,或者假设无视的三个
         * 只将后两个%d转给接下来的两个参数,第三个参数无影响*/
        printf("scan format:%%*d%%d%%d\n");
        //sc_ret=scanf("%*d%d%d",&v1,&v2,&v3);//v1:2,v2:3,v3:0
        sc_ret=scanf("%*d%d%d",&v2,&v3);//v1:0,v2:2,v3:3
        printf("v1:%d,v2:%d,v3:%d\n",v1,v2,v3);
        printf("return: %d\n",sc_ret);//成功返回2,表示有两个参数被转换成功存储。

        /* 对于%ms规则,输入时,现象
         * 必须输入类似如下:"input whole:111 input part:222"
         * 但在输入之间可以有多个空白,比如 input    whole:111 input part:222" 之类。
         * 非空白字符必须与scanf指定的字符匹配,否则直接退出*/
        printf("scan format:input whole:%%ms, input part:%%ms\n");
        sc_ret=scanf("input whole:%ms\ninput part:%ms",&whole_str,&part_str);
        printf("whole:%s,part:%s\n",whole_str,part_str);//whole:111,part:222
        printf("return: %d\n",sc_ret);//成功返回2,表示有两个参数被转换成功存储。
        free(whole_str);
        whole_str = NULL;
        free(part_str);
        part_str = NULL;

}

其它

一个参考,来自:https://www.cnblogs.com/xmnn1990/p/4722332.html

 C语言如何接收通过键盘输入的任意长度字符串

有时候需要对用户输入的字符串进行处理,由于事先不知道用户会一次性输入多长的字符串,一般有三种处理方法:
1、根据估计用户最多输入字符串长度进行申请空间。
2、使用getch、scanf(%c)等一个字符一个字符的接收处理。
3、使用
while(1)
{
scanf("%1000s",&str);
....
//对str字串进行处理
...
//在末尾
  if(strlen(str)!=1000)//如果长度不为1000说明已经接收完,此时可以跳出循环
   break; 
}
第一种方法的缺点是,用户输入量很有可能比程序员估计的要长。申请小了,会溢出,大了浪费。
第二种方法似乎可行,一个个字符的接收处理,直到遇到回车符为止跳出,但效率不高,每次都需要向系统的输入缓存获取数据需要消耗较多的时间。
第三种方法是对第二种方法的改进,一次最长获取长度为1000的字符串,如果用户输入超过1000,可采用循环接收,每次接收都保存在str里,没有增加额外的储存空间。
显然,第三种方法是最优的。
上一篇 下一篇

猜你喜欢

热点阅读