C語言格式化輸入

2020-07-16 10:04:29
當從一個格式化資料來源中讀取資料時,C 語言提供了 scanf()函數系列。與 printf()函數一樣,scanf()函數需要一個格式化字串作為其引數,以控制 I/O 格式與程式內部資料之間的轉換。本文介紹在 scanf()和 printf()函數中使用格式化字串和轉換修飾符的差異。

scanf()函數系列

各種 scanf()函數處理輸入源字元的方式都是相同的。不同的是這些函數所針對的資料來源種類,以及它們接收引數的方式。下面的 scanf()函數針對位元組導向流:
int scanf(const char*restrict format,...);
從標準輸入流 stdin 中讀取資料。
int fscanf(FILE*restrict fp,const char*restrict format,...);
從 fp 所參照的輸入流中讀取資料。
int sscanf(const char*restrict src,const char*restrict format,...);
從 src 指向的 char 陣列中讀取資料。

省略號表示還有更多的可選引數。可選引數是指向變數的指標,scanf()函數將轉換結果儲存在這些變數中。

類似於 printf()函數,scanf()函數系列也包含變體版本。變體版本將一個指標作為引數,指向一個參數列,而不是在函數呼叫時直接接收數量可變的引數。

這些變體版本的函數名稱以字母 v 開頭,表示“variable argument list”(可變參數列):例如,vscanf()、vfscanf()和 vsscanf()。如果想使用支援可變參數列的函數,除了標頭檔案 stdio.h 以外,還必須包含標頭檔案 stdarg.h。

這些函數都具有相應的針對寬字元導向流的版本。寬字元函數名稱中包含 wscanf 而不是 scanf,例如 wscanf()和 vfwscanf()。

C11 標準為這些函數都提供了一個新的“安全”的版本。這些對應的新函數均以字尾 _s(如 fscanf_s())。新函數測試在讀入一個字串到陣列之前,是否超出了陣列邊界。

格式化字串

scanf()函數的格式化字串包含普通字元和轉換說明,轉換說明定義了如何解釋以及轉換讀入的字元序列。scanf()函數所使用的大多數轉換修飾符都與 printf()函數所定義的一樣。然而,scanf()函數的轉換說明沒有標記和精度選項。針對 scanf()函數轉換說明的通用語法如下所示:

%[*][欄位寬度][長度修飾符]修飾符


對於格式化字串中的每個轉換說明,從輸入源讀入的字元的數量與轉換方式都會與轉換修飾符一致。結果會儲存在對應指標引數所指向的物件中。如下例所示:
int age = 0;
char name[64] = "";
printf( "Please enter your first name and your age:n" );
scanf( "%s%d", name, &age );

假設使用者在提示符下輸入如下內容:
Bob 27n

呼叫 scanf()函數,會將字串 Bob 寫進 char 陣列 name 中,然後將 27 寫進 int 變數 age 中。

所有的轉換說明,除了具有修飾符 c 的情況以外,都會忽略前面的空白字元(whitespace character)。在上例中,使用者可以在第一個詞 Bob 前,或者在 Bob 與 27 之間,放置任意多個空格、製表符或換行符,這些操作均不影響結果。

針對給定的轉換說明,當 scanf()讀到任何空白字元時,或者任何無法以該轉換說明解釋的字元時,讀取序列字元的操作將會終止。無法被解釋的字元會被放回到輸入流中,下一個轉換說明將從該字元開始。在前述例子中,假設使用者輸入如下:
Bob 27yearsn

在讀取到字元 y 時,它不可能是十進位制數值的一部分,針對轉換說明 %d,scanf()會停止讀取對應的字元。在呼叫該函數後,字元 yearsn 會繼續留在輸入流的緩衝區中。

如果在忽略所有空白符之後,scanf()還是找不到符合當前轉換說明的字元,則生成錯誤,scanf()函數終止處理輸入。下面將介紹如何捕獲這類錯誤。

通常,在呼叫 scanf()函數時,格式化字串只包含轉換說明。如果不是,那麼格式化字串中除轉換說明與空白符以外的其他所有字元,必須與輸入源對應位置的字元完全一致。否則 scanf()函數就會終止處理,並將不匹配的字元放回到輸入流中。

格式化字串中所出現的一個或多個連續空白符,必須符合輸入流中連續空格的數量。換句話說,對於格式化字串中出現的所有空白符,scanf()會讀取並略過資料來源中的所有空白字元,直到讀入第一個非空白符。在理解這一點後,請判斷下面的 scanf()呼叫方式有什麼問題。
scanf( "%s%dn", name, &age );    // 有什麼問題

假設使用者輸入下面這一行字元:
Bob 27n

本例中,scanf()在讀入換行符後不會返回,而是繼續讀取更多輸入,直到出現非空白字元出現。

有時候,需要讀取並略過符合給定轉換說明的字元序列,不儲存結果。可以在轉換說明中採用 %* 來達到前述效果。對於具有星號的轉換說明,不要包括對應的指標引數。

scanf()函數的返回值是成功儲存資料項的數量。如果一切執行順利,返回值就是轉換說明的數量(但不計包含星號的轉換說明)。如果發生讀取錯誤或在轉換資料項前就到達了輸入源尾部,則 scanf()函數會返回值 EOF。如下例所示:
if ( scanf( "%s%d", name, &age ) < 2 )
  fprintf( stderr, "Bad input.n" );
