printf 與 scanf


學習 C 的過程中,通常是從主控台,也就是文字模式下開始,為了與程式互動,在主控台下輸出程式執行結果,或是從主控台取得使用者的輸入資料是基本需求,在 C 中標準輸入輸出是由 stdio.h 提供,這也就是為何要在程式的一開頭總是加上:

#include <stdio.h>

將訊息輸出至主控台,稱之為標準輸出(Stand output),C 藉由 printf() 將訊息輸出至主控台,至今已經看過幾個 printf() 函式的應用了,基本上,printf() 就是將指定的文字、數值等輸出至螢幕上,並且執行過後會傳回所輸出的字元數,例如:

#include <stdio.h>

int main(void) {
    int count = printf("This is a test!\n");
    printf("%d\n", count);

    return 0;
}

"This is a test!\n" 當中包括換行字元,共有 16 個字元,因此 count 的值會是 16,顯示結果如下:

This is a test!
16

標準輸出可以被重新導向至檔案,可以在執行程式時使用 >> 將輸出結果導向至指定檔案,例如(假設編譯後的可執行檔為 main):

main >> result.txt

如果程式的目的是顯示 "Hello! World!",則上面的執行會將結果導向至 result.txt,而不會在螢幕上顯示 "Hello! World!",result.txt中將會有輸出結果 Hello! World!。

要重新導向標準輸出是用 >,標準輸入則是 <,而 >> 除了重導標準輸出,還有附加的功能,也就是會把輸出附加到被導向的目標檔案後頭,如果目標檔案本來不存在,那麼效果就和 > 一樣。

如果在使用 printf 時要指定整數、浮點數、字元等進行顯示,要配合格式指定字(format specifier),以下列出幾個可用的格式指定碼:

  • %c:以字元方式輸出
  • %d:10 進位整數輸出
  • %o:以 8 進位整數方式輸出
  • %u:無號整數輸出
  • %x%X:將整數以 16 進位方式輸出
  • %f:浮點數輸出
  • %e%E:使用科學記號顯示浮點數
  • %g%G:浮點數輸出,取 %f%e%f%E),看哪個表示精簡
  • %%:顯示 %
  • %s:字串輸出
  • %lulong unsigned 型態的整數
  • %p:指標型態

基本上,要顯示的是什麼資料型態,就必須搭配對應資料型態的格式指定字,但 %d 若用來輸出某個字元,將顯示其整數編碼值,若 %c 用來顯示某個整數,將顯示該整數對應編碼的字元,一個使用的範例如下所示:

#include <stdio.h>

int main(void) {
    printf("顯示字元 %c\n", 'A');
    printf("顯示字元編碼 %d\n", 'A');
    printf("顯示字元編碼 %c\n", 65);    
    printf("顯示十進位整數 %d\n", 15);
    printf("顯示八進位整數 %o\n", 15);
    printf("顯示十六進位整數 %X\n", 15);
    printf("顯示十六進位整數 %x\n", 15);    
    printf("顯示科學記號 %E\n", 0.001234);    
    printf("顯示科學記號 %e\n", 0.001234);    

    return 0;
}

顯示結果如下所示:

顯示字元 A
顯示字元編碼 65
顯示字元編碼 A
顯示十進位整數 15
顯示八進位整數 17
顯示十六進位整數 F
顯示十六進位整數 f
顯示科學記號 1.234000E-03
顯示科學記號 1.234000e-03

可以在輸出浮點數時指定精度,例如若為浮點數:

printf("example:%.2f\n", 19.234);

.2 指定小數點後取兩位,執行結果會輸出:

example:19.23

也可以指定輸出時,至少要預留的字元寬度,無論是數值或字串,例如:

printf("example:%6.2f\n", 19.234);

整數 6 表示預留 6 個字元寬度,由於預留了 6 個字元寬度,不足的部份要由空白字元補上,執行結果會輸出如下(19.23只佔五個字元,所以補上一個空白在前端):

example: 19.23

若在 % 之後指定負號,例如 %-6.2f,表示靠左對齊,沒有指定則靠右對齊,例如:

#include <stdio.h>

int main(void) {
    printf("example:%6.2f\n", 19.234);
    printf("example:%-6.2f\n", 19.234);

    return 0;
}

顯示結果如下:

example: 19.23
example:19.23

若事先無法決定字元寬度,則可以使用 *,例如:

#include <stdio.h>

int main(void) {
    printf("%*d\n", 1, 1);
    printf("%*d\n", 2, 1);
    printf("%*d\n", 3, 1);

    return 0;
}

printf()* 將被之後的第一個引數所取代,所以第一個 printf() 將預留一個字元寬度,第二個預留兩個字元寬度,第三個預留三個,顯示結果如下:

1
 1
  1

若是字串的話,也可以使用 %.*s,這表示要顯示字串中 0 到多個字元,實際的字元數可以在第二個參數指定,例如:

#include <stdio.h>

int main(void) {
    printf("%.*s\n", 3, "Justin");
    printf("%.*s\n", 5, "Justin");
    printf("%.*s\n", 7, "Justin");
}

執行結果如下:

Jus
Justi
Justin

如果打算取得使用者的輸入,可以使用標準輸入的 scanf 函式,並搭配格式指定字與 & 取址運算子指定給變數,例如:

#include <stdio.h>

int main(void) {
    int input;

    printf("請輸入數字:");
    scanf("%d", &input);

    printf("你輸入的數字:%d\n", input);

    return 0;
}

在程式中先宣告了一個整數變數 input,使用 scanf() 函式時,若輸入的數值為整數,則使用格式指定字 %d,若輸入的是其他資料型態,則必須使用對應的格式指定字,如果是 double,特別注意要使用 %lf 來指定。

你必須告知程式儲存資料的變數位址,為此,必須使用 & 取址運算子,這會將變數的記憶體位址取出,則輸入的數值就知道變數的記憶體位址並儲存之(但字元陣列名稱本身就有位址資訊,故不用 & 來取址,之後說明陣列時會再看到)。

執行結果:

請輸入數字:10
你輸入的數字:10

scanf 在接受輸入時,可以接受多個值,也可以指定輸入的格式,例如:

#include <stdio.h>

int main(void) {
    int number1, number2;

    printf("請輸入兩個數字,中間使用空白區隔):");
    scanf("%d %d", &number1, &number2);
    printf("你輸入的數字:%d %d\n", number1, number2);

    printf("請再輸入兩個數字,中間使用-號區隔):");
    scanf("%d-%d", &number1, &number2);
    printf("你輸入的數字:%d-%d\n", number1, number2);

    return 0;
}

在第一個 scanf 中,指定了使用空白來區隔兩個輸入,而第二個 scanf 中,指定了使用 - 來區隔兩個輸入,一個執行與輸入的結果如下所示:

請輸入兩個數字,中間使用空白區隔):10 20
你輸入的數字:10 20
請再輸入兩個數字,中間使用-號區隔):30-40
你輸入的數字:30-40

scanf 還可以指定可接受的字元集合,例如若只想接受 1 到 5 的字元,則可以如下:

#include <stdio.h>

int main(void) {
    char buf[50];

    printf("請輸入 1 到 5 的字元:");
    scanf("%[1-5]", buf);
    printf("輸入的字元為 %s\n", buf);

    fflush(stdin); // 清除輸入緩衝區

    printf("請輸入 XYZ 任一字元:");
    scanf("%[XYZ]", buf);
    printf("輸入的字元為 %s\n", buf);

    return 0;
}

上面的 str 宣告,為 C 語言中的字元陣列與字串scanf 函式連續讀入符合集合的字元並放到字元陣列中,直到讀到不符合的字元為止,剩下的字元仍會存在輸入緩衝區中,可以直接使用 fflush(stdin) 清除輸入緩衝區,以利進行下一次重新輸入,scanf 函式若成功,則傳回成功填寫的格式指定字數目,否則直接略過輸入而傳回 0,在學會流程控制語法之後,可以據以判斷該作什麼處理(像是使用 if 來針對不接受輸入時的處理)。

執行結果如下:

請輸入 1 到 5 的字元:146731
輸入的字元為 14
請輸入 XYZ 任一字元:XYZXDX
輸入的字元為 XYZX

你可以使用 %[0-9] 指定取得 0 至 9 的字元,使用 %[A-z] 指定取得 ASCII 表中的 A 到 z 的字元,如果要排除的話,則使用 ^,例如 %[^ABC] 可取得 ABC 字元以外的所有字元。

在上面的例子中,buf 的長度為 50,輸入的字元最多只能是 49 個(在〈字元陣列與字串〉中會看到,字串是字元陣列,而且最後一個元素必須是空字元),scanf 收到的字元若超出 buf,稱為緩衝區溢位(buffer overflow),會發生不可預期的結果,甚至成為安全弱點。

預防的方法之一是,限定 scanf 每次執行可以接受的最大字元數,例如:

#include <stdio.h>

int main(void) {
    char buf[10];

    printf("請輸入字串:");
    scanf("%9s", buf);
    printf("輸入的字串:%s\n", buf);

    return 0;
}

在上例中,若輸入的字元超過 9,buf 也只會收到 9 個字元加上一個空字元,超過的字元會留在輸入緩衝區。

另一個預防輸入超過 buf 長度的方式,是使用 fgets,這可以參考〈putchar、getchar、puts、fgets〉的說明。