淺析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代碼的淺析。





發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章