浅析Scanf源码

记得当初从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代码的浅析。

时间: 2024-11-08 23:00:26

浅析Scanf源码的相关文章

浅析libuv源码-获取精确时间

在Timer模块中有提到,libuv控制着延迟事件的触发,那么必须想办法精确控制时间. 如果是JS,获取当前时间可以直接通过Date.now()得到一个时间戳,然后将两段时间戳相减得到时间差.一般情况下当然没有问题,但是这个方法并不保险,因为本地计算机时间可以修改. libuv显然不会用这么愚蠢的办法来计算时间,C++内部有更为精妙的方法来处理这个事. 首先在上一节中,一个简单的事件轮询代码如下: int main() { uv_loop_t *loop = uv_default_loop();

浅析libuv源码-node事件轮询解析(1)

好久没写东西了,过了一段咸鱼生活,无意中想起了脉脉上面一句话: 始终保持自己的竞争力.所以,继续开写! 一般的JavaScript源码看的已经没啥意思了,我也不会写什么xx入门新手教程,最终决定还是啃原来的硬骨头,从外层libuv => node => v8一步步实现原有的目标吧. libuv核心还是事件轮询,前几天从头到尾看了一遍官网的文档,对此有了一些更深的理解. (虽然现在开发用的mac,但是为了衔接前面的文章,所以代码仍旧以windows系统为基础,反正差别也不大) 首先看一眼官网给的

浅析libuv源码-node事件轮询解析(4)

这篇应该能结,简图如下. 上一篇讲到了uv__work_submit方法,接着写了. void uv__work_submit(uv_loop_t* loop, struct uv__work* w, enum uv__work_kind kind, void (*work)(struct uv__work* w), void (*done)(struct uv__work* w, int status)) { // 上篇主要讲的这里 初始化线程池等 uv_once(&once, init_on

阿里架构师浅析ThreadLocal源码——黄金分割数的使用

一. 前提 最近接触到的一个项目要兼容新老系统,最终采用了ThreadLocal(实际上用的是InheritableThreadLocal)用于在子线程获取父线程中共享的变量.问题是解决了,但是后来发现对ThreadLocal的理解不够深入,于是顺便把它的源码阅读理解了一遍.在谈到ThreadLocal之前先买个关子,先谈谈黄金分割数.本文在阅读ThreadLocal源码的时候是使用JDK8(1.8.0_181). 二. 黄金分割数与斐波那契数列 首先复习一下斐波那契数列,下面的推导过程来自某搜

浅析pinyin4j源码 简单利用pinyin4j对中文字符进行自然排序

pinyin4j项目  官网地址 http://pinyin4j.sourceforge.net/ 我们先把资源下载下来,连同源码和jar包一起放入工程.如下图: 接下来在demo包下,我们写一个测试类,简单使用pinyin4j对中文字符进行自然排序 新建一个ConvertTest.java package demo; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; im

浅析pinyin4j源码 简单利用pinyin4j对中文字符进行自然排序(转)

pinyin4j项目  官网地址 http://pinyin4j.sourceforge.net/ 我们先把资源下载下来,连同源码和jar包一起放入工程.如下图: 接下来在demo包下,我们写一个测试类,简单使用pinyin4j对中文字符进行自然排序 新建一个ConvertTest.java package demo; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; im

浅析easyswoole源码Di原理与实现

依赖注入 简介:Dependency Injection 依赖注入 EasySwoole实现了简单版的IOC,使用 IOC 容器可以很方便的存储/获取资源,实现解耦. 使用依赖注入,最重要的一点好处就是有效的分离了对象和它所需要的外部资源,使得它们松散耦合,有利于功能复用,更重要的是使得程序的整个体系结构变得非常灵活. 在我们的日常开发中,创建对象的操作随处可见以至于对其十分熟悉的同时又感觉十分繁琐,每次需要对象都需要亲手将其new出来,甚至某些情况下由于坏编程习惯还会造成对象无法被回收,这是相

Volley框架源码浅析(一)

尊重原创http://blog.csdn.net/yuanzeyao/article/details/25837897 从今天开始,我打算为大家呈现关于Volley框架的源码分析的文章,Volley框架是Google在2013年发布的,主要用于实现频繁而且粒度比较细小的Http请求,在此之前Android中进行Http请求通常是使用HttpUrlConnection和HttpClient进行,但是使用起来非常麻烦,而且效率比较地下,我想谷歌正式基于此种原因发布了Volley框架,其实出了Voll

PM2源码浅析

PM2工作原理 最近在玩一个游戏,<地平线:黎明时分>,最终Boss是一名叫黑底斯的人,所谓为人,也许不对,黑底斯是一段强大的毁灭进程,破坏了盖娅主进程,从而引发的整个大陆机械兽劣化故事. 为什么要讲这么一段呢,是希望大家可以更好地理解pm2的原理,要理解pm2就要理解god和santan的关系,god和santan的关系就相当于盖娅和黑底斯在pm2中的01世界中,每一行代码每一个字节都安静的工作god就是Daemon进程 守护进程,重启进程,守护node程序世界的安宁,santan就是进程的