讀古今文學網 > CPrimerPlus(第6版)(中文版) > 第4章 字符串和格式化輸入/輸出 >

第4章 字符串和格式化輸入/輸出

本章介紹以下內容:

函數:strlen

關鍵字:const

字符串

如何創建、存儲字符串

如何使用strlen函數獲取字符串的長度

用C預處理器指令#define和ANSIC的const修飾符創建符號常量

本章重點介紹輸入和輸出。與程序交互和使用字符串可以編寫個性化的程序,本章將詳細介紹C語言的兩個輸入/輸出函數:printf和scanf。學會使用這兩個函數,不僅能與用戶交互,還可根據個人喜好和任務要求格式化輸出。最後,簡要介紹一個重要的工具——C預處理器指令,並學習如何定義、使用符號常量。

4.1 前導程序

與前兩章一樣,本章以一個簡單的程序開始。程序清單4.1與用戶進行簡單的交互。為了使程序的形式靈活多樣,代碼中使用了新的註釋風格。

程序清單4.1 talkback.c程序

// talkback.c -- 演示與用戶交互

#include <stdio.h>

#include <string.h> // 提供strlen函數的原型

#define DENSITY 62.4// 人體密度(單位:磅/立方英尺)

int main

{

float weight, volume;

int size, letters;

char name[40];  // name是一個可容納40個字符的數組

printf("Hi! What's your first name?\n");

scanf("%s", name);

printf("%s, what's your weight in pounds?\n", name);

scanf("%f", &weight);

size = sizeof name;

letters = strlen(name);

volume = weight / DENSITY;

printf("Well, %s, your volume is %2.2f cubic feet.\n",

name, volume);

printf("Also, your first name has %d letters,\n",

letters);

printf("and we have %d bytes to store it.\n", size);

return 0;

}

運行talkback.c程序,輸入結果如下:

Hi! What's your first name?

Christine

Christine, what's your weight in pounds?

154

Well, Christine, your volume is 2.47 cubic feet.

Also, your first name has 9 letters,

and we have 40 bytes to store it.

該程序包含以下新特性。

用數組(array)儲存字符串(character string)。在該程序中,用戶輸入的名被儲存在數組中,該數組佔用內存中40個連續的字節,每個字節儲存一個字符值。

使用%s轉換說明來處理字符串的輸入和輸出。注意,在scanf中,name沒有&前綴,而weight有(稍後解釋,&weight和name都是地址)。

用C預處理器把字符常量DENSITY定義為62.4。

用C函數strlen獲取字符串的長度。

對於BASIC的輸入/輸出而言,C的輸入/輸出看上去有些複雜。不過,複雜換來的是程序的高效和方便控制輸入/輸出。而且,一旦熟悉用法後,會發現它很簡單。

4.2 字符串簡介

字符串(character string)是一個或多個字符的序列,如下所示:

"Zing went the strings of my heart!"

雙引號不是字符串的一部分。雙引號僅告知編譯器它括起來的是字符串,正如單引號用於標識單個字符一樣。

4.2.1 char類型數組和null字符

C語言沒有專門用於儲存字符串的變量類型,字符串都被儲存在char類型的數組中。數組由連續的存儲單元組成,字符串中的字符被儲存在相鄰的存儲單元中,每個單元儲存一個字符(見圖4.1)。

圖4.1 數組中的字符串

注意圖4.1中數組末尾位置的字符\0。這是空字符(null character),C語言用它標記字符串的結束。空字符不是數字0,它是非打印字符,其ASCII碼值是(或等價於)0。C中的字符串一定以空字符結束,這意味著數組的容量必須至少比待存儲字符串中的字符數多1。因此,程序清單4.1中有40個存儲單元的字符串,只能儲存39個字符,剩下一個字節留給空字符。

那麼,什麼是數組?可以把數組看作是一行連續的多個存儲單元。用更正式的說法是,數組是同類型數據元素的有序序列。程序清單4.1通過以下聲明創建了一個包含40個存儲單元(或元素)的數組,每個單元儲存一個char類型的值:

char name[40];

