C语言-深入浅出scanf
这里将对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()
会退出。
失败的情况有两种:
- 输入失败,也就是说输入的字符是不可用的。
- 匹配失败,也就是说遇到了不合适的输入。
directives
包括如下情况:
- 空白字符序列(space/tab/newline等,参见
isspace()
)。directives
匹配输入中任何数目的空白,包括没有空白。 - 一般字符(即,不是空白,也不是
%
)。这个字符必须精确地匹配输入的下一个字符。 -
conversion specification
(即转换规则), 以%
字符开始,输入的字符序列会根据转换规则进行转换,转换的结果会存储到对应的指针参数中。如果下一个输入的条目与转换规则不匹配,那么转换失败,也就是前面提到的匹配失败
的情况。
转换规则
format
字符串参数中的 directives
序列,可能是转换规则。
语法表示
每一条转换规则 (conversion specification
)都以 %
或者 %n$
开始,接着:
- 一个可选的
*
(抑制赋值字符),scanf()
根据转换规则读取输入,但是会忽略这个输入。不会有相应的指针参数与之对应,并且这个规则也不会被计算在scanf()
返回的成功的赋值的次数上。 - 对于整数类型的转换,在起始标记后面可以有一个引号 ' , 这个规则会导致一些以千为单位分割的数字被读取(例如:1,000,000表示100万),通常会被当前语言选项的
LC_NUMERIC
类型定义(参见setlocale(3)
)。引号可在*
字符的前面或者后面。 - 一个可选的
m
字符。经常用于字符串类型转换(string conversions
如%s
、%c
、%[
),并为调用者避免了为存储被转换的输入字符串而申请额外缓存空间。在起始标记后面加上m
之后,scanf()
会根据用户输入自动分配足够的空间,并将其地址赋值给后面的指针参数,指针类型应该是char*
类型的(不需要在调用scanf()
之前初始化它以及分配空间),当然使用之后,要通过free来释放对应的指针空间。 - 一个可选的十进制整数,指定最大宽度来限制输入的长度(
maximum field width
)。字符的读取会在到达最大宽度或一个不匹配的字符被读取时停止。多数转换规则会忽略输入初始空白字符(后面会指出例外情况),这些空白字符不会被计算到整数数字对应的限制(最大宽度)里面。对于字符串输入转换规则,都会为字符串存储一个结束符号'\0',表示字符串的结尾,但是如果使用了这里的数字限制输入长度,当用户输入的字符串超过长度限制的时候,将不会在后面存储相应的字符串结束符号'\0'了。 - 一个可选的类型修正字符(
type modifier character
)。例如l
类型修正用于整数转换规则如%d
, 用来指明对应的指针参数是一个long int
类型而非修正前(即%d
)的int
类型指针。 - 一个转换规则标记,指明将输入转换的类型(例如使用了标记
d
的转换规则%d
, 表示将输入转成整数)。
format
字符串参数中的转换规则主要有两种形式:以 %
开始,或者以 %n$
开始。两种形式不能够在同样的 format
字符穿参数中同时出现,除非包含 %n$
转换规则的字符串可以包含 %%
和 %*
。
- 如果
format
包含%
转换规则,那么这些规则都会依次与接下来的指针参数相对应。 - 在
%n$
形式的转换规则中(这个形式是在 POSIX.1-2001提出的,而非C99),n是一个十进制整数,指明被转换的输入应当被存放在format
参数后面第n个指针参数引用的地址中。
标记字符
类型修改字符
下面的类型修改字符可以在转换规则中出现:
- h 表示转换的可能是d,i,o,u,x,X,或n中的一个,接下来的指针指向
short int
或者unsigned short int
而非int
。 - hh 类似h,但是接下来的是一个指向
singed char
或unsiged char
的指针。 - j 类似h, 但是接下来的是一个指向
intmax_t
或者uintmax_t
的指针。此修改字符在C99
引用。 - l 表示转换的可能是d,i,o,u,x,X,或n中的一个,接下来的指针指向
long int
或者unsigned long int
而非int
;也可能转换的是e,f,或g中的一个,接下来的指针指向double
而非 float 。指定两个 l 字符等价于 L 。如果与%c或%s一起使用, 那么相应的指针参数会被认为指向宽字符或者宽字符串。 - L 表示转换的可能是e,f或g之一,对应的指针指向
long double
; 或者转换的是 d,i,o,u或x之一,对应指针指向long long
类型。 - q 等价于 L, ANSI没有这个规则。
- t 类似h,但是接下来的指针指向
ptrdiff_t
类型。此修改字符在C99期间引入。 - z 类似h,但是接下来的指针指向
size_t
类型。此修改字符在C99期间引入。
转换规则标记
下面是可用的转换规则
- % 匹配一个字符
%
。也就是说%%
在format
字符串参数中的对应位置匹配一个单个的%
字符输入。这个规则不会有转换发生(除了最开始的空白字符会被忽略),因为不会有赋值动作(对应的指针)。 - d 匹配一个可选的单个10进制整数(
%d
);对应的指针必须指向int
类型。 - i 匹配一个可选的整数,对应的指针必须指向
int
类型。如果整数以0x
或0X
开头,则表示十六进制;如果以0
开头则表示8进制;其它情况认为是10进制整数。输入时,只能使用对应进制的字符。 - o 匹配无符号8进制整数,对应的指针必须指向
unsigned int
类型。 - u 匹配无符号10进制整数,对应的指针必须指向
unsigned int
类型。 - x 匹配无符号16进制整数,对应的指针必须指向
unsigned int
类型。 - X 与
x
规则一样。 - f 匹配可选的有符号浮点数,对应的指针必须指向
float
类型。 - e 与
f
规则一样。 - g 与
f
规则一样。 - E 与
f
规则一样。 - a 与
f
规则一样。C99支持。 - s 匹配非空白字符串序列,对应的指针必须指向一个首元素为字符类型的字符数组,并且数组的长度足以容纳输入的整个字符串序列以及最终的字符串终结符号 (
\0
) ,字符串终结符号是自动被添加的。输入字符串的读取操作会在遇到空白、或达到指定的最大宽度(maximum field width)之时停止读取(并将读取的内容赋值至对应的字符数组内)。 - c 匹配一个字符序列,该序列的长度通过最大宽度(maximum field width, 前面提到过)指定,默认宽度值为1,对应指针必须指向
char
类型,并且必须有足够的空间容纳所有的字符(不会自动添加字符串终结符号)。通常输入的起始空白忽略在这里会被阻止。如果想要首先忽略空白,需要显式地将空格放在format中使用。 - [ 在一个指定的可接受字符集中匹配一个非空字符串序列,对应的指针必须指向
char
类型,并且必须有足够的空间容纳字符串中所有的字符以及字符串终结符号。通常的起始空白忽略在这里会被阻止。这个字符串由一个字符集或者对应的非集中的字符组成。集合的定义介于中括号起始符号[
以及终止符]
之间,如果指定的是字符集之外的字符,那么需要在集合起始(即中括号起始符[)之后紧接着一个音调符号^
。如果想要在字符集或者非集中包含中括号终止符]
, 那么将这个]紧邻这放在集合起始符号[
的后面,或者非集符号^
后面,放在其它任何地方都会导致集合描述终止。连字符-
也是特殊字符,如果连字符位于两个其它字符之间,那么这两个字符对应编码之间的字符也被填入集合。如果想要在字符集中包含连字符-
, 那就将它放到所有集合的最后一个字符位置上,其后紧接着集合描述终止符号]
。例如:[^]0-9-]
表示匹配任何除了]
字符、0-9
之间数字字符、以及连字符-
之外的所有字符。这个转换规则会在输入字符串时遇到一个与字符集不匹配的字符,或者超过长度限制(field width
的时候停止读取输入。(即:^,],-
这三个字符在这个字符集里是特殊字符,必须至于特定的位置才能正确表达这些特殊字符) - p 匹配一个指针值(类似
printf(3)
中%p
打印的那样),对应的指针参数必须指向一个void
类型的指针,即void*
。 - n 不期望有任何匹配,相反的,相应数目的字符会被消耗,而并非将输入转换并存储到一个对应的指向
int
类型的指针中。这其实并不是一个转换规则,也不会增加scanf()
函数的返回值(成功匹配的项数)。赋值可以通过*
(赋值抑制)字符被阻止,但是接下来对返回值的影响将会是未定义的。因此,%*n
转换规则是不应该被使用的规则。(此需要举例理解?)
返回值
成功的时候, scanf()
将会返回被成功匹配以及赋值的输入的项数(而非输入的字符数目),如果在完成前出现了匹配失败的情况,返回值可以比提供的少,甚至是0。
如果在第一个成功的转换或者匹配失败前输入结束,则返回 EOF
。在读取出现错误的时候也会返回 EOF
, 这时候流错误指示符号将会被设置, errno
将会表示相应的错误(参见 ferror(3)
)。
举例
下面是对scanf的举例,针对如下转换规则:
- 使用
%n$
前缀的例子 - 前缀后面字符是 *,',m的例子
- 使用 [与n标记的例子
-
format
中包含要求输入精确匹配的非空字符的例子
代码路径: [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里,没有增加额外的储存空间。
显然,第三种方法是最优的。