1. 程式人生 > 其它 >C 語言中的 printf() 和 scanf() 簡介

C 語言中的 printf() 和 scanf() 簡介

目錄

printf() 函式和 scanf() 函式能讓使用者可以與程式交流,它們是輸出/輸入函式,或簡稱為 I/O 函式。它們不僅是 C 語言中的 I/O 函式,而且是最多才多藝的函式。過去,這些函式和 C 庫的一些其他函式一樣,並不是 C 語言定義的一部分。最初,C 把輸入/輸出的實現留給了編譯器的作者,這樣可以針對特殊的機器更好地匹配輸入/輸出。後來,考慮到相容性的問題,各編譯器都提供不同版本的 printf() 和 scanf()。儘管如此,各版本之間偶爾有一些差異。C90 和 C99 標準規定了這些函式的標準版本,本文亦遵循這一標準。

雖然 printf() 是輸出函式,scanf() 是輸入函式,但是它們的工作原理幾乎相同。兩個函式都使用格式字串和引數列表。我們先介紹 printf(),再介紹 scanf()。

一、printf() 函式

請求 printf() 函式列印資料的指令要與待列印資料的型別相匹配。例如,列印整數時使用 %d,列印字元時使用 %c。這些符號被稱為轉換說明(conversion specification),它們指定了如何把資料轉換成可顯示的形式。我們先列出 ANSI C 標準為 printf() 提供的轉換說明,然後再示範如何使用一些較常見的轉換說明。表 3 列出了一些轉換說明和各自對應的輸出型別。

表 3 轉換說明及其列印的輸出結果

轉換說明 輸出
%a 浮點數、十六進位制數和p記數法(C99/C11)
%A 浮點數、十六進位制數和p記數法(C99/C11)
%c 單個字元
%d 有符號十進位制整數
%e 浮點數,e記數法
%E 浮點數,e記數法
%f 浮點數,十進位制記數法
%g 根據值的不同,自動選擇%f%e%e格式用於指數小於-4或者大於或等於精度時
%G 根據值的不同,自動選擇%f%E%E格式用於指數小於-4或者大於或等於精度時
%i 有符號十進位制整數(與%d相同)
%o 無符號八進位制整數
%p 指標
%s 字串
%u 無符號十進位制整數
%x 無符號十六進位制整數,使用十六進位制數0f
%X 無符號十六進位制整數,使用十六進位制數0F
%% 列印一個百分號

二、使用 printf()

程式清單 6 的程式中使用了一些轉換說明。

程式清單 6 printout.c 程式

/* printout.c -- 使用轉換說明 */
#include <stdio.h>
#define PI 3.141593
int main(void)
{
     int number = 7;
     float pies = 12.75;
     int cost = 7800;

     printf("The %d contestants ate %f berry pies.\n", number,
               pies);
     printf("The value of pi is %f.\n", PI);
     printf("Farewell! thou art too dear for my possessing,\n");
     printf("%c%d\n", '$', 2 * cost);

     return 0;
}

該程式的輸出如下:

The 7 contestants ate 12.750000 berry pies.
The value of pi is 3.141593.
Farewell! thou art too dear for my possessing,
$15600

這是 printf() 函式的格式:

printf( 格式字串, 待列印項1, 待列印項2,...);

待列印項 1、待列印項 2 等都是要列印的項。它們可以是變數、常量,甚至是在列印之前先要計算的表示式。格式字串應包含每個待列印項對應的轉換說明。例如,考慮下面的語句:

printf("The %d contestants ate %f berry pies.\n", number,pies);

格式字串是雙引號括起來的內容。上面語句的格式字串包含了兩個待列印項 number 和 pies 對應的兩個轉換說明。圖 6 演示了 printf() 語句的另一個例子。

圖 6 printf()的引數

下面是程式清單 6 中的另一行:

printf("The value of pi is %f.\n", PI);

該語句中,待列印項列表只有一個項——符號常量 PI。

如圖 7 所示,格式字串包含兩種形式不同的資訊:

  • 實際要列印的字元;
  • 轉換說明。

圖 7 剖析格式字串

警告

格式字串中的轉換說明一定要與後面的每個項相匹配,若忘記這個基本要求會導致嚴重的後果。千萬別寫成下面這樣:

printf("The score was Squids %d, Slugs %d.\n", score1);

這裡,第 2 個 %d 沒有對應任何項。系統不同,導致的結果也不同。不過,出現這種問題最好的狀況是得到無意義的值。

如果只打印短語或句子,就不需要使用任何轉換說明。如果只打印資料,也不用加入說明文字。程式清單 6 中的最後兩個 printf() 語句都沒問題:

printf("Farewell! thou art too dear for my possessing,\n");
printf("%c%d\n", '$', 2 * cost);

注意第 2 條語句,待列印列表的第 1 個項是一個字元常量,不是變數;第 2 個項是一個乘法表達式。這說明 printf() 使用的是值,無論是變數、常量還是表示式的值。

由於 printf() 函式使用%符號來標識轉換說明,因此列印 % 符號就成了個問題。如果單獨使用一個 % 符號,編譯器會認為漏掉了一個轉換字元。解決方法很簡單,使用兩個 % 符號就行了:

pc = 2*6;
printf("Only %d%% of Sally's gribbles were edible.\n", pc);

下面是輸出結果:

Only 12% of Sally's gribbles were edible.

三、printf() 的轉換說明修飾符