name後面的方括號表明這是一個數組,方括號中的40表明該數組中的元素數量。char表明每個元素的類型(見圖4.2)。

圖4.2 聲明一個變量和聲明一個數組

字符串看上去比較複雜!必須先創建一個數組,把字符串中的字符逐個放入數組,還要記得在末尾加上一個\0。還好,計算機可以自己處理這些細節。

4.2.2 使用字符串

試著運行程序清單4.2,使用字符串其實很簡單。

程序清單4.2 praise1.c程序

/* praise1.c -- 使用不同類型的字符串 */

#include <stdio.h>

#define PRAISE "You are an extraordinary being."

int main(void)

{

char name[40];

printf("What's your name? ");

scanf("%s", name);

printf("Hello, %s.%s\n", name, PRAISE);

return 0;

}

%s告訴printf打印一個字符串。%s出現了兩次,因為程序要打印兩個字符串:一個儲存在name數組中;一個由PRAISE來表示。運行praise1.c,其輸出如下所示:

What's your name? Angela Plains

Hello, Angela.You are an extraordinary being.

你不用親自把空字符放入字符串末尾,scanf在讀取輸入時就已完成這項工作。也不用在字符串常量PRAISE末尾添加空字符。稍後我們會解釋#define指令,現在先理解PRAISE後面用雙引號括起來的文本是一個字符串。編譯器會在末尾加上空字符。

注意(這很重要),scanf只讀取了Angela Plains中的Angela,它在遇到第1個空白(空格、製表符或換行符)時就不再讀取輸入。因此,scanf在讀到Angela和Plains之間的空格時就停止了。一般而言,根據%s轉換說明,scanf只會讀取字符串中的一個單詞,而不是一整句。C語言還有其他的輸入函數(如,fgets),用於讀取一般字符串。後面章節將詳細介紹這些函數。

字符串和字符

字符串常量"x"和字符常量'x'不同。區別之一在於'x'是基本類型(char),而"x"是派生類型(char數組);區別之二是"x"實際上由兩個字符組成:'x'和空字符\0(見圖4.3)。

圖4.3 字符'x'和字符串"x"

4.2.3 strlen函數

上一章提到了 sizeof 運算符,它以字節為單位給出對象的大小。strlen函數給出字符串中的字符長度。因為 1 字節儲存一個字符,讀者可能認為把兩種方法應用於字符串得到的結果相同,但事實並非如此。請根據程序清單4.3,在程序清單4.2中添加幾行代碼,看看為什麼會這樣。

程序清單4.3 praise2.c程序

/* praise2.c */

// 如果編譯器不識別%zd,嘗試換成%u或%lu。

#include <stdio.h>

#include <string.h>/* 提供strlen函數的原型 */

#define PRAISE "You are an extraordinary being."

int main(void)

{

char name[40];

printf("What's your name? ");

scanf("%s", name);

printf("Hello, %s.%s\n", name, PRAISE);

printf("Your name of %zd letters occupies %zd memory cells.\n",

strlen(name), sizeof name);

printf("The phrase of praise has %zd letters ",

strlen(PRAISE));

printf("and occupies %zd memory cells.\n", sizeof PRAISE);

return 0;

}

如果使用ANSI C之前的編譯器,必須移除這一行:

#include <string.h>

string.h頭文件包含多個與字符串相關的函數原型,包括strlen。第11章將詳細介紹該頭文件(順帶一提,一些ANSI之前的UNIX系統用strings.h代替string.h,其中也包含了一些字符串函數的聲明)。

一般而言,C 把函數庫中相關的函數歸為一類,並為每類函數提供一個頭文件。例如,printf和scanf都隸屬標準輸入和輸出函數,使用stdio.h頭文件。string.h頭文件中包含了strlen函數和其他一些與字符串相關的函數(如拷貝字符串的函數和字符串查找函數)。

注意,程序清單4.3使用了兩種方法處理很長的printf語句。第1種方法是將printf語句分為兩行(可以在參數之間斷為兩行,但是不要在雙引號中的字符串中間斷開);第 2 種方法是使用兩個printf語句打印一行內容,只在第2條printf語句中使用換行符(\n)。運行該程序,其交互輸出如下:

