C 語言中的 sscanf 詳解

MElephant發表於2024-05-18

一、函式介紹

函式原型int sscanf(const char *str, const char *format, ...);

返 回 值:成功返回匹配成功的模式個數,失敗返回 -1。

RETURN VALUE

  • These functions return the number of input items successfully matched and assigned, which can be fewer than provided for, or even zero in the event of an early matching failure.
    這些函式返回成功匹配和賦值的輸入項的數目,這個數目可能比提供的要少,或者在早期匹配失敗的情況下甚至為零。

  • The value EOF is returned if the end of input is reached before either the first successful conver‐sion or a matching failure occurs.
    如果在第一次成功轉換或匹配失敗之前到達輸入結束,則返回 EOF 值。

舉 例

iRet = sscanf("123ab", "%[0-9]%[a-z]", sz1, sz2); // iRet = 2, sz1 = "123", sz2 = "ab"
iRet = sscanf("123ab", "%[0-9]%[A-Z]", sz1, sz2); // iRet = 1, sz1 = "123"
iRet = sscanf("123ab", "%[a-z]%[a-z]", sz1, sz2); // iRet = 0
iRet = sscanf("", "%[a-z]", sz1); 			 	  // iRet = -1

二、sscanf函式和正規表示式

以下內容摘抄自:sscanf函式和正規表示式 - km的小天地 - ITeye部落格

備註:實驗五有所糾正。

此文所有的實驗都是基於下面的程式:

char str[10] = "!!!!!!!!!!"; // 10 個感嘆號

我們把 str 的每個字元都初始化為感嘆號,當 str 的值發生變化時,使用 printf 列印 str 的值,對比先前的感嘆號,這樣就可以方便的觀察 str 發生了怎樣的變化。

下面我們做幾個小實驗,看看使用 sscanf 和正規表示式格式化輸入後,str 有什麼變化。

實驗一

(void)sscanf("123456", "%s", str);			// str 的值變為 "123456\0!!!"

這個實驗很簡單,把源字串 "123456" 複製到 str 的前 6 個字元,並且把 str 的第 7 個字元設為 nil 字元,也就是 \0

實驗二

(void)sscanf("123456", "%3s", str);			// str 的值變為 "123\0!!!!!!"

看到沒有,正規表示式的百分號後面多了一個 3,這告訴 sscanf 只複製 3 個字元給 str,然後把第 4 個字元設為 nil 字元。

實驗三

(void)sscanf("abcABC", "%[a-z]", str);		// str 的值變為 "abc\0!!!!!!"

從這個實驗開始我們會使用正規表示式,中括號裡面的 a-z 就是一個正規表示式,它可以表示從 a 到 z 的任意字元。

在繼續討論之前,我們先來看看百分號表示什麼意思:% 表示選擇,% 後面的是條件。比如:

  • 實驗一的 "%s",s 是一個條件,表示任意字元,"%s" 的意思是:只要輸入的東西是一個字元,就把它複製給 str;
  • 實驗二的 "%3s" 又多了一個條件,只複製 3 個字元;
  • 實驗三的 "%[a-z]" 的條件稍微嚴格一些,輸入的東西不但是字元,還得是一個小寫字母,所以實驗三隻複製了小寫字母 "abc" 給 str,別忘了加上 nil 字元。

實驗四

(void)sscanf("AAAaaaBBB", "%[^a-z]", str);	// str 的值變為 "AAA\0!!!!!!"

符號 ^ 表示邏輯非,對於所有字元,只要不是小寫字母,都滿足 "%[^a-z]" 正規表示式。

前 3 個字元都不是小寫字元,所以將其複製給 str,但最後 3 個字元也不是小寫字母,為什麼不複製給 str 呢?這是因為當碰到不滿足條件的字元後,sscanf 就會停止執行,不再掃描之後的字元

符號 ^ 是排除的意思,可以理解為:一直複製,直到遇到我不想複製的字元為止,如可以透過如下模式過濾到行的結尾:%[^\r\n]

實驗五

(void)sscanf("AAAaaaBBB", "%[A-Z]%[a-z]", &str[0], &str[5]); 	// "AAA\0!abc\0!"

先把大寫字母 ABC 複製到 str[0] 開始的記憶體,然後把小寫字母 abc 複製給 str[5] 開始的記憶體。

實驗六

(void)sscanf("AAAaaaBBB", "%*[A-Z]%[a-z]", str);				// "aaa\0!!!!!!"

這個實驗出現了一個新的符號:%*,與 % 相反,%* 表示過濾滿足條件的字元。

在這個實驗中,%*[A-Z] 過濾了所有大寫字母,然後再使用 %[a-z] 把之後的小寫字母複製給 str。如果只有 %* 沒有 % 的話,sscanf 不會複製任何字元到 str,這時 sscanf 的作用僅僅是過濾字串。

實驗七

(void)sscanf("AAAaaaBBB", "%[a-z]", str);						// "!!!!!!!!!!"

做完前面幾個實驗後,我們都知道 sscanf 複製完成後,還會在 str 的後面加上一個 nil 字元,但如果沒有一個字元滿足條件,sscanf 不會在 str 的後面加 nil 字元,str 的值依然是 10 個感嘆號。

這個實驗也說明了,如果不使用 %* 過濾掉前面不需要的字元,你永遠別想取得中間的字元。

實驗八

(void)sscanf("AAAaaaBC=a", "%*[A-Z]%*[a-z]%[^a-z=]", str);		// "BC\0!!!!!!!"
(void)sscanf("AAAaaaBCa=", "%*[A-Z]%*[a-z]%[^a-z=]", str);		// "BC\0!!!!!!!"

