你可能沒有留意到的 scanf() 用法
Tags: c
Published: 2022-07-09 08:11:18

[`scanf()`](https://en.cppreference.com/w/c/io/fscanf) 是一個大部分教材都會講, 但通常沒有講得像是 [`printf()`](https://en.cppreference.com/w/c/io/fprintf) 那麼詳細的函式, 本文嘗試介紹幾個有趣的功能, 希望能在關鍵時刻發揮效用。

## 檢查輸入字樣

`scanf()` 的工作方式就是一個字一個字掃描 (scan) 輸入的文字, 比對**格式字串**, 並且依據格式字串中的**轉換規格 (conversion specifcation)** 取出並轉換資料, 因此我們可以利用格式字串強制使用者輸入固定文字。舉例來說, 我們希望使用者輸入手機號碼時一定要以 "09" 開頭, 那麼就可以用以下的範例：

```c
#include<stdio.h>

int main() {
    char phone[11] = "";
    printf("請輸入 09 開頭的手機號碼：");
    scanf("09%s", phone);
    printf("你輸入的是 09%s", phone);
}
```

執行時如果沒有在開頭輸入 "09"：

```
請輸入 09 開頭的手機號碼：
123456789
你輸入的是 09
```

你會發現並不會取得輸入內容, 這是因為在比對格式字串開頭時就出錯。如果依照 "09" 開頭的格式輸入：

```
請輸入 09 開頭的手機號碼：
0912123456
你輸入的是 0912123456
```

就可以取得正確的輸入。

你還可以進一步使用 `scanf()` 的傳回值檢查是否發生比對錯誤的狀況。`scanf()` 的傳回值是依據轉換規格成功轉換的資料個數, 如果像是上例中一開始比對字樣時就出錯, 傳回值就會是 0, 因此, 我們可以改寫上述範例：

```c
#include<stdio.h>

int main() {
    char phone[11] = "";
    int num;
    
    printf("請輸入 09 開頭的手機號碼：");
    num = scanf("09%s", phone);
    if(num > 0)
        printf("你輸入的是 09%s", phone);
    else
        printf("手機號碼要以 \"09\" 開頭");
}
```

再次輸入不以 "09" 開頭的號碼：

```
請輸入 09 開頭的手機號碼：
123456789
手機號碼要以 "09" 開頭
```

就會看到錯誤訊息了。

## 使用 %\[\] 轉換規格限制可輸入的文字

在剛剛的例子中, 我們雖然已經強制使用者要先輸入 "09", 但如果使用者亂輸入, 就可能得到不是數字的結果, 我們可以使用 `%[]` 轉換規格表明能夠接受的字元, 例如：

```c
#include<stdio.h>

int main() {
    char phone[11] = "";
    int num;
    
    printf("請輸入 09 開頭的手機號碼：");
    num = scanf("09%[0-9]", phone);
    if(num > 0)
        printf("你輸入的是 09%s", phone);
    else
        printf("手機號碼要以 \"09\" 開頭, 只能接受數字");
}
```

`%[0-9]` 表示要接受 '0'~'9' 之間的字元, `scanf()` 就會掃描輸入, 一直到遇到不可接受的字元為止。像是以下執行結果：

```
請輸入 09 開頭的手機號碼：
09abcdefg
手機號碼要以 "09" 開頭, 只能接受數字
```

輸入的都不是數字, 所以轉換失敗, 如果改成以下：

```
請輸入 09 開頭的手機號碼：
0912123abc
你輸入的是 0912123
```

就會看到只取到 'a' 之前的內容。如果你希望使用者在輸入電話號碼時, 也可以加上連字號, 就可以把程式改成這樣：

```C
#include<stdio.h>

int main() {
    char phone[11] = "";
    int num;
    
    printf("請輸入 09 開頭的手機號碼：");
    num = scanf("09%[0-9-]", phone);
    if(num > 0)
        printf("你輸入的是 09%s", phone);
    else
        printf("手機號碼要以 \"09\" 開頭, 只能接受數字");
}
```

`%[0-9-]` 就表示可接受 '0'~'9' 以及 '-'。執行結果如下：

```
請輸入 09 開頭的手機號碼：
0912-345-678
你輸入的是 0912-345-678
```

除了正面表列可接受的字元外, 也可以用負面表列不接受的字元, 這可以用 `^` 來表示, 這最常用在想將使用者輸入的一整行文字放入變數中的時候, 如果是用 `%s`, 遇到空格就會停止, 例如：

```c
#include<stdio.h>

int main() {
    char name[20] = "";
    int num;
    
    printf("請輸入英文全名：");
    num = scanf("%s", name);
    printf("你的名字是 %s", name);
}
```

執行結果如下：

```
請輸入英文全名：
Steve Jobs
你的名字是 Steve
```

只取到了空格前的內容。如果改用 `%[^\n] ` 負面表列不接受換行字元, 其他字元都可以, 就可以取到整行文字, 例如：

```c
#include<stdio.h>

int main() {
    char name[20] = "";
    int num;
    
    printf("請輸入英文全名：");
    num = scanf("%[^\n]", name);
    printf("你的名字是 %s", name);
}
```

取得的就是完整的資料了：

```
請輸入英文全名：
Steve Jobs
你的名字是 Steve Jobs
```

由於會一直掃描, 直到遇到了換行字元為止, 所以整行文字都取到了。

## 限制轉換時取用的字數

前面輸入電話號碼的範例潛藏了一個問題, 假設我們希望這個程式可以有多國語言版, 因此先把程式改寫成這樣, 將文字訊息都先抽離成單一變數, 方便修改成不同語言的版本：

```c
#include<stdio.h>

int main() {
    char phone[11] = "";
    char msg_ok[] = "你輸入的號碼是 ";
    char msg_err[] = "手機號碼要以 \"09\" 開頭, 只能接受數字";
    char msg_prompt[] = "請輸入 09 開頭的手機號碼：";
    
    int num;
    
    printf("%s", msg_prompt);
    num = scanf("09%[0-9-]", phone);
    if(num > 0)
        printf("%s09%s", msg_ok, phone);
    else
        printf("%s", msg_err);
}
```

在執行時使用者不小心輸入了過長的電話號碼, 像是這樣：

```
請輸入 09 開頭的手機號碼：
0912-123-456-789
7890912-123-456-789
```

你會發現顯示的訊息開頭怎麼怪怪的？本來應該是 "你輸入的號碼是...", 但怎麼變成 "789...."？這是因為 `phone` 只宣告了 11 個字元, 但是在轉換資料時超過了, 因此資料覆蓋到後續的其他訊息了。

要解決這個問題, 我們可以限制實際轉換資料時取用的字數, 像是這樣：

```c
#include<stdio.h>

int main() {
    char phone[11] = "";
    char msg_ok[] = "你輸入的號碼是 ";
    char msg_err[] = "手機號碼要以 \"09\" 開頭, 只能接受數字";
    char msg_prompt[] = "請輸入 09 開頭的手機號碼：";
    
    int num;
    
    printf("%s", msg_prompt);
    num = scanf("09%10[0-9-]", phone);
    if(num > 0)
        printf("%s09%s", msg_ok, phone);
    else
        printf("%s", msg_err);
}
```

`%10[0-9]` 的 10 表示最多就是 10 個字元 (`scanf()` 會幫你加上結尾的 '\0', 因此最多字元數是陣列大小再減 1), 這樣不管使用者輸入多長的字元, 都只會取前 10 個字元：

```
請輸入 09 開頭的手機號碼：
0912-123-456-789-123-456
你輸入的號碼是 0912-123-456
```

可以看到即使輸入很長一串, 也不會出錯了。

理論上, 在使用 `%[]` 或是 `%s` 時, 都應該要加上長度限制, 避免出現使用者輸入過長文字導致覆蓋到超過陣列邊界記憶體的問題發生。

## 自動消耗空白字元

除了 `%c`、`%[]`、`%n` 以外, 其餘的轉換規格都會先略過開頭的空白字元, 這包含 '\t'、'\n' 在內。以底下的程式為例：

```c
#include<stdio.h>

int main() {
    int num1, num2;
    
    printf("請輸入兩個數值");
    scanf("%d%d", &num1, &num2);
    printf("你輸入了 %d, %d", num1, num2);
}
```

雖然在 `scanf()` 中的格式字串內兩個 `%d` 轉換格式是緊接在一起, 但實際輸入時, 你可以在兩個數值之間加入任意個數的空白, 像是這樣：

```
請輸入兩個數值
10        20
你輸入了 10, 20
```

因為第 2 個 `%d` 會略過之間的空白, 從第一個非空白字元開始轉換整數。

由於空白字元包含 '\n' 在內, 因此即使輸入時兩個數值個別一行也沒有問題：

```
請輸入兩個數值
10
20
你輸入了 10, 20
```

## 小結

本文介紹了幾個一般人學習時可能沒有注意到的 `scanf()` 使用方式, 希望能幫助大家善用這些看起來好像很簡單的標準函式。