What's your name? Serendipity Chance

Hello, Serendipity.You are an extraordinary being.

Your name of 11 letters occupies 40 memory cells.

The phrase of praise has 31 letters and occupies 32 memory cells.

sizeof運算符報告,name數組有40個存儲單元。但是,只有前11個單元用來儲存Serendipity,所以strlen得出的結果是11。name數組的第12個單元儲存空字符,strlen並未將其計入。圖4.4演示了這個概念。

圖4.4 strlen函數知道在何處停止

對於 PRAISE,用 strlen得出的也是字符串中的字符數(包括空格和標點符號)。然而,sizeof運算符給出的數更大,因為它把字符串末尾不可見的空字符也計算在內。該程序並未明確告訴計算機要給字符串預留多少空間,所以它必須計算雙引號內的字符數。

第 3 章提到過,C99 和 C11 標準專門為 sizeof 運算符的返回類型添加了%zd 轉換說明,這對於strlen同樣適用。對於早期的C,還要知道sizeof和strlen返回的實際類型(通常是unsigned或unsigned long)。

另外,還要注意一點:上一章的 sizeof 使用了圓括號,但本例沒有。圓括號的使用時機否取決於運算對象是類型還是特定量?運算對象是類型時,圓括號必不可少,但是對於特定量,可有可無。也就是說,對於類型,應寫成sizeof(char)或sizeof(float);對於特定量,可寫成sizeof name或sizeof 6.28。儘管如此,還是建議所有情況下都使用圓括號,如sizeof(6.28)。

程序清單4.3中使用strlen和sizeof,完全是為了滿足讀者的好奇心。在實際應用中,strlen和 sizeof 是非常重要的編程工具。例如,在各種要處理字符串的程序中,strlen很有用。詳見第11章。

下面我們來學習#define指令。

4.3 常量和C預處理器

有時,在程序中要使用常量。例如,可以這樣計算圓的周長:

circumference = 3.14159 * diameter;

這裡,常量3.14159代表著名的常量pi(π)。在該例中,輸入實際值便可使用這個常量。然而,這種情況使用符號常量(symbolic constant)會更好。也就是說,使用下面的語句,計算機稍後會用實際值完成替換:

circumference = pi * diameter;

為什麼使用符號常量更好?首先,常量名比數字表達的信息更多。請比較以下兩條語句:

owed = 0.015 * housevalue;

owed = taxrate * housevalue;

如果閱讀一個很長的程序,第2條語句所表達的含義更清楚。

另外,假設程序中的多處使用一個常量,有時需要改變它的值。畢竟,稅率通常是浮動的。如果程序使用符號常量,則只需更改符號常量的定義,不用在程序中查找使用常量的地方,然後逐一修改。

那麼,如何創建符號常量?方法之一是聲明一個變量,然後將該變量設置為所需的常量。可以這樣寫:

float taxrate;

taxrate = 0.015;

這樣做提供了一個符號名,但是taxrate是一個變量,程序可能會無意間改變它的值。C語言還提供了一個更好的方案——C預處理器。第2 章中介紹了預處理器如何使用#include包含其他文件的信息。預處理器也可用來定義常量。只需在程序頂部添加下面一行:

#define TAXRATE 0.015

編譯程序時,程序中所有的TAXRATE都會被替換成0.015。這一過程被稱為編譯時替換(compile-time substitution)。在運行程序時,程序中所有的替換均已完成(見圖 4.5)。通常,這樣定義的常量也稱為明示常量(manifest constant)[1]。

請注意格式,首先是#define,接著是符號常量名(TAXRATE),然後是符號常量的值(0.015)(注意,其中並沒有=符號)。所以,其通用格式如下:

#define NAME value

