scanf,简明精干之美
基本语法
scanf的函数签名为:
int scanf(const char* format, ...);
其中format为格式字符串,后跟不定参数传递待赋值变量的地址。
较高级的C语言提倡使用scanf_s代替scanf以确保内存安全,其函数签名为
int scanf_s(const char* format, ...)
。用法区别为,不定参数列表需要传递待赋值变量的地址和最大写入内存大小,如:
scanf_s("%d", &var, sizeof(int));
scanf()的核心在于格式字符串上。
format-string的说明符形式为:
[=%[*][width][modifiers]type=]
其中,%标志说明符的开始("%%"符号的escape-character是"%%");
*是一个可选字符,任何被指定了*的format-string均不会被赋值至对应的变量中(相当于这一段scan后被舍弃)。
width表示了该说明符读取的最大字符数;
modifiers表示一个对变量类型的修饰符(“长”“短”一类,比如ll表示long
long,s表示short,用于修饰%d,%u,%f);
type的“全解”如下。
以下部分内容可能会因编译器不同而有所不同,仅供MSVC
19.3参考。
d: 表示读入一个十进制整数。使用之时会优先读取前置负号,如果负号后没有十进制字符,则该次读取失败(变量不会被赋值),但负号已经从流中被读取(“取出”)。
函数的对应预期变量是一个int*
,并将以"1+31"的方式将读取的数值写入该地址(第一位符号,后31位是数值,表示范围\(-2^{31} ~ 2^{31} - 1\)。
如果需要读取long/long long/short整型,请将modifiers指定为l/ll/s(应用形如%lld
)。long long的范围自然扩展至\(-2^{63} ~ 2^{63} - 1\)。 如果发生溢出,则结果为未定义行为(Undefined Behavior)。s: 表示读入一个字符串。此处“字符串”的定义是连续的任意字符,直至终止字符('\0')或空白字符(换行、水平/垂直制表符、空格)。读取时,%s不会读取这些终止符(亦不会将之从流中取出)。
函数的对应预期变量是一个char*
,并将读取到的所有字符(最大数目由width指定)依次写入其中,并在最后追加一个'\0',所以需要预留的内存空间至少是(width+1)*sizeof(char)
。c:表示读入单个字符。如果指定了width,则会连续读取width个字符,存储在该对应地址开始的连续位置(增量为
sizeof(char)
),且末尾不会追加'\0'。 函数的对应预期变量是一个char*
。u:表示读入一个无符号十进制整数。 函数的对应预期变量是一个
unsigned int*
。f:表示读入一个浮点数。 函数的对应预期变量是一个
float*
。如需读取至double*
,请使用%lf
;long double则请使用%llf
。
浮点数应形如:[符号]十进制数[.十进制数][e[符号]十进制数]。
有效的浮点数实例: 3;3.14;+6.24e5;-9e-7。
x,X: 十六进制整数,除字符集合改为所有的十六进制字符(大小写均有效)外行为与d相似。十六进制数前的'0x'/'0X'前缀是可选的。 函数的对应预期变量是一个
int*
,修饰规则与前述相同。o: 八进制整数,不再赘述。
i: 读取一个整数,行为类似于python中的
int(x,0)
(即自动根据前缀判断进制),0-八进制,0x/0X-十六进制,其它-十进制(参考C中的numeric constants规则)。[]:扫描字符集合,集成了一个弱化版的正则表达式。
具体地,[]内部可以放置扫描字符的集合,集合的表达方式有逐个列出/横杠表示范围,并支持'^'表示'否'。
举例:%[a-zA-Z]
扫描所有字母;%[^\n]
扫描直至换行符(相当于获取一整行);%1[x]
获取至多一个x字符。
'^'的转义字符为'^',但在C的源代码中应写作
\\^
(因为''自身会被预处理转义)。
返回值:赋值成功的变量数,若到达文件末尾则返回EOF(负数宏,在许多平台上通常为-1)。
当且仅当赋值成功的变量数为0且到达文件尾时,返回EOF(亦即只要存在赋值成功的变量,即不会返回负数)。
底层逻辑
假设scanf的目标流为in。scanf从左往右扫描format-string,遇到以下字符时分别采取对应的措施:
-
specifier(如上):尝试根据specifier的规则处理in中的下一段符合要求的字符;
若specifier没有加星号(*),则取va_arg(不定参数列表)中的下一个参数,对之解引用并将数据写入其中。
-
任意空白字符:将format-string读取点前进至下一个非空白字符,同时跳过流in中所有的空白字符。即,任意数目的连续空白字符对于scanf而言是等效的。
-
其它字符:scanf将从流中尝试取走一个给定的字符(char)
,行为类似于给定形如
%*1[(char)]
。
重点来咯!!!
对于上述的第1、3种情况(即扫描format-string需要处理的是specifier/其它非空白字符),一旦读取失败,则对format-string的扫描终止,读取结束。
第2种情况(处理空白字符)时,即使流中没有空白字符,依然继续。
每次调用scanf时,如果specifier不是
%c
或扫描字符合集%[]
,则会自动先跳过所有的空白字符一次。
假设待读取流形如 1 2\n34abc d
,代码及读取效果示例:
1 | void input1() |
实战示例
Talk is cheap. Show me the code.
1 |
|
PTA与巧用scanf有关的题目示例
一元多项式函数求导(强化版)
给出一个一元多项式函数 (形如\(f(x)=ax^b+cx^d+...\)),求出其导函数,结果用标准书写格式输出,必须合并同类项,降幂排列。如果结果多项式为空,仅输出0。
每一项的系数绝对值小于100,指数为小于100的非负整数。输入字符串长度不超过3000。变量用字母 x 表示。题目保证输入的多项式均合法。
输入格式:
在一行中输入一个多项式字符串。输出格式:
在一行中按照标准书写格式输出字符串。输入样例:
x^4-3x^2+4
输出样例:
4x^3-6x
1 |
|
注释:
在scan前置系数上,采用了统一interface的策略(即在接收string的第一个字符设置为'+',规避开第一项的'+'缺失问题)。
<stdlib.h>
中的atoi
可以帮助处理数字字符。每一步都是借助弱化的regex来“步步为营”,
比如scanf("%1[x]",temp)
试探有无'x',若无则continue
;
scanf("%1[\\^]",temp)
试探有无'^',若无则确定为一次幂。[]
的优点体现在strong exception guarantee若失败则不会产生其它影响。