深度理解scanf

在C裡面,scanf 負責程式的輸入,用更精確的描述則是「以選定的格式讀取輸入並以選定的型態儲存」

沒錯,「以選定的格式」意味著 scanf 可以讓我們「自訂」輸入的格式,是個非常強大的功能,尤其是在出題者毫不體諒解題者(可憐的我們QQ)的時候 [雖然通常都是故意的 \(゚⊿゚)/ ]

光是口頭說明無法證明它為何如此強大,讓我們舉個例子吧~

假設你現在要把黏在一起的5個正8位數(也就是一個40位數)分開輸出,如果直接用 %d 當 int 讀取的話肯定會 overflow (超過 int 的範圍限制, 2^32),所以會需要寫個幾行的程式將 這些數字 用陣列儲存,再依序將 5段 char 用函式(或手動)轉換成數字

但當你精通 scanf 的時候,只需要短短2行就能解決

scanf("%8d%8d%8d%8d%8d", &a ,&b, &c, &d, &e);
/* A Piece Of Cake _(:3」ㄥ)_ */

到底 scanf 怎麼達成這些效果呢?就讓我們開始講解啦!

[ scanf ]

scanf 是內建在 <stdio.h> (standard input/output, 標準輸入輸出) 函式庫裡的函式,讀取輸入流的資料並依照 “格式(format)” 來存放至 “額外參數 ( . . . )” 所指定的位址,並回傳「成功讀取的額外參數個數」、若抵達資料末(end-of-file) 則會回傳 EOF [註1]

寫成函式的話長這樣 :

// 這是一個 scanf 函式
int scanf(const char *format, ... ){
    /* content */
}

平常我們習慣看到的則是這樣

scanf("%c%c%c", &a, &b, &c);

沒錯,scanf 主要分為2個部份,第一個是 format 也就是 scanf 前面用 “” 括起來的字串,另一個部份則是第一個 ‘ , ‘ 後面指定儲存位址(變數)的額外參數

這些參數的數量由前面格式分類符,也就是 “%什麼什麼” 來決定,有幾個 “%什麼什麼” 就要有幾個額外參數 (p.s. 這個格式分類符有自己固定的格式,在稍後會做詳細解說)


大綱介紹完那就換細部了,參數 format 是一表示資料儲存格式的字串,而 format 字串會依照3種情況做不同的處理 :

  • 空格 : 也就是指在 “” 中的空格符(ex. tab, space, \n),在 format 中任何連續的空格符都會被視為同一空格符,並且任一空格都能接收輸入流任意數量的連續空格(也可為”沒有空格”)

欸逗… 好像有點抽象齁 那舉幾個例子吧?

[scanf blank example A]
/* printf("%c%c%c\n", a, b, c) should be "abc" */
scanf("%c  %c %c", &a, &b, &c);

/* this is a valid input */
a b c
/* still valid, space between %c's stands for ? blank */
a
    b

c
/* valid either, space between %c's stands for zero blank */
abc
[scanf blank example B]
/* printf("%c%c%c\n", a, b, c) should be "abc"  */
scanf("%c%c%c", &a, &b, &c);

/* this is a valid input */
abc
/* this is not valid, printf will print "a b",
   the rest of input stream will be ignored    */
/* there is no blank in "format" between a and b, thus
   the 'space' between input a and b will be read as char */
a b c

還覺得有些疑惑的話,不仿自己操作看看,就能理解其中差異了 \(゚∀。)/


  • 非空格/格式分類符字元 : 就是除了 “%什麼什麼” 跟 空格符 以外的全部東西,每當被讀取就會與輸入流的下個字元做比對,若一樣則會互相抵銷,繼續下一個比對 ; 若不一樣則 scanf 會自動跳出,吃到的參數不會被還原、還沒吃到的參數則會為維持沒有被讀取的狀態

簡單來說,就是抄筆記不用橡皮擦直接塗掉的概念,抄對了就繼續、抄錯就把錯的部份塗掉不管,把還沒抄完的部份留到下一行 [註2]

補充個,如果想要在比對單純的 ‘%’ 而不會被當做是格式分類符的話,打 ‘%%’ 就行了