實際應用時,用選定的符號常量名和合適的值來替換NAME和value。注意,末尾不用加分號,因為這是一種由預處理器處理的替換機制。為什麼 TAXRATE 要用大寫?用大寫表示符號常量是 C 語言一貫的傳統。這樣,在程序中看到全大寫的名稱就立刻明白這是一個符號常量,而非變量。大寫常量只是為了提高程序的可讀性,即使全用小寫來表示符號常量,程序也能照常運行。儘管如此,初學者還是應該養成大寫常量的好習慣。

另外,還有一個不常用的命名約定,即在名稱前帶c_或k_前綴來表示常量(如,c_level或k_line)。

符號常量的命名規則與變量相同。可以使用大小寫字母、數字和下劃線字符,首字符不能為數字。程序清單4.4演示了一個簡單的示例。

圖4.5 輸入的內容和編譯後的內容

程序清單4.4 pizza.c程序

/* pizza.c -- 在比薩餅程序中使用已定義的常量 */

#include <stdio.h>

#define PI 3.14159

int main(void)

{

float area, circum, radius;

printf("What is the radius of your pizza?\n");

scanf("%f", &radius);

area = PI * radius * radius;

circum = 2.0 * PI *radius;

printf("Your basic pizza parameters are as follows:\n");

printf("circumference = %1.2f, area = %1.2f\n", circum,area);

return 0;

}

printf語句中的%1.2f表明,結果被四捨五入為兩位小數輸出。下面是一個輸出示例:

What is the radius of your pizza?

6.0

Your basic pizza parameters are as follows:

circumference = 37.70, area = 113.10

#define指令還可定義字符和字符串常量。前者使用單引號,後者使用雙引號。如下所示:

#define BEEP '\a'

#define TEE 'T'

#define ESC '\033'

#define OOPS "Now you have done it!"

記住,符號常量名後面的內容被用來替換符號常量。不要犯這樣的常見錯誤:

/* 錯誤的格式 */

#define TOES = 20

如果這樣做,替換TOES的是= 20,而不是20。這種情況下,下面的語句:

digits = fingers + TOES;

將被轉換成錯誤的語句:

digits = fingers + = 20;

4.3.1 const限定符

C90標準新增了const關鍵字,用於限定一個變量為只讀 [2]。其聲明如下:

const int MONTHS = 12; // MONTHS在程序中不可更改,值為12

這使得MONTHS成為一個只讀值。也就是說,可以在計算中使用MONTHS,可以打印MONTHS,但是不能更改MONTHS的值。const用起來比#define更靈活,第12章將討論與const相關的內容。

4.3.2 明示常量

C頭文件limits.h和float.h分別提供了與整數類型和浮點類型大小限制相關的詳細信息。每個頭文件都定義了一系列供實現使用的明示常量 [3]。例如,limits.h頭文件包含以下類似的代碼:

#define INT_MAX +32767

#define INT_MIN -32768

這些明示常量代表int類型可表示的最大值和最小值。如果系統使用32 位的int,該頭文件會為這些明示常量提供不同的值。如果在程序中包含limits.h頭文件,就可編寫下面的代碼:

printf("Maximum int value on this system = %d\n", INT_MAX);

如果系統使用4字節的int,limits.h頭文件會提供符合4字節int的INT_MAX和INT_MIN。表4.1列出了limits.h中能找到的一些明示常量。

表4.1 limits.h中的一些明示常量

類似地,float.h頭文件中也定義一些明示常量,如FLT_DIG和DBL_DIG,分別表示float類型和double類型的有效數字位數。表4.2列出了float.h中的一些明示常量(可以使用文本編輯器打開並查看系統使用的float.h頭文件)。表中所列都與float類型相關。把明示常量名中的FLT分別替換成DBL和LDBL,即可分別表示double和long double類型對應的明示常量(表中假設系統使用2的冪來表示浮點數)。

表4.2 float.h中的一些明示常量

程序清單4.5演示了如何使用float.h和limits.h中的數據(注意,編譯器要完全支持C99標準才能識別LLONG_MIN標識符)。

程序清單4.5 defines.c程序

// defines.c -- 使用limit.h和float頭文件中定義的明示常量

#include <stdio.h>

#include <limits.h>  // 整型限制

