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() 語句的另一個例子。

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 后取其余數。

把 336 轉換成字符

圖 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() 簡介

(完)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM