记得当初从C语言学习开始就使用scanf,关于scanf的用法也略知一二,对使用scanf出现的问题并未进行深刻探究,故笔者打算对scanf实现进行探究。
如何找到scanf源码
关于VC中的CRT代码在 VS目录下的\VC\crt\src中,我们就先把scanf.c扒出来。
int __cdecl scanf ( const char *format, ... ) { va_list arglist; va_start(arglist, format); return vscanf_fn(_input_l, format, NULL, arglist); }
scanf函数实际调用的是vscanf_fn
int __cdecl vscanf_fn ( INPUTFN inputfn, const char *format, _locale_t plocinfo, va_list arglist ) /* * stdin 'SCAN', 'F'ormatted */ { int retval = 0; _VALIDATE_RETURN( (format != NULL), EINVAL, EOF); _lock_str2(0, stdin); __try { retval = (inputfn(stdin, format, plocinfo, arglist)); } __finally { _unlock_str2(0, stdin); } return(retval); }
实际上我们是根据inputfn这个函数指针来进行我们的scanf操作,我们继续来找这个inputfn出处。
我们在input.c中找到了真正的处理函数,我们的重点是解析这个文件,我们来看input函数说明就知道了。
/*** *int _input(stream, format, arglist), static int input(format, arglist) * *Purpose: * get input items (data items or literal matches) from the input stream * and assign them if appropriate to the items thru the arglist. this * function is intended for internal library use only, not for the user * * The _input entry point is for the normal scanf() functions * The input entry point is used when compiling for _cscanf() [CPRFLAF * defined] and is a static function called only by _cscanf() -- reads from * console. * * This code also defines _input_s, which works differently for %c, %s & %[. * For these, _input_s first picks up the next argument from the variable * argument list & uses it as the maximum size of the character array pointed * to by the next argument in the list. * *Entry: * FILE *stream - file to read from * char *format - format string to determine the data to read * arglist - list of pointer to data items * *Exit: * returns number of items assigned and fills in data items * returns EOF if error or EOF found on stream before 1st data item matched * *Exceptions: * *******************************************************************************/
浅析源码前准备
首先来学习几个关键函数和Macro的用法:
【注意:nolock针对的是线程】msdn介绍:
_nolock that they do
not lock the calling thread. They might be faster because they do not incur the overhead of locking out other threads. Use these functions only in thread-safe contexts such as single-threaded applications or where the calling scope already handles thread isolation.
static _TINT __cdecl _inc(FILE* fileptr) { return (_gettc_nolock(fileptr)); }
_inc 调用_getcc_nolock获得缓冲区读取到的一个字符 。
static void __cdecl _un_inc(_TINT chr, FILE* fileptr) { if (_TEOF != chr) { _ungettc_nolock(chr,fileptr); } }
_un_inc 调用的_ungettc_nolock将字符chr重新放入fileptr中。【这个un-字面意思让我很苦恼,后来仔细一读才知道这个意思】
static _TINT __cdecl _whiteout(int* counter, FILE* fileptr) { _TINT ch; do { ++*counter; ch = _inc(fileptr); if (ch == _TEOF) { break; } } while(_istspace((_TUCHAR)ch)); return ch; }
_whiteout这个意思很直白,把空白字符(包括‘ ‘,‘\n‘,‘\t‘等)全部输出来直到遇到第一个非空白字符【此时空白字符我们已经把它取出来了,后面过程我们得先把它放回去】
#define _gettc_nolock _getc_nolock #define _getc_nolock(_stream) _fgetc_nolock(_stream) #define _fgetc_nolock(_stream) (--(_stream)->_cnt >= 0 ? 0xff & *(_stream)->_ptr++ : _filbuf(_stream))
最后一个Maroc检查缓冲区中可读字符数,如果为0则清空缓冲区。
#define INC() (++charcount, _inc(stream)) #define UN_INC(chr) (--charcount, _un_inc(chr, stream)) #define EAT_WHITE() _whiteout(&charcount, stream)
现在来看,这三个Macro就很简单了:
①INC() 读取字符
②UN_INC(chr) 放回字符
③EAT_WHITE() 把空白字符全给吃掉!
有了上面的基础,对缓冲区流文件的处理过程就没难度了。【Windows总喜欢把各种操作归结为对FILE(文件)的操作,比如API :CreateFile】
浅析代码
代码中的swich case之间包含来了很多goto语句【写代码的是相当资深的老司机】
不多提其他,对format解析开头开始:
while (*format) { if (_istspace((_TUCHAR)*format)) { UN_INC(EAT_WHITE()); /* put first non-space char back */ do { tch = *++format; } while (_istspace((_TUCHAR)tch)); continue; ………………
这里的UN_INC(EAT_WHITE()),是把当初EAT_WHITE读出的第一个非空白字符再放入缓冲区。
上面代码完成对键盘缓冲区中空白符的清理,直到正常读取第一个字符。
当读入%号,进行处理:
if (_T('%') == *format && _T('%') != *(format + 1))
我们能找到各种各样的格式化输入,比如:
格式字符 说明
%a 读入一个浮点值(仅C99有效)
%A 同上
%c 读入一个字符
%d 读入十进制整数
%i 读入十进制,八进制,十六进制整数
%o 读入八进制整数
%x 读入十六进制整数
%X 同上
%c 读入一个字符
%s 读入一个
%f 读入一个浮点数
%F 同上
%e 同上
%E 同上
%g 同上
%G 同上
%p 读入一个指针
%u 读入一个无符号十进制整数
%n 至此已读入值的等价字符数
%[] 扫描字符集合
%% 读%符号
%* 指定类型的数据但不保存
在此我们主要解析 %[ ] 和 %*
① 通过定制我们的扫描集%[ ],让输入更加灵活,比如
scanf("%[a-zA-Z]",&chr); //实现只能输入a-z,A-Z
scanf("%[^a-z]",&chr); //实现输入非a-z
scanf("%[^\n]",str); //实现可读取回车
② %*读取指定类型数据,不保存
scanf("%*d%c", &i); //读取%d但不保存,将读取的%c保存到i
③^代表反转的意思
下面我们来看实现代码:
if (_T('^') == *scanptr) { ++scanptr; --reject; /* set reject to 255 */ } /* Allocate "table" on first %[] spec */ #if ALLOC_TABLE if (table == NULL) { table = (char*)_malloc_crt(TABLESIZE); if ( table == NULL) goto error_return; malloc_flag = 1; } #endif /* ALLOC_TABLE */ memset(table, 0, TABLESIZE); if (LEFT_BRACKET == comchr) if (_T(']') == *scanptr) { prevchar = _T(']'); ++scanptr; table[ _T(']') >> 3] = 1 << (_T(']') & 7); } while (_T(']') != *scanptr) { rngch = *scanptr++; if (_T('-') != rngch || !prevchar || /* first char */ _T(']') == *scanptr) /* last char */ table[(prevchar = rngch) >> 3] |= 1 << (rngch & 7); else { /* handle a-z type set */ rngch = *scanptr++; /* get end of range */ if (prevchar < rngch) /* %[a-z] */ last = rngch; else { /* %[z-a] */ last = prevchar; prevchar = rngch; } /* last could be 0xFF, so we handle it at the end of the for loop */ for (rngch = prevchar; rngch < last; ++rngch) { table[rngch >> 3] |= 1 << (rngch & 7); } table[last >> 3] |= 1 << (last & 7); prevchar = 0; } }
reject反转标记,如果出现^ 则reject = FF; 其后方便进行 ^ 进行反转。
对于[ ]字符集,有一个char table[32]来保存256个ascii字符。【此处每个char为8bits,所以有32组可以完全包含256个ascii字符】
微软对table中字符做了这样的处理:
table[rngch >> 3] |= 1 << (rngch & 7);
即:将所读的字符串分到32组中【rngch>>3相当于除以8】,每个table[n]有8bits,每个bit中,出现的字符位会被置为1,未出现则为0,这样就完美囊括了256个ASCII字符。
判断字符是否存在,直接这样处理:
(table[ch >> 3] ^ reject) & (1 << (ch & 7))
以上是我当初并不所知的用法,下面我们来探究%d等的用法
当初时常写代码出现这种情况:
char a; char b; scanf("%c", &a); printf("%c", a); scanf("%c", &b); printf("%c", b);
当键入一个字符回车以后,发现无法再键入第二个字符,断点调试发现\n被保存入了b中,这是因为case 为 c时,\n进入并被保存给了b,要解决这个问题最一般的做法也就似刷新缓冲区之类的。
int a; int b; scanf("%d", &a); printf("%d", a); scanf("%d", &b); printf("%d", b);
当键入1,回车,2时并未发现赋值错误的情况,这也是%d的处理方式问题,我们来看case d:
在其中有很多判断_ISXDIGIT(ch)的,假若不是阿拉伯数字,则会执行跳出当前%d字符读取,执行1313行的 ++format; /* skip to next char */
即:%d跳过了\n的读取,继续读取下一个字符。
代码结构如下:
if (_T('%') == *format && _T('%') != *(format + 1)) { …………………… ++format; /* skip to next char */ } else /* ('%' != *format) */ { ……………………… }
在读代码时候读到一个函数 hextodec还不错:
static _TINT __cdecl _hextodec ( _TCHAR chr) { return _ISDIGIT(chr) ? chr : (chr & ~(_T('a') - _T('A'))) - _T('A') + 10 + _T('0'); }
将读取的16进制字符 0 - F转成 10进制数
以上是我对scanf代码的浅析。