#include <float.h>// 浮點型限制

int main(void)

{

printf("Some number limits for this system:\n");

printf("Biggest int: %d\n", INT_MAX);

printf("Smallest long long: %lld\n", LLONG_MIN);

printf("One byte = %d bits on this system.\n", CHAR_BIT);

printf("Largest double: %e\n", DBL_MAX);

printf("Smallest normal float: %e\n", FLT_MIN);

printf("float precision = %d digits\n", FLT_DIG);

printf("float epsilon = %e\n", FLT_EPSILON);

return 0;

}

該程序的輸出示例如下:

Some number limits for this system:

Biggest int: 2147483647

Smallest long long: -9223372036854775808

One byte = 8 bits on this system.

Largest double: 1.797693e+308

Smallest normal float: 1.175494e-38

float precision = 6 digits

float epsilon = 1.192093e-07

C預處理器是非常有用的工具,要好好利用它。本書的後面章節中會介紹更多相關應用。

4.4 printf和scanf

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

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

4.4.1 printf函數

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

表4.3 轉換說明及其打印的輸出結果

4.4.2 使用printf

程序清單4.6的程序中使用了一些轉換說明。

程序清單4.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等都是要打印的項。它們可以是變量、常量,甚至是在打印之前先要計算的表達式。第3章提到過,格式字符串應包含每個待打印項對應的轉換說明。例如,考慮下面的語句:

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

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

下面是程序清單4.6中的另一行:

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

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

如圖4.7所示,格式字符串包含兩種形式不同的信息:

實際要打印的字符;

轉換說明。

圖4.6 printf的參數

圖4.7 剖析格式字符串

警告

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

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

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

如果只打印短語或句子,就不需要使用任何轉換說明。如果只打印數據,也不用加入說明文字。程序清單4.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.

4.4.3 printf的轉換說明修飾符

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

表4.4 printf的修飾符

注意 類型可移植性

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類型值專用的轉換說明。

表4.5 printf中的標記

1.使用修飾符和標記的示例

接下來,用程序示例演示如何使用這些修飾符和標記。先來看看字段寬度在打印整數時的效果。考慮程序清單4.7中的程序。

程序清單4.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);

return 0;

printf("*%-10d*\n", PAGES);

}

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

*959*

*959*

* 959*

*959 *

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

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

程序清單4.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的值,看看編譯器如何打印不同大小的值。程序清單4.9演示了其他組合。

程序清單4.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,%F打印出1F,%#x打印出0x1f。

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

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

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

程序清單4.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個字符。另外,-標記使得文本左對齊輸出。

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 richer!\n",name,cash);

4.4.4 轉換說明的意義

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

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

1.轉換不匹配

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

轉換說明與待打印值的類型不匹配會怎樣?上一章中介紹過不匹配導致的一些問題。匹配非常重要,一定要牢記於心。程序清單4.11演示了一些不匹配的整型轉換示例。

程序清單4.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字節。這種截斷(見圖4.8)相當於用一個整數除以256,只保留其餘數。在這種情況下,餘數是80,對應的ASCII值是字符P。用專業術語來說,該數字被解釋成「以256為模」(modulo 256),即該數字除以256後取其餘數。

圖4.8 把336轉換成字符

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

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

程序清單4.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類型的整數(見圖4.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);

圖4.9 傳遞參數

2.printf的返回值

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

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

程序清單4.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)。

3.打印較長的字符串

有時,printf語句太長,在屏幕上不方便閱讀。如果空白(空格、製表符、換行符)僅用於分隔不同的部分,C 編譯器會忽略它們。因此,一條語句可以寫成多行,只需在不同部分之間輸入空白即可。例如,程序清單4.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種方法,如程序清單4.14所示。

程序清單4.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"才是"young lovers"。

4.4.5 使用scanf

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

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

如果用scanf讀取基本變量類型的值,在變量名前加上一個&;

如果用scanf把字符串讀入字符數組中,不要使用&。

程序清單4.15中的小程序演示了這兩條規則。

程序清單4.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修飾符。表4.6列出了C99標準中常用的轉換說明。