% 和轉換字元之間插入修飾符可修飾基本的轉換說明。表 4 和表 5 列出可作為修飾符的合法字元。如果要插入多個字元,其書寫順序應該與表 4 中列出的順序相同。不是所有的組合都可行。表中有些字元是 C99 新增的,如果編譯器不支援 C99,則可能不支援表中的所有項。

修飾符 含義
標記 表4.5描述了5種標記(-+、空格、#0),可以不使用標記或使用多個標記
示例:"%-10d"
數字 最小欄位寬度
如果該欄位不能容納待列印的數字或字串,系統會使用更寬的欄位
示例:"%4d"
.數字 精度
對於%e%E%f轉換,表示小數點右邊數字的位數
對於%g%G轉換,表示有效數字最大位數
對於%s轉換,表示待列印字元的最大數量
對於整型轉換,表示待列印數字的最小位數
如有必要,使用前導0來達到這個位數
只使用.表示其後跟隨一個0,所以%.f%.0f相同
示例:"%5.2f"列印一個浮點數,欄位寬度為5字元,其中小數點後有兩位數字
h 和整型轉換說明一起使用,表示short intunsigned short int型別的值
示例:"%hu""%hx""%6.4hd"
hh 和整型轉換說明一起使用,表示signed charunsigned char型別的值
示例:"%hhu""%hhx""%6.4hhd"
j 和整型轉換說明一起使用,表示intmax_tuintmax_t型別的值。這些型別定義在stdint.h
示例:"%jd""%8jx"
l 和整型轉換說明一起使用,表示long intunsigned long int型別的值
示例:"%ld""%8lu"
ll 和整型轉換說明一起使用,表示long long intunsigned long long int型別的值(C99)
示例:"%lld""%8llu"
L 和浮點轉換說明一起使用,表示long double型別的值
示例:"%Lf""%10.4Le"
t 和整型轉換說明一起使用,表示ptrdiff_t型別的值。ptrdiff_t是兩個指標差值的型別(C99)
示例:"%td""%12ti"
z 和整型轉換說明一起使用,表示size_t型別的值。size_tsizeof返回的型別(C99)
示例:"%zd""%12zd"

注意 型別可移植性

sizeof 運算子以位元組為單位返回型別或值的大小。這應該是某種形式的整數,但是標準只規定了該值是無符號整數。在不同的實現中,它可以是 unsigned int、unsigned long 甚至是 unsigned long long。因此,如果要用 printf() 函式顯示 sizeof 表示式,根據不同系統,可能使用 %u%lu%llu。這意味著要查詢你當前系統的用法,如果把程式移植到不同的系統還要進行修改。鑑於此,C 提供了可移植性更好的型別。首先,stddef.h 標頭檔案(在包含 stdio.h 標頭檔案時已包含其中)把 size_t 定義成系統使用 sizeof 返回的型別,這被稱為底層型別(underlying type)。其次,printf() 使用 z 修飾符表示列印相應的型別。同樣,C 還定義了 ptrdiff_t 型別和 t 修飾符來表示系統使用的兩個地址差值的底層有符號整數型別。

注意 float引數的轉換

對於浮點型別,有用於 double 和 long double 型別的轉換說明,卻沒有 float 型別的轉換說明。這是因為在 K&R C 中,表示式或引數中的 float 型別值會被自動轉換成 double 型別。一般而言, ANSI C 不會把 float 自動轉換成 double 。然而,有大量的現有程式都假設 float 型別的引數被自動轉換成 double 型別,為了保護這些程式, printf() 函式中所有 float 型別的引數(對未使用顯式原型的所有 C 函式都有效)仍自動轉換成 double 型別。因此,無論是 K&R C 還是 ANSI C ,都沒有顯示 float 型別值專用的轉換說明。

表 5 printf() 中的標記

標記 含義
- 待列印項左對齊。即,從欄位的左側開始列印該項
示例:"%-20s"
+ 有符號值若為正,則在值前面顯示加號;若為負,則在值前面顯示減號
示例:"%+6.2f"
空格 有符號值若為正,則在值前面顯示前導空格(不顯示任何符號);若為負,則在值前面顯示減號+標記並覆蓋空格
示例:"%6.2f"
# 把結果轉換為另一種形式。如果是%o格式,則以0開始;如果是%x%X格式,則以0x0X開始;對於所有的浮點格式,#保證了即使後面沒有任何數字,也列印一個小數點字元。對於%g%G格式,#防止結果後面的0被刪除
示例:"%#o""%#8.0f""%+#10.3e"
0 對於數值格式,用前導0代替空格填充欄位寬度。對於整數格式,如果出現-標記或指定精度,則忽略該標記
示例:"%010d""%08.3f"

3.1 使用修飾符和標記的示例

接下來,用程式示例演示如何使用這些修飾符和標記。先來看看欄位寬度在列印整數時的效果。考慮程式清單 7 中的程式。

程式清單 7 width.c 程式

/* width.c -- 欄位寬度 */
#include <stdio.h>
#define PAGES 959
int main(void)
{
     printf("*%d*\n", PAGES);
     printf("*%2d*\n", PAGES);
     printf("*%10d*\n", PAGES);
     printf("*%-10d*\n", PAGES);

     return 0;
}

程式清單 7 通過 4 種不同的轉換說明把相同的值列印了 4 次。程式中使用星號(*)標出每個欄位的開始和結束。其輸出結果如下所示:

*959*
*959*
*       959*
*959       *

第 1 個轉換說明 %d 不帶任何修飾符,其對應的輸出結果與帶整數字段寬度的轉換說明的輸出結果相同。在預設情況下,沒有任何修飾符的轉換說明,就是這樣的列印結果。第 2 個轉換說明是 %2d,其對應的輸出結果應該是 2 欄位寬度。因為待列印的整數有 3 位數字,所以欄位寬度自動擴大以符合整數的長度。第 3 個轉換說明是 %10d,其對應的輸出結果有 10 個空格寬度,實際上在兩個星號之間有 7 個空格和 3 位數字,並且數字位於欄位的右側。最後一個轉換說明是 %-10d,其對應的輸出結果同樣是 10 個空格寬度,- 標記說明列印的數字位於欄位的左側。熟悉它們的用法後,我們就能很好地控制輸出格式。試著改變 PAGES 的值,看看編譯器如何列印不同位數的數字。

接下來看看浮點型格式。請輸入、編譯並執行程式清單 8 中的程式。

程式清單 8 floats.c 程式

// floats.c -- 一些浮點型修飾符的組合
#include <stdio.h>

int main(void)
{
     const double RENT = 3852.99;  // const變數

     printf("*%f*\n", RENT);
     printf("*%e*\n", RENT);
     printf("*%4.2f*\n", RENT);
     printf("*%3.1f*\n", RENT);
     printf("*%10.3f*\n", RENT);
     printf("*%10.3E*\n", RENT);
     printf("*%+4.2f*\n", RENT);
     printf("*%010.2f*\n", RENT);

     return 0;
}

該程式中使用了 const 關鍵字,限定變數為只讀。該程式的輸出如下:

*3852.990000*
*3.852990e+03*
*3852.99*
*3853.0*
*  3852.990*
* 3.853E+03*
*+3852.99*
*0003852.99*

本例的第 1 個轉換說明是 %f。在這種情況下,欄位寬度和小數點後面的位數均為系統預設設定,即欄位寬度是容納待列印數字所需的位數和小數點後列印 6 位數字。

第 2 個轉換說明是 %e。預設情況下,編譯器在小數點的左側列印 1 個數字,在小數點的右側列印 6 個數字。這樣列印的數字太多!解決方案是指定小數點右側顯示的位數,程式中接下來的 4 個例子就是這樣做的。請注意,第 4 個和第 6 個例子對輸出結果進行了四捨五入。另外,第 6 個例子用 E 代替了 e。

第 7 個轉換說明中包含了 + 標記,這使得列印的值前面多了一個代數符號(+)。0 標記使得列印的值前面以 0 填充以滿足欄位要求。注意,轉換說明 %010.2f 的第 1 個 0 是標記,句點(.)之前、標記之後的數字(本例為 10)是指定的欄位寬度。

嘗試修改 RENT 的值,看看編譯器如何列印不同大小的值。程式清單 9 演示了其他組合。

程式清單 9 flags.c 程式

/* flags.c -- 演示一些格式標記 */
#include <stdio.h>
int main(void)
{
     printf("%x %X %#x\n", 31, 31, 31);
     printf("**%d**% d**% d**\n", 42, 42, -42);
     printf("**%5d**%5.3d**%05d**%05.3d**\n", 6, 6, 6, 6);

     return 0;
}

該程式的輸出如下:

1f 1F 0x1f
**42** 42**-42**
**    6**  006**00006**  006**

第 1 行輸出中,1f 是十六進位制數,等於十進位制數 31。第 1 行 printf() 語句中,根據 %x 打印出 1f,%X 打印出 1F,%#x 打印出 0x1f

第 2 行輸出演示瞭如何在轉換說明中用空格在輸出的正值前面生成前導空格,負值前面不產生前導空格。這樣的輸出結果比較美觀,因為打印出來的正值和負值在相同欄位寬度下的有效數字位數相同。

第 3 行輸出演示瞭如何在整型格式中使用精度(%5.3d)生成足夠的前導 0 以滿足最小位數的要求(本例是 3)。然而,使用 0 標記會使得編譯器用前導 0 填充滿整個欄位寬度。最後,如果 0 標記和精度一起出現,0 標記會被忽略。

下面來看看字串格式的示例。考慮程式清單 10 中的程式。

程式清單 10 stringf.c 程式

/* stringf.c -- 字串格式 */
#include <stdio.h>
#define BLURB "Authentic imitation!"
int main(void)
{
     printf("[%2s]\n", BLURB);
     printf("[%24s]\n", BLURB);
     printf("[%24.5s]\n", BLURB);
     printf("[%-24.5s]\n", BLURB);

     return 0;
}

該程式的輸出如下:

[Authentic imitation!]
[     Authentic imitation!]
[                    Authe]
[Authe                    ]

注意,雖然第 1 個轉換說明是 %2s,但是欄位被擴大為可容納字串中的所有字元。還需注意,精度限制了待列印字元的個數。.5 告訴 printf() 只打印 5 個字元。另外,- 標記使得文字左對齊輸出。

3.2 學以致用

學習完以上幾個示例,試試如何用一個語句列印以下格式的內容:

The NAME family just may be $XXX.XX dollars richer!

這裡,NAME 和 XXX.XX 代表程式中變數(如 name[40] 和 cash)的值。可參考以下程式碼:

printf("The %s family just may be $%.2f dollars richer!\n",name,cash);

四、轉換說明的意義

下面深入探討一下轉換說明的意義。轉換說明把以二進位制格式儲存在計算機中的值轉換成一系列字元(字串)以便於顯示。例如,數字 76 在計算機內部的儲存格式是二進位制數 01001100。%d 轉換說明將其轉換成字元 7 和 6,並顯示為 76;%x 轉換說明把相同的值(01001100)轉換成十六進位制記數法 4c;%c 轉換說明把 01001100 轉換成字元 L。

轉換(conversion)可能會誤導讀者認為原始值被替換成轉換後的值。實際上,轉換說明是翻譯說明,%d 的意思是“把給定的值翻譯成十進位制整數文字並打印出來”。

4.1 轉換不匹配

前面強調過,轉換說明應該與待列印值的型別相匹配。通常都有多種選擇。例如,如果要列印一個 int 型別的值,可以使用 %d%x%o。這些轉換說明都可用於列印 int 型別的值,其區別在於它們分別表示一個值的形式不同。類似地,列印 double 型別的值時,可使用 %f%e%g

轉換說明與待列印值的型別不匹配會怎樣?匹配非常重要,一定要牢記於心。程式清單 11 演示了一些不匹配的整型轉換示例。

程式清單 11 intconv.c 程式

/* intconv.c -- 一些不匹配的整型轉換 */
#include <stdio.h>
#define PAGES 336
#define WORDS 65618
int main(void)
{
     short num = PAGES;
     short mnum = -PAGES;

     printf("num as short and unsigned short:  %hd %hu\n", num,num);
     printf("-num as short and unsigned short: %hd %hu\n", mnum,mnum);
     printf("num as int and char: %d %c\n", num, num);
     printf("WORDS as int, short, and char: %d %hd %c\n",WORDS,WORDS, WORDS);

     return 0;
}

在我們的系統中,該程式的輸出如下:

num as short and unsigned short: 336 336
-num as short and unsigned short: -336 65200
num as int and char: 336 P
WORDS as int, short, and char: 65618 82 R

請看輸出的第 1 行,num 變數對應的轉換說明 %hd%hu 輸出的結果都是 336。這沒有任何問題。然而,第 2 行 mnum 變數對應的轉換說明 %u(無符號)輸出的結果卻為 65200,並非期望的 336。這是由於有符號 short int 型別的值在我們的參考系統中的表示方式所致。首先,short int 的大小是 2 位元組;其次,系統使用二進位制補碼來表示有符號整數。這種方法,數字 0~32767 代表它們本身,而數字 32768~65535 則表示負數。其中,65535 表示 -1,65534 表示 -2,以此類推。因此,-336 表示為 65200(即,65536-336)。所以被解釋成有符號 int 時,65200 代表 -336;而被解釋成無符號 int 時,65200 則代表 65200。一定要謹慎!一個數字可以被解釋成兩個不同的值。儘管並非所有的系統都使用這種方法來表示負整數,但要注意一點:別期望用 %u 轉換說明能把數字和符號分開。

第 3 行演示瞭如果把一個大於 255 的值轉換成字元會發生什麼情況。在我們的系統中,short int 是 2 位元組,char 是 1 位元組。當 printf() 使用 %c 列印 336 時,它只會檢視儲存 336 的 2 位元組中的後 1 位元組。這種截斷(見圖 8)相當於用一個整數除以 256,只保留其餘數。在這種情況下,餘數是 80,對應的 ASCII 值是字元 P。用專業術語來說,該數字被解釋成“以 256 為模”(modulo 256),即該數字除以 256 後取其餘數。

圖 8 把 336 轉換成字元

最後,我們在該系統中列印比 short int 型別最大整數(32767)更大的整數(65618)。這次,計算機也進行了求模運算。在本系統中,應把數字 65618 儲存為 4 位元組的 int 型別值。用 %hd 轉換說明列印時,printf() 只使用最後 2 個位元組。這相當於 65618 除以 65536 的餘數。這裡,餘數是 82。鑑於負數的儲存方法,如果餘數在 32767~65536 範圍內會被列印成負數。對於整數大小不同的系統,相應的處理行為類似,但是產生的值可能不同。

混淆整型和浮點型,結果更奇怪。考慮程式清單 12。

程式清單 12 floatcnv.c 程式

/* floatcnv.c -- 不匹配的浮點型轉換 */
#include <stdio.h>
int main(void)
{
     float n1 = 3.0;
     double n2 = 3.0;
     long n3 = 2000000000;
     long n4 = 1234567890;

     printf("%.1e %.1e %.1e %.1e\n", n1, n2, n3, n4);
     printf("%ld %ld\n", n3, n4);
     printf("%ld %ld %ld %ld\n", n1, n2, n3, n4);

     return 0;
}

在我們的系統中,該程式的輸出如下:

3.0e+00 3.0e+00 3.1e+46 1.7e+266
2000000000 1234567890
0 1074266112 0 1074266112

第 1 行輸出顯示,%e 轉換說明沒有把整數轉換成浮點數。考慮一下,如果使用 %e 轉換說明列印 n3(long 型別)會發生什麼情況。首先,%e 轉換說明讓 printf() 函式認為待列印的值是 double 型別(本系統中 double 為 8 位元組)。當 printf() 檢視 n3(本系統中是 4 位元組的值)時,除了檢視 n3 的 4 位元組外,還會檢視檢視 n3 相鄰的 4 位元組,共 8 位元組單元。接著,它將 8 位元組單元中的位組合解釋成浮點數(如,把一部分位組合解釋成指數)。因此,即使 n3 的位數正確,根據 %e 轉換說明和 %ld 轉換說明解釋出來的值也不同。最終得到的結果是無意義的值。

第 1 行也說明了前面提到的內容:float 型別的值作為 printf() 引數時會被轉換成 double 型別。在本系統中,float 是 4 位元組,但是為了 printf() 能正確地顯示該值,n1 被擴成 8 位元組。

第 2 行輸出顯示,只要使用正確的轉換說明,printf() 就可以列印 n3 和 n4。

第 3 行輸出顯示,如果 printf() 語句有其他不匹配的地方,即使用對了轉換說明也會生成虛假的結果。用 %ld 轉換說明列印浮點數會失敗,但是在這裡,用 %ld 列印 long 型別的數竟然也失敗了!問題出在 C 如何把資訊傳遞給函式。具體情況因編譯器實現而異。“引數傳遞”框中針對一個有代表性的系統進行了討論。

引數傳遞

引數傳遞機制因實現而異。下面以我們的系統為例,分析引數傳遞的原理。函式呼叫如下:

printf("%ld %ld %ld %ld\n", n1, n2, n3, n4);

該呼叫告訴計算機把變數 n1、n2、n3 和 n4 的值傳遞給程式。這是一種常見的引數傳遞方式。程式把傳入的值放入被稱為棧(stack)的記憶體區域。計算機根據變數型別(不是根據轉換說明)把這些值放入棧中。因此,n1 被儲存在棧中,佔 8 位元組(float 型別被轉換成 double 型別)。同樣,n2 也在棧中佔 8 位元組,而 n3 和 n4 在棧中分別佔 4 位元組。然後,控制轉到 printf() 函式。該函式根據轉換說明(不是根據變數型別)從棧中讀取值。%ld 轉換說明表明 printf() 應該讀取 4 位元組,所以 printf() 讀取棧中的前 4 位元組作為第 1 個值。這是 n1 的前半部分,將被解釋成一個 long 型別的整數。根據下一個 %ld 轉換說明,printf() 再讀取 4 位元組,這是 n1 的後半部分,將被解釋成第 2 個 long 型別的整數(見圖 9)。類似地,根據第 3 個和第 4 個 %ld,printf() 讀取 n2 的前半部分和後半部分,並解釋成兩個 long 型別的整數。因此,對於 n3 和 n4,雖然用對了轉換說明,但 printf() 還是讀錯了位元組。

float n1;    /* 作為double型別傳遞 */
double n2;
long n3, n4;
...
printf("%ld %ld %ld %ld\n", n1, n2, n3, n4);

圖 9 傳遞引數

4.2 printf() 的返回值

大部分 C 函式都有一個返回值,這是函式計算並返回給主調程式(calling program)的值。例如,C 庫包含一個 sqrt() 函式,接受一個數作為引數,並返回該數的平方根。可以把返回值賦給變數,也可以用於計算,還可以作為引數傳遞。總之,可以把返回值像其他值一樣使用。printf() 函式也有一個返回值,它返回列印字元的個數。如果有輸出錯誤,printf() 則返回一個負值(printf() 的舊版本會返回不同的值)。

printf() 的返回值是其列印輸出功能的附帶用途,通常很少用到,但在檢查輸出錯誤時可能會用到(如,在寫入檔案時很常用)。如果一張已滿的 CD 或 DVD 拒絕寫入,程式應該採取相應的行動,例如終端蜂鳴 30 秒。不過,要實現這種情況必須先了解 if 語句。程式清單 13 演示瞭如何確定函式的返回值。

程式清單 13 prntval.c 程式

/* prntval.c -- printf()的返回值 */
#include <stdio.h>
int main(void)
{
     int bph2o = 212;
     int rv;

     rv = printf("%d F is water's boiling point.\n", bph2o);
     printf("The printf() function printed %d characters.\n",
               rv);
     return 0;
}

該程式的輸出如下:

212 F is water's boiling point.
The printf() function printed 32 characters.

首先,程式用 rv = printf(...); 的形式把 printf() 的返回值賦給 rv。因此,該語句執行了兩項任務:列印資訊和給變數賦值。其次,注意計算針對所有字元數,包括空格和不可見的換行符(\n)。

4.3 列印較長的字串

有時,printf() 語句太長,在螢幕上不方便閱讀。如果空白(空格、製表符、換行符)僅用於分隔不同的部分,C 編譯器會忽略它們。因此,一條語句可以寫成多行,只需在不同部分之間輸入空白即可。例如,程式清單 13 中的一條 printf() 語句:

printf("The printf() function printed %d characters.\n",
          rv);

該語句在逗號和 rv 之間斷行。為了讓讀者知道該行未完,示例縮進了 rv。C 編譯器會忽略多餘的空白。

但是,不能在雙引號括起來的字串中間斷行。如果這樣寫:

printf("The printf() function printed %d
           characters.\n", rv);

C 編譯器會報錯:字串常量中有非法字元。在字串中,可以使用 \n 來表示換行字元,但是不能通過按下 Enter(或 Return)鍵產生實際的換行符。

給字串斷行有 3 種方法,如程式清單 14 所示。

程式清單 14 longstrg.c 程式

/* longstrg.c ––列印較長的字串 */
#include <stdio.h>
int main(void)
{
     printf("Here's one way to print a ");
     printf("long string.\n");
     printf("Here's another way to print a \
long string.\n");
     printf("Here's the newest way to print a "
               "long string.\n");      /* ANSI C */

     return 0;
}

該程式的輸出如下:

Here's one way to print a long string.
Here's another way to print a long string.
Here's the newest way to print a long string.

方法 1:使用多個 printf() 語句。因為第 1 個字串沒有以 \n 字元結束,所以第 2 個字串緊跟第 1 個字串末尾輸出。

方法2:用反斜槓(\)和 Enter(或 Return)鍵組合來斷行。這使得游標移至下一行,而且字串中不會包含換行符。其效果是在下一行繼續輸出。但是,下一行程式碼必須和程式清單中的程式碼一樣從最左邊開始。如果縮排該行,比如縮排 5 個空格,那麼這 5 個空格就會成為字串的一部分。

方法3:ANSI C 引入的字串連線。在兩個用雙引號括起來的字串之間用空白隔開,C 編譯器會把多個字串看作是一個字串。因此,以下3種形式是等效的:

printf("Hello, young lovers, wherever you are.");
printf("Hello, young "        "lovers" ", wherever you are.");
printf("Hello, young lovers"
        ", wherever you are.");

上述方法中,要記得在字串中包含所需的空格。如,"young""lovers" 會成為 "younglovers",而 "young " "lovers" 才是 "younglovers"

五、使用 scanf()

剛學完輸出,接下來我們轉至輸入——學習 scanf() 函式。C 庫包含了多個輸入函式,scanf() 是最通用的一個,因為它可以讀取不同格式的資料。當然,從鍵盤輸入的都是文字,因為鍵盤只能生成文字字元:字母、數字和標點符號。如果要輸入整數 2014,就要鍵入字元 2、0、1、4。如果要將其儲存為數值而不是字串,程式就必須把字元依次轉換成數值,這就是 scanf() 要做的。scanf() 把輸入的字串轉換成整數、浮點數、字元或字串,而 printf() 正好與它相反,把整數、浮點數、字元和字串轉換成顯示在螢幕上的文字。

scanf() 和printf() 類似,也使用格式字串和引數列表。scanf() 中的格式字串表明字元輸入流的目標資料型別。兩個函式主要的區別在引數列表中。printf() 函式使用變數、常量和表示式,而 scanf() 函式使用指向變數的指標。這裡,讀者不必瞭解如何使用指標,只需記住以下兩條簡單的規則:

  • 如果用 scanf() 讀取基本變數型別的值,在變數名前加上一個 &
  • 如果用 scanf() 把字串讀入字元陣列中,不要使用 &

程式清單 15 中的小程式演示了這兩條規則。

程式清單 15 input.c 程式

// input.c -- 何時使用&
#include <stdio.h>
int main(void)
{
     int age;             // 變數
     float assets;        // 變數
     char pet[30];        // 字元陣列,用於儲存字串

     printf("Enter your age, assets, and favorite pet.\n");
     scanf("%d %f", &age, &assets);   // 這裡要使用&
     scanf("%s", pet);                // 字元陣列不使用&
     printf("%d $%.2f %s\n", age, assets, pet);

     return 0;
}

下面是該程式與使用者互動的示例:

Enter your age, assets, and favorite pet.
38
92360.88 llama
38 $92360.88 llama

scanf() 函式使用空白(換行符、製表符和空格)把輸入分成多個欄位。在依次把轉換說明和欄位匹配時跳過空白。注意,上面示例的輸入項(粗體部分是使用者的輸入)分成了兩行。只要在每個輸入項之間輸入至少一個換行符、空格或製表符即可,可以在一行或多行輸入:

Enter your age, assets, and favorite pet.
  42

     2121.45

     guppy
42 $2121.45 guppy

唯一例外的是 %c 轉換說明。根據 %c,scanf() 會讀取每個字元,包括空白。我們稍後詳述這部分。

scanf() 函式所用的轉換說明與 printf() 函式幾乎相同。主要的區別是,對於 float 型別和 double 型別,printf() 都使用 %f%e%E%g%G 轉換說明。而 scanf() 只把它們用於 float 型別,對於 double 型別要使用l修飾符。表 6 列出了 C99 標準中常用的轉換說明。

表 6 ANSI C 中 scanf() 的轉換說明

轉換說明 含義
%c 把輸入解釋成字元
%d 把輸入解釋成有符號十進位制整數
%e%f%g%a 把輸入解釋成浮點數(C99標準新增了%a
%E%F%G%A 把輸入解釋成浮點數(C99標準新增了%A
%i 把輸入解釋成有符號十進位制整數
%o 把輸入解釋成有符號八進位制整數
%p 把輸入解釋成指標(地址)
%s 把輸入解釋成字串。從第1個非空白字元開始,到下一個空白字元之前的所有字元都是輸入
%u 把輸入解釋成無符號十進位制整數
%x%X 把輸入解釋成有符號十六進位制整數

可以在表 6 所列的轉換說明中(百分號和轉換字元之間)使用修飾符。如果要使用多個修飾符,必須按表 7 所列的順序書寫。

表 7 scanf() 轉換說明中的修飾符

轉換說明 含義
* 抑制賦值(詳見後面解釋)
示例:"%*d"
數字 最大欄位寬度。輸入達到最大欄位寬度處,或第1次遇到空白字元時停止
示例:"%10s"
hh 把整數作為signed charunsigned char型別讀取
示例:"%hhd""%hhu"
ll 把整數作為long longunsigned long long型別讀取(C99
示例:"%lld""%llu"
hlL "%hd""%hi"表明把對應的值儲存為short int型別
"%ho""%hx""%hu"表明把對應的值儲存為unsigned short int型別
"%ld""%li"表明把對應的值儲存為long型別
"%lo""%lx""%lu"表明把對應的值儲存為unsigned long型別
"%le""%lf""%lg"表明把對應的值儲存為double型別
efg前面使用L而不是l,表明把對應的值被儲存為long double型別。如果沒有修飾符,diox表明對應的值被儲存為int型別,fg表明把對應的值儲存為float型別
j 在整型轉換說明後面時,表明使用intmax_tuintmax_t型別(C99)
示例:"%jd""%ju"
z 在整型轉換說明後面時,表明使用sizeof的返回型別(C99)
示例:"%zd""%zo"
t 在整型轉換說明後面時,表明使用表示兩個指標差值的型別(C99)
示例:"%td""%tx"

如你所見,使用轉換說明比較複雜,而且這些表中還省略了一些特性。省略的主要特性是,從高度格式化源中讀取選定資料,如穿孔卡或其他資料記錄。因為在本文中,scanf() 主要作為與程式互動的便利工具,所以我們不在文中討論更復雜的特性。

5.1 從 scanf() 角度看輸入

接下來,我們更詳細地研究 scanf() 怎樣讀取輸入。假設 scanf() 根據一個 %d 轉換說明讀取一個整數。scanf() 函式每次讀取一個字元,跳過所有的空白字元,直至遇到第 1 個非空白字元才開始讀取。因為要讀取整數,所以 scanf() 希望發現一個數字字元或者一個符號(+ 或 -)。如果找到一個數字或符號,它便儲存該字元,並讀取下一個字元。如果下一個字元是數字,它便儲存該數字並讀取下一個字元。scanf() 不斷地讀取和儲存字元,直至遇到非數字字元。如果遇到一個非數字字元,它便認為讀到了整數的末尾。然後,scanf() 把非數字字元放回輸入。這意味著程式在下一次讀取輸入時,首先讀到的是上一次讀取丟棄的非數字字元。最後,scanf() 計算已讀取數字(可能還有符號)相應的數值,並將計算後的值放入指定的變數中。

如果使用欄位寬度,scanf() 會在欄位結尾或第 1 個空白字元處停止讀取(滿足兩個條件之一便停止)。

如果第 1 個非空白字元是 A 而不是數字,會發生什麼情況?scanf() 將停在那裡,並把 A 放回輸入中,不會把值賦給指定變數。程式在下一次讀取輸入時,首先讀到的字元是 A。如果程式只使用 %d 轉換說明,scanf() 就一直無法越過 A 讀下一個字元。另外,如果使用帶多個轉換說明的 scanf(),C 規定在第 1 個出錯處停止讀取輸入。

用其他數值匹配的轉換說明讀取輸入和用 %d 的情況相同。區別在於 scanf() 會把更多字元識別成數字的一部分。例如,%x 轉換說明要求 scanf() 識別十六進位制數 a~f 和 A~F。浮點轉換說明要求 scanf() 識別小數點、e 記數法(指數記數法)和新增的 p 記數法(十六進位制指數記數法)。

如果使用 %s 轉換說明,scanf() 會讀取除空白以外的所有字元。scanf() 跳過空白開始讀取第 1 個非空白字元,並儲存非空白字元直到再次遇到空白。這意味著 scanf() 根據 %s 轉換說明讀取一個單詞,即不包含空白字元的字串。如果使用欄位寬度,scanf() 在欄位末尾或第 1 個空白字元處停止讀取。無法利用欄位寬度讓只有一個 %s 的 scanf() 讀取多個單詞。最後要注意一點:當 scanf() 把字串放進指定陣列中時,它會在字元序列的末尾加上 '\0',讓陣列中的內容成為一個 C 字串。

實際上,在 C 語言中 scanf() 並不是最常用的輸入函式。這裡重點介紹它是因為它能讀取不同型別的資料。C 語言還有其他的輸入函式,如 getchar() 和 fgets()。這兩個函式更適合處理一些特殊情況,如讀取單個字元或包含空格的字串。目前,無論程式中需要讀取整數、小數、字元還是字串,都可以使用 scanf() 函式。

5.2 格式字串中的普通字元

scanf() 函式允許把普通字元放在格式字串中。除空格字元外的普通字元必須與輸入字串嚴格匹配。例如,假設在兩個轉換說明中新增一個逗號:

scanf("%d,%d", &n, &m);

scanf() 函式將其解釋成:使用者將輸入一個數字、一個逗號,然後再輸入一個數字。也就是說,使用者必須像下面這樣進行輸入兩個整數:

88,121

由於格式字串中,%d 後面緊跟逗號,所以必須在輸入 88 後再輸入一個逗號。但是,由於 scanf() 會跳過整數前面的空白,所以下面兩種輸入方式都可以:

88, 121

88,
121

格式字串中的空白意味著跳過下一個輸入項前面的所有空白。例如,對於下面的語句:

scanf("%d ,%d", &n, &m);

以下的輸入格式都沒問題:

88,121
88 ,121
88 , 121

請注意,“所有空白”的概念包括沒有空格的特殊情況。

除了 %c,其他轉換說明都會自動跳過待輸入值前面所有的空白。因此,scanf("%d%d", &n, &m)scanf("%d %d", &n, &m) 的行為相同。對於 %c,在格式字串中新增一個空格字元會有所不同。例如,如果在格式字串中把空格放到 %c 的前面,scanf() 便會跳過空格,從第 1 個非空白字元開始讀取。也就是說,scanf("%c", &ch) 從輸入中的第 1 個字元開始讀取,而 scanf(" %c", &ch) 則從第 1 個非空白字元開始讀取。

5.3 scanf() 的返回值

scanf() 函式返回成功讀取的項數。如果沒有讀取任何項,且需要讀取一個數字而使用者卻輸入一個非數值字串,scanf() 便返回 0。當 scanf() 檢測到“檔案結尾”時,會返回 EOF(EOF 是 stdio.h 中定義的特殊值,通常用 #define 指令把 EOF 定義為 -1)。在讀者學會 if 語句和 while 語句後,便可使用 scanf() 的返回值來檢測和處理不匹配的輸入。

六、printf() 和 scanf() 的 * 修飾符

printf() 和 scanf() 都可以使用 * 修飾符來修改轉換說明的含義。但是,它們的用法不太一樣。首先,我們來看 printf() 的 * 修飾符。

如果你不想預先指定欄位寬度,希望通過程式來指定,那麼可以用 * 修飾符代替欄位寬度。但還是要用一個引數告訴函式,欄位寬度應該是多少。也就是說,如果轉換說明是 %*d,那麼引數列表中應包含 *d 對應的值。這個技巧也可用於浮點值指定精度和欄位寬度。程式清單 16 演示了相關用法。

程式清單 16 varwid.c 程式

/* varwid.c -- 使用變寬輸出欄位 */
#include <stdio.h>
int main(void)
{
     unsigned width, precision;
     int number = 256;
     double weight = 242.5;

     printf("Enter a field width:\n");
     scanf("%d", &width);
     printf("The number is :%*d:\n", width, number);
     printf("Now enter a width and a precision:\n");
     scanf("%d %d", &width, &precision);
     printf("Weight = %*.*f\n", width, precision, weight);
     printf("Done!\n");

     return 0;
}

變數 width 提供欄位寬度,number 是待列印的數字。因為轉換說明中 *d 的前面,所以在 printf() 的引數列表中,width 在 number 的前面。同樣,width 和 precision 提供列印 weight 的格式化資訊。下面是一個執行示例:

Enter a field width:
6
The number is :   256:
Now enter a width and a precision:
8 3
Weight =  242.500
Done!

這裡,使用者首先輸入 6,因此 6 是程式使用的欄位寬度。類似地,接下來使用者輸入 8 和 3,說明欄位寬度是 8,小數點後面顯示 3 位數字。一般而言,程式應根據 weight 的值來決定這些變數的值。

scanf() 中 * 的用法與此不同。把 * 放在%和轉換字元之間時,會使得 scanf() 跳過相應的輸入項。程式清單 17 就是一個例子。

程式清單 17 skip2.c 程式

/* skiptwo.c -- 跳過輸入中的前兩個整數 */
#include <stdio.h>
int main(void)
{
     int n;

     printf("Please enter three integers:\n");
     scanf("%*d %*d %d", &n);
     printf("The last integer was %d\n", n);

     return 0;
}

程式清單 17 中的 scanf() 指示:跳過兩個整數,把第 3 個整數拷貝給 n。下面是一個執行示例:

Please enter three integers:
2013 2014 2015
The last integer was 2015

在程式需要讀取檔案中特定列的內容時,這項跳過功能很有用。

七、printf() 的用法提示

想把資料列印成列,指定固定欄位寬度很有用。因為預設的欄位寬度是待列印數字的寬度,如果同一列中列印的數字位數不同,那麼下面的語句:

printf("%d %d %d\n", val1, val2, val3);

打印出來的數字可能參差不齊。例如,假設執行 3 次 printf() 語句,使用者輸入不同的變數,其輸出可能是這樣:

12 234 1222
4 5 23
22334 2322 10001

使用足夠大的固定欄位寬度可以讓輸出整齊美觀。例如,若使用下面的語句:

printf("%9d %9d %9d\n", val1, val2, val3);

上面的輸出將變成:

   12      234      1222
    4        5        23
22334     2322     10001

在兩個轉換說明中間插入一個空白字元,可以確保即使一個數字溢位了自己的欄位,下一個數字也不會緊跟該數字一起輸出(這樣兩個數字看起來像是一個數字)。這是因為格式字串中的普通字元(包括空格)會被打印出來。

另一方面,如果要在文字中嵌入一個數字,通常指定一個小於或等於該數字寬度的欄位會比較方便。這樣,輸出數字的寬度正合適,沒有不必要的空白。例如,下面的語句:

printf("Count Beppo ran %.2f miles in 3 hours.\n", distance);

其輸出如下:

Count Beppo ran 10.22 miles in 3 hours.

如果把轉換說明改為 %10.2f,則輸出如下:

Count Beppo ran      10.22 miles in 3 hours.

本地化設定

美國和世界上的許多地區都使用一個點來分隔十進位制值的整數部分和小數部分,如 3.14159。然而,許多其他地區用逗號來分隔,如 3,14159。讀者可能注意到了,printf() 和 scanf() 都沒有提供逗號的轉換說明。C 語言考慮了這種情況。因此 C 程式可以選擇特定的本地化設定。例如,如果指定了荷蘭語言環境,printf() 和 scanf() 在顯示和讀取浮點值時會使用本地慣例(在這種情況下,用逗號代替點分隔浮點值的整數部分和小數部分)。另外,一旦指定了環境,便可在程式碼的數字中使用逗號:

double pi = 3,14159; // 荷蘭本地化設定

C 標準有兩個本地化設定:"C"和""(空字串)。預設情況下,程式使用"C"本地化設定,基本上符合美國的用法習慣。而""本地化設定可以替換當前系統中使用的本地語言環境。原則上,這與"C"本地化設定相同。事實上,大部分作業系統(如 UNIX、Linux 和 Windows)都提供本地化設定選項列表,只不過它們提供的列表可能不同。

原文:C 語言中的 printf() 和 scanf() 簡介

(完)