這邊比較容易理解就不給範例囉,再給下去感覺篇幅會過長 (`3´)


接下來就是秀操作的核心啦~

  • 格式分類符(下文簡寫為 ‘%’ ) : format 中真正掌管讀取格式的幕後黑手,也就是俗稱的 “%什麼什麼”,每個 ‘%’ 都對應 format 後的一個額外參數(變數),而調整 ‘%’ 的模板能修改讀取格式

‘%’ 模板 : % [ * ] [讀取字元數] [變數位元長度] 分類符

  1. 所有 [ ] 都不是必要的,視情況才使用
  2. [ * ] : 每當 % 後有 ‘ * ‘ ,則這個 ‘%’ 吃到的東西會被無視掉,相當於上面 ‘非空格/格式分類符字元’ 的上位版本,讓讀取格式有更多的調整空間
  3. [ 讀取字元數 ] : 就是一開始範例中 %8d 的那個 8,想要吃幾格的字元就放多大的數字進去,適用剛才的抄筆記法則,少了無所謂、多了等下次
  4. [ 變數位元長度 ] : 基本概念就是怎樣的變數怎麼吃,是 long long int 就加個 ll ( %d -> %lld )、是 short int 就加個 h (%hd),這是有對照表的,但是基本常用的也沒幾個就是,需要再找就好了_(┐「ε : )_

分類符 : 因為內容比較多所以單獨拿出來講,除了 %d->int, %c->char 之類的基本款(也有對照表),還有一個特別的 scanset 方式,長這樣 %[ . . . ],只會吃 [ ] 中出現的字元、吃到沒有為止 ; 也有反過來的版本 %[^ . . . ],吃全部直到遇到 ^ 後的字元,同樣適用抄筆記法則

char str[100];
scanf("%[kfc]", str);

/* input example */
fckKK!
/*      str      */
fck
scanf("%[A-z], str"); /*[註3]*/

/* input example */
Tom60229世紀大賽狗
/*      str      */
Tom
/* classic read WHOLE line by scanf */
scanf("%[^\n]", str); /*[註4]*/

/* input example */
Shan Shia Ni Goe Yen
/*      str      */
Shan Shia Ni Goe Yen

重頭戲終於講完了,剩下的就剩額外參數而已囉~

後面的額外參數就是在管理這些資料最後會被存到哪邊去,而每個額外參數都會是一個 “位址”,這也就能為大家長久以來的問題解惑啦!

/* 差別待遇阿 */
char c, str[10];
scanf("%c %s", &c , str);

因為 scanf 的參數要是位址的緣故,在上面的例子中,對 c 而言我們需要給 scanf 的是「變數的指標」而不是「變數本身」,自然要加上 取址符號 ‘&’ ; 而 str 是陣列,本身就是指標,因此不用額外加上 ‘&’ 啦!


最後的最後,不知道大家有沒有注意到,scanf 的 format 本身就是一串字串,而且還附帶著我們最熟悉對味的 “” 呢?

這個熟悉的味道…難道是?! \ (∀゚ )人(゚∀゚)人( ゚∀)人(∀゚ ) /
/* 好像在哪裡見過呢? */
char hello[20] = "Hello World!";
scanf("%c%c%c", &a, &b, &c);

沒錯!你突破華點了盲生!( ͡° ͜ʖ ͡°)

int a, b;
char format[20];
printf("Type your format:");
scanf("%s", format);
scanf(format, &a, &b);
printf("%d %d", a, b);

/* execute */
Type your format:%3d%5d
12345678
123 45678

在程式執行中修改輸入格式這種 sao 操作是能辦到的!(雖然對解題沒啥幫助就是了. . . ? )


[註1] 同一個 scanf 如果讀到一半才遇到資料末是不會回傳 EOF 的喔!

[註2] 抄筆記定律 . . . 除了在這個網站以外還沒看過別人這麼講呢!是個新興的說法呢!σ ゚∀ ゚) ゚∀゚)σ

[註3] %[A-z] 跟 %[a-Z] 是完全不同的東西喔!而且後面那個什麼都吃不到的?因為這個幾到幾只能由小到大,也就是按照字元的 ascii碼 來做比較,A->65, Z->90, a->97, z->122,所以 %[a-Z] 是不能用噠!同理 %[0-9] 能跑、%[9-0] 則不行 (0->30, 9->39)

[註4] %[^ . . . ] 有另一種解釋,就是能將原本 %s 以空格作為分隔符的這項設計改為自訂分隔符,並且也能像 %[ . . . ] 一樣用幾到幾來分隔