表4.6 ANSI C中scanf的轉換說明

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

表4.7 scanf轉換說明中的修飾符

續表

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

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。這兩個函數更適合處理一些特殊情況,如讀取單個字符或包含空格的字符串。我們將在第7章、第11章、第13章中討論這些函數。目前,無論程序中需要讀取整數、小數、字符還是字符串,都可以使用scanf函數。

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個非空白字符開始讀取。

3.scanf的返回值

scanf函數返回成功讀取的項數。如果沒有讀取任何項,且需要讀取一個數字而用戶卻輸入一個非數值字符串,scanf便返回0。當scanf檢測到「文件結尾」時,會返回EOF(EOF是stdio.h中定義的特殊值,通常用#define指令把EOF定義為-1)。我們將在第6章中討論文件結尾的相關內容以及如何利用scanf的返回值。在讀者學會if語句和while語句後,便可使用scanf的返回值來檢測和處理不匹配的輸入。

4.4.6 printf和scanf的*修飾符

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

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

程序清單4.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跳過相應的輸出項。程序清單4.17就是一個例子。

程序清單4.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;

}

程序清單4.17中的scanf指示:跳過兩個整數,把第3個整數拷貝給n。下面是一個運行示例:

Please enter three integers:

2013 2014 2015

The last integer was 2015

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

4.4.7 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

4523

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語言考慮了這種情況。本書附錄B的參考資料V中介紹了C支持的本地化概念,因此C程序可以選擇特定的本地化設置。例如,如果指定了荷蘭語言環境,printf和scanf在顯示和讀取浮點值時會使用本地慣例(在這種情況下,用逗號代替點分隔浮點值的整數部分和小數部分)。另外,一旦指定了環境,便可在代碼的數字中使用逗號:

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

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

4.5 關鍵概念

C語言用char類型表示單個字符,用字符串表示字符序列。字符常量是一種字符串形式,即用雙引號把字符括起來:"Good luck, my friend"。可以把字符串儲存在字符數組(由內存中相鄰的字節組成)中。字符串,無論是表示成字符常量還是儲存在字符數組中,都以一個叫做空字符的隱藏字符結尾。

在程序中,最好用#define 定義數值常量,用 const 關鍵字聲明的變量為只讀變量。在程序中使用符號常量(明示常量),提高了程序的可讀性和可維護性。

C 語言的標準輸入函數(scanf)和標準輸出函數(printf)都使用一種系統。在該系統中,第1個參數中的轉換說明必須與後續參數中的值相匹配。例如,int轉換說明%d與一個浮點值匹配會產生奇怪的結果。必須格外小心,確保轉換說明的數量和類型與函數的其餘參數相匹配。對於scanf,一定要記得在變量名前加上地址運算符(&)。

空白字符(製表符、空格和換行符)在 scanf處理輸入時起著至關重要的作用。除了%c 模式(讀取下一個字符),scanf在讀取輸入時會跳過非空白字符前的所有空白字符,然後一直讀取字符,直至遇到空白字符或與正在讀取字符不匹配的字符。考慮一下,如果scanf根據不同的轉換說明讀取相同的輸入行,會發生什麼情況。假設有如下輸入行:

-13.45e12# 0

如果其對應的轉換說明是%d,scanf會讀取3個字符(-13)並停在小數點處,小數點將被留在輸入中作為下一次輸入的首字符。如果其對應的轉換說明是%f,scanf會讀取-13.45e12,並停在#符號處,而#將被留在輸入中作為下一次輸入的首字符;然後,scanf把讀取的字符序列-13.45e12轉換成相應的浮點值,並儲存在float類型的目標變量中。如果其對應的轉換說明是%s,scanf會讀取-13.45e12#,並停在空格處,空格將被留在輸入中作為下一次輸入的首字符;然後,scanf把這 10個字符的字符碼儲存在目標字符數組中,並在末尾加上一個空字符。如果其對應的轉換說明是%c,scanf只會讀取並儲存第1個字符,該例中是一個空格 [4]。

4.6 本章小結