這是一個綜合實驗,但這個實驗的目的不是幫我們複習前面所學的知識,而是展示兩個值得注意的地方:

  1. %* 可以使用多次,比如在這個實驗裡面,先用 %*[A-Z] 過濾大寫字母,然後用 %*[a-z] 過濾小寫字母。
  2. ^ 後面可以帶多個條件,且這些條件都受 ^ 的作用,比如 %[^a-z=] 表示 ^a-z^= (既不是小寫字母,也不是等於號)。

實驗九

int k;
(void)sscanf("AAA123BBB456", "%*[^0-9]%i", &k);					// k = 123

首先,%*[^0-9] 過濾掉前面非數字的字元,然後用 %i 把數字字元轉換成 int 型的整數,複製到變數 k,注意引數必須使用 k的地址。

三、避免 sscanf 寫記憶體越界

如上實驗一和實驗二,如果不對長度進行限制,則預設將匹配成功的字元都寫入到 str 中,思考這麼一個問題:如果源資料的長度 > 接收陣列的長度,會不會寫越界呢?

實驗十

/**
 * | str | 低地址 eb80
 * | tmp | 高地址 eb90
 */
char tmp[16] = "!!!!!!!!!!!!!!!!"; // 16 個感嘆號
char str[16] = {0};

// tmp = "!!!!!!!!!!!!!!!!"
(void)sscanf("0123456789abcdef-123", "%s", str);
// tmp = "-123\0!!!!!!!!!!!"

在本實驗中,我們申請了兩塊長度均為 16 的字串 tmp 和 str,其中 tmp 用感嘆號初始化。

嘗試用 str 去讀取長度為 20 的源字串 "0123456789abcdef-123",由於沒有限制 sscanf 的處理長度,所以 sscanf 會寫越界,將多出來的 "-123" 寫入到了 tmp 中。

記憶體讀寫越界嚴重時會導致程式崩潰,所以我們要儘可能去避免,而對於 sscanf 來說,可以透過限制處理的長度來保證不會寫越界:

// tmp = "!!!!!!!!!!!!!!!!"
(void)sscanf("0123456789abcdef-123", "%15s", str); // str[15] 儲存結束符 '\0'
// tmp = "!!!!!!!!!!!!!!!!", str = "0123456789abcde"

但是這麼寫有個弊端,那就是如果 str 的長度發生變化,sscanf 中也需要同步修改,這對於程式維護而言肯定是不方便的。

目前還沒有想到一個很好的方法可以解決這個問題,如果大家有更好的方法,請不吝賜教。

最後,附上實驗的 Demo

#include <stdio.h>

void Print(const char *pcStr, size_t ulStrLen)
{
    for (int i = 0; i < ulStrLen; i++)
    {
        if (pcStr[i] == 0) printf("\\0");
        else printf("%c", pcStr[i]);
    }
    puts("");
    return;
}

void Test1()
{
    char str[10] = "!!!!!!!!!!";
    (void)sscanf("123456", "%s", str);

    Print(str, sizeof(str));
}

void Test2()
{
    char str[10] = "!!!!!!!!!!";
    (void)sscanf("123456", "%3s", str);

    Print(str, sizeof(str));
}

void Test3()
{
    char str[10] = "!!!!!!!!!!";
    (void)sscanf("abcABC", "%[a-z]", str);

    Print(str, sizeof(str));
}

void Test4()
{
    char str[10] = "!!!!!!!!!!";
    (void)sscanf("AAAaaaBBB", "%[^a-z]", str);

    Print(str, sizeof(str));
}

void Test5()
{
    char str[10] = "!!!!!!!!!!";
    (void)sscanf("AAAaaaBBB", "%[A-Z]%[a-z]", &str[0], &str[5]);

    Print(str, sizeof(str));
}

void Test6()
{
    char str[10] = "!!!!!!!!!!";
    (void)sscanf("AAAaaaBBB", "%*[A-Z]%[a-z]", str);

    Print(str, sizeof(str));
}

void Test7()
{
    char str[10] = "!!!!!!!!!!";
    (void)sscanf("AAAaaaBBB", "%[a-z]", str);

    Print(str, sizeof(str));
}

void Test8()
{
    char str[10] = "!!!!!!!!!!";
    (void)sscanf("AAAaaaBC=", "%*[A-Z]%*[a-z]%[^a-z=]", str);
    Print(str, sizeof(str));
}

void Test9()
{
    int k;
    (void)sscanf("AAA123BBB456", "%*[^0-9]%i", &k);
    printf("%d\n", k);
}

void Test10()
{
    /**
     * | str | 低地址 eb80
     * | tmp | 高地址 eb90
     */
    char tmp[16] = "!!!!!!!!!!!!!!!!";
    char str[16] = {0};

    Print(tmp, sizeof(tmp));    // tmp = "!!!!!!!!!!!!!!!!"
    (void)sscanf("0123456789abcdef-123", "%s", str);
    Print(tmp, sizeof(tmp));    // tmp = "-123\0!!!!!!!!!!!"

    return;
}

void Test11()
{
#define LEN_8 8
#define TO_STR(x) #x
#define SSCANF_LEN_LIMIT(len) TO_STR(%len)

    char szBuf[LEN_8 + 1];
    (void)sscanf("12345abcde", SSCANF_LEN_LIMIT(LEN_8) "[0-9a-z]", szBuf); // "%8" "s"
    puts(szBuf);

    return;
}

int main()
{
    Test11();
    return 0;
}

相關文章