else
{ /* ...測試儲存的值... */ }

欄位寬度

欄位寬度是十進位制整型正數,它指定了對於給定的轉換說明,scanf()所讀取字元的最大數量。對於字串輸入來說,欄位寬度可以防止緩衝區出現溢位情況:
char city[32];
printf( "Your city: ");
if ( scanf( "%31s", city ) < 1 )     // 不要讀入超過31個字元
  fprintf( stderr, "Error reading from standard input. n" );
else
/* ... */

printf()會輸出超過指定欄位寬度的字元,但 scanf()不同於 printf(),轉換修飾符 s 不會讀入超過指定欄位寬度的字元到緩衝區。

讀取字元和字串

轉換說明 %c 和 %1c 都會從輸入流中讀取一個字元,包括空白符。通過指定欄位寬度,可以讀取數量等於欄位寬度的字元,包括空白符,只要沒有遇到輸入流的結束。當採用這種方式讀取多個字元時,對應的指標引數必須指向一個空間足夠大的 char 陣列,以儲存下所有讀到的字元。

使用轉換修飾符 c 的 scanf()函數,不會在讀入字元序列的尾部加上字串終止符。例如:
scanf( "%*5c" );
該 scanf()呼叫會讀取並丟棄輸入源緊接著的 5 個字元。

轉換說明 %s 總是讀取恰好一個詞,遇到空白符時結束讀取。如果想讀取整行文字,可以使用函數 fgets()。

下面的範例逐詞地讀取文字檔案的內容。假設檔案指標 fp 關聯了一個文字檔案,並且該檔案已開啟,以用於讀取:
char word[128];
while ( fscanf( fp, "%127s", word ) == 1 )
{
   /* ...處理讀到的詞... */
}

除了轉換修飾符 s 以外,也可以使用“掃描集”(scanset)修飾符來讀取字串,它由方括號所包含的一串無序字元組成([scanset])。scanf()函數接著讀取所有字元,然後將它們儲存為一個字串(帶有字串終止符),直到遇到不匹配掃描集中任一字元時才停止。例如:
char strNumber[32];
scanf( "%[0123456789]", strNumber );

如果使用者輸入 345X67,那麼 scanf()會把 345 字串儲存到陣列 strNumber 中。字元 X 以及後續字元則仍然留在輸入緩衝區中。

逆向使用轉換掃描集(也就是說,除掃描集中的字元外,其他都符合),做法是在掃描集的左括號後面加上一個插入號(^)。下面的 scanf()呼叫讀取所有字元(包括空白符),直到句子結束的標點符號,然後再讀入標點符號本身:
char ch, sentence[512];
scanf( "%511[^.!?]%c", sentence, &ch );

下面的 scanf()呼叫讀取並丟棄所有字元,一直到當前行結束:
scanf( "%*[^n]%*c" );

讀取整數

類似 printf()函數,scanf()函數為整數提供了下面的轉換修飾符:d、i、u、o、x 和 X。它們允許讀入十進位制、八進位與十六進位制表示法,並轉換為 int 或 unsigned int 變數。如下例所示:
// 讀入一個非負的十進位制整數
unsigned int value = 0;
if ( scanf( "%u", &value ) < 1 )
  fprintf( stderr, "Unable to read an integer.n" );
else
  /* ... */

對於 scanf()函數內的修飾符 i,讀入數位的基數(進位制)並非預先定義好的。基數是由讀入的數位字元序列的字首符號所決定的,這些符號的表示方式與 C 原始碼中整數常數相同。

如果字元序列不是以 0 開始,那麼它會被解釋為十進位制數位。如果以 0 開始,並且第二個字元不是 x 或 X,那麼該序列會被解釋為八進位數位。如果以 0x 或 0X 開始,則以十六進位制數位讀入。

如果想把所讀取的整數賦值給一個 short、char、long 或 long long 變數(或者它們所對應的無符號型別),必須在轉換修飾符之前插入一個長度修飾符:h 表示 short,hh 表示 char,l 表示 long,ll 表示 long long。在下面的範例中,FILE 指標 fp 指向一個開啟用於讀取的檔案:
unsigned long position = 0;
if (fscanf( fp, "%lX", &position) < 1 )  // 讀取一個十六進位制整數
  /* ... 處理錯誤:無法讀入數位... */

讀取浮點數

當處理浮點數時,scanf()函數使用與 printf()相同的轉換修飾符:f、e、E、g 和 G。而且,C99 新增了修飾符 a 和 A。所有這些修飾符以同樣的方式解釋讀取的字元序列。可以被解釋成浮點數的字元序列,與 C 語言中的有效浮點常數是一樣的。scanf()也可以轉換整數,並將它們儲存在浮點變數中。

所有這些修飾符將數位轉換成 float 型別浮點值。如果想將它們轉換並儲存成 double 或 long double,必須插入一個長度修飾符:double 使用 l(小寫L),long double 則使用 L。如下例所示:
float x = 0.0F;
double xx = 0.0;
// 讀取兩個浮點數:將一個轉換為float,另一個轉換為double
if ( scanf( "%f %lf", &x, &xx ) < 2 )
  /* ... */

如果該 scanf()呼叫接收到的輸入序列是 12.37n,那麼會將 12.3 儲存在到 float 變數 x 中,而 7.0 儲存到 double 變數 xx 中。