字符串是一系列被視為一個處理單元的字符。在C語言中,字符串是以空字符(ASCII碼是0)結尾的一系列字符。可以把字符串儲存在字符數組中。數組是一系列同類型的項或元素。下面聲明了一個名為name、有30個char類型元素的數組:

char name[30];

要確保有足夠多的元素來儲存整個字符串(包括空字符)。

字符串常量是用雙引號括起來的字符序列,如:"This is an example of a string"。

scanf函數(聲明在string.h頭文件中)可用於獲得字符串的長度(末尾的空字符不計算在內)。scanf函數中的轉換說明是%s時,可讀取一個單詞。

C預處理器為預處理器指令(以#符號開始)查找源代碼程序,並在開始編譯程序之前處理它們。處理器根據#include指令把另一個文件中的內容添加到該指令所在的位置。#define指令可以創建明示常量(符號常量),即代表常量的符號。limits.h和float.h頭文件用#define定義了一組表示整型和浮點型不同屬性的符號常量。另外,還可以使用const限定符創建定義後就不能修改的變量。

printf和scanf函數對輸入和輸出提供多種支持。兩個函數都使用格式字符串,其中包含的轉換說明表明待讀取或待打印數據項的數量和類型。另外,可以使用轉換說明控制輸出的外觀:字段寬度、小數位和字段內的佈局。

4.7 複習題

複習題的參考答案在附錄A中。

1.再次運行程序清單 4.1,但是在要求輸入名時,請輸入名和姓(根據英文書寫習慣,名和姓中間有一個空格),看看會發生什麼情況?為什麼?

2.假設下列示例都是完整程序中的一部分,它們打印的結果分別是什麼?

a.printf("He sold the painting for $%2.2f.\n", 2.345e2);

b.printf("%c%c%c\n", 'H', 105, '\41');

c.#define Q "His Hamlet was funny without being vulgar."printf("%s\nhas %d characters.\n", Q, strlen(Q));

d.printf("Is %2.2e the same as %2.2f?\n", 1201.0, 1201.0);

3.在第2題的c中,要輸出包含雙引號的字符串Q,應如何修改?

4.找出下面程序中的錯誤。

define B booboo

define X 10

main(int)

{

int age;

char name;

printf("Please enter your first name.");

scanf("%s", name);

printf("All right, %c, what's your age?\n", name);

scanf("%f", age);

xp = age + X;

printf("That's a %s! You must be at least %d.\n", B, xp);

rerun 0;

}

5.假設一個程序的開頭是這樣:

#define BOOK "War and Peace"

int main(void)

{

float cost =12.99;

float percent = 80.0;

請構造一個使用BOOK、cost和percent的printf語句,打印以下內容:

This copy of "War and Peace" sells for $12.99.

That is 80% of list.

6.打印下列各項內容要分別使用什麼轉換說明?

a.一個字段寬度與位數相同的十進制整數

b.一個形如8A、字段寬度為4的十六進制整數

c.一個形如232.346、字段寬度為10的浮點數

d.一個形如2.33e+002、字段寬度為12的浮點數

e.一個字段寬度為30、左對齊的字符串

7.打印下面各項內容要分別使用什麼轉換說明?

a.字段寬度為15的unsigned long類型的整數

b.一個形如0x8a、字段寬度為4的十六進制整數

c.一個形如2.33E+02、字段寬度為12、左對齊的浮點數

d.一個形如+232.346、字段寬度為10的浮點數

e.一個字段寬度為8的字符串的前8個字符

8.打印下面各項內容要分別使用什麼轉換說明?

a.一個字段寬度為6、最少有4位數字的十進制整數

b.一個在參數列表中給定字段寬度的八進制整數

c.一個字段寬度為2的字符

d.一個形如+3.13、字段寬度等於數字中字符數的浮點數

e.一個字段寬度為7、左對齊字符串中的前5個字符

9.分別寫出讀取下列各輸入行的scanf語句,並聲明語句中用到變量和數組。

a.101

b.22.32 8.34E−09

c.linguini

d.catch 22

e.catch 22 (但是跳過catch)

10.什麼是空白?

11.下面的語句有什麼問題?如何修正?

printf("The double type is %z bytes..\n", sizeof(double));

12.假設要在程序中用圓括號代替花括號,以下方法是否可行?

#define ( {

#define ) }

4.8 編程練習

1.編寫一個程序,提示用戶輸入名和姓,然後以「名,姓」的格式打印出來。

2.編寫一個程序,提示用戶輸入名和姓,並執行一下操作:

a.打印名和姓,包括雙引號;

b.在寬度為20的字段右端打印名和姓,包括雙引號;

c.在寬度為20的字段左端打印名和姓,包括雙引號;

d.在比姓名寬度寬3的字段中打印名和姓。

3.編寫一個程序,讀取一個浮點數,首先以小數點記數法打印,然後以指數記數法打印。用下面的格式進行輸出(系統不同,指數記數法顯示的位數可能不同):

a.輸入21.3或2.1e+001;

b.輸入+21.290或2.129E+001;

4.編寫一個程序,提示用戶輸入身高(單位:英吋)和姓名,然後以下面的格式顯示用戶剛輸入的信息:

Dabney, you are 6.208 feet tall

使用float類型,並用/作為除號。如果你願意,可以要求用戶以厘米為單位輸入身高,並以米為單位顯示出來。

5.編寫一個程序,提示用戶輸入以兆位每秒(Mb/s)為單位的下載速度和以兆字節(MB)為單位的文件大小。程序中應計算文件的下載時間。注意,這裡1字節等於8位。使用float類型,並用/作為除號。該程序要以下面的格式打印 3 個變量的值(下載速度、文件大小和下載時間),顯示小數點後面兩位數字:

At 18.12 megabits per second, a file of 2.20 megabytes

downloads in 0.97 seconds.

6.編寫一個程序,先提示用戶輸入名,然後提示用戶輸入姓。在一行打印用戶輸入的名和姓,下一行分別打印名和姓的字母數。字母數要與相應名和姓的結尾對齊,如下所示:

Melissa Honeybee

78

接下來,再打印相同的信息,但是字母個數與相應名和姓的開頭對齊,如下所示:

Melissa Honeybee

7  8

7.編寫一個程序,將一個double類型的變量設置為1.0/3.0,一個float類型的變量設置為1.0/3.0。分別顯示兩次計算的結果各3次:一次顯示小數點後面6位數字;一次顯示小數點後面12位數字;一次顯示小數點後面16位數字。程序中要包含float.h頭文件,並顯示FLT_DIG和DBL_DIG的值。1.0/3.0的值與這些值一致嗎?

8.編寫一個程序,提示用戶輸入旅行的里程和消耗的汽油量。然後計算並顯示消耗每加侖汽油行駛的英里數,顯示小數點後面一位數字。接下來,使用1加侖大約3.785升,1英里大約為1.609千米,把單位是英里/加侖的值轉換為升/100公里(歐洲通用的燃料消耗表示法),並顯示結果,顯示小數點後面 1 位數字。注意,美國採用的方案測量消耗單位燃料的行程(值越大越好),而歐洲則採用單位距離消耗的燃料測量方案(值越低越好)。使用#define 創建符號常量或使用 const 限定符創建變量來表示兩個轉換係數。

[1].其實,符號常量的概念在K&R合著的《C語言程序設計》中介紹過。但是,在歷年的C標準中(包括最新的C11),並沒有符號常量的概念,只提到過#define最簡單的用法是定義一個「明示常量」。市面上各編程書籍對此概念的理解不同,有些作者把#define宏定義實現的「常量」歸為「明示常量」;有些作者(如,本書的作者)則認為「明示常量」相當於「符號常量」。——譯者注

[2].注意,在C語言中,用const類型限定符聲明的是變量,不是常量。——譯者注

[3].再次提醒讀者注意,本書作者認為「明示常量」相當於「符號常量」,經常在書中混用這兩個術語。——譯者注

[4].注意,「 -13.45e12# 0」的負號前面有一個空格。——譯者注