C現代方法(第22章)筆記——輸入/輸出

文章目錄

  • 第22章 輸入/輸出
    • 22.1 流
      • 22.1.1 文件指針
      • 22.1.2 標準流和重定向
      • 22.1.3 文本文件與二進制文件
    • 22.2 文件操作
      • 22.2.1 打開文件
      • 22.2.2 模式
      • 22.2.3 關閉文件
      • 22.2.4 為打開的流附加文件
      • 22.2.5 從命令行獲取文件名
      • 22.2.6 臨時文件
      • 22.2.7 文件緩沖
      • 22.2.8 其他文件操作
    • 22.3 格式化的輸入/輸出
      • 22.3.1 ...printf函數
      • 22.3.2 ...printf轉換說明
      • 22.3.3. C99對...printf轉化說明的修改(C99)
      • 22.3.4 ...printf轉換說明示例
      • 22.3.5 ...scanf函數
      • 22.3.6 ..scanf格式串
      • 22.3.7 ...scanf轉換說明
      • 22.3.8 C99對...scanf轉換說明的改變(C99)
      • 22.3.9 scanf示例
      • 22.3.10 檢測文件末尾和錯誤條件
    • 22.4 字符的輸入/輸出
      • 22.4.1 輸出函數
      • 22.4.2 輸入函數
        • 22.4.2.1 程序——復制文件
    • 22.5 行的輸入/輸出
      • 22.5.1 輸出函數
      • 22.5.2 輸入函數
    • 22.6 塊的輸入/輸出
    • 22.7 文件定位
      • 22.7.1 程序——修改零件記錄文件
    • 22.8 字符串的輸入/輸出
      • 22.8.1 輸出函數
      • 22.8.2 輸入函數
    • 問與答
    • 寫在最后

第22章 輸入/輸出

——在人與機器共存的世界中,懂得思變的一定是人,別指望機器。

C語言的輸入/輸出庫是標準庫中最大且最重要的部分。由于輸入/輸出是C語言的高級應用,因此這里將用一整章(篇幅最長)來討論<stdio.h>頭——輸入/輸出函數的主要存儲位置。

從第2章開始,我們已經在使用<stdio.h>了,而且已經對printf函數、scanf函數、putchar函數、getchar函數、puts函數以及gets函數的使用有了一定的了解。本章會提供有關這6個函數的更多信息,并介紹一些新的用于文件處理的函數。值得高興的是,許多新函數和我們已經熟知的函數有著緊密的聯系。例如,fprintf函數就是printf函數的“文件版”。

本章將首先討論一些基本問題:流的概念、FILE類型、輸入和輸出重定向,以及文本文件和二進制文件的差異(22.1節)。隨后將討論特別為使用文件而設計的函數,包括打開和關閉文件的函數(22.2節)。在討論完printf函數、scanf函數以及與“格式化”輸入/輸出相關的函數(22.3節)后,我們將著眼于讀/寫非格式化數據的函數。

  • 每次讀寫一個字符的getc函數、putc函數以及相關的函數(22.4節)。
  • 每次讀寫一行字符的gets函數、puts函數以及相關的函數(22.5節)。
  • 讀/寫數據塊的fread函數和fwrite函數(22.6節)。

隨后,22.7節會說明如何對文件執行隨機的訪問操作。最后,22.8節會描述sprintf函數、snprintf函數和sscanf函數,它們是printf函數和scanf函數的變體,后兩者分別用于寫入和讀取一個字符串。

本章涵蓋了<stdio.h>中的絕大部分函數,但忽略了其中8個函數。perror函數是這8個函數中的一個,它與<errno.h>頭緊密相關,所以把它推遲到24.2節討論<errno.h>頭時再來介紹。26.1節涵蓋了其余7個函數(vfprintfvprintfvsprintfvsnprintfvfscanfvscanfvsscanf)。這些函數依賴于va_list類型,該類型在26.1節介紹。

C89中,所有的標準輸入/輸出函數都屬于<stdio.h>。但從C99開始有所不同,有些輸入/輸出函數在<wchar.h>頭(25.5節)中聲明。<wchar.h>中的函數用于處理寬字符而不是普通字符,但大多數函數與<stdio.h>中的函數緊密相關。<stdio.h>中用于讀或寫數據的函數稱為字節輸入/輸出函數,而<wchar.h>中的類似函數則稱為寬字符輸入/輸出函數


22.1 流

C語言中,術語流(stream)表示任意輸入的源或任意輸出的目的地。許多小型程序(就像前面章節中介紹的那些)都是通過一個流(通常和鍵盤相關)獲得全部的輸入,并且通過另一個流(通常和屏幕相關)寫出全部的輸出。

較大規模的程序可能會需要額外的流。這些流常常表示存儲在不同介質(如硬盤驅動器、CDDVD和閃存)上的文件,但也很容易和不存儲文件的設備(如網絡端口、打印機等)相關聯。這里將集中討論文件,因為它們常見且容易理解。但是,請千萬記住一點:<stdio.h>中的許多函數可以處理各種形式的流,而不僅限于表示文件的流


22.1.1 文件指針

C程序中對流的訪問是通過文件指針(file pointer)實現的。此指針的類型為FILE *FILE類型在<stdio.h>中聲明)。用文件指針表示的特定流具有標準的名字;如果需要,還可以聲明另外一些文件指針。例如,如果程序除了標準流之外還需要兩個流,則可以包含如下聲明:

FILE *fp1, *fp2;

雖然操作系統通常會限制可以同時打開的流的數量,但程序可以聲明任意數量的FILE *類型變量。


22.1.2 標準流和重定向

<stdio.h>提供了3個標準流(見表22-1)。這3個標準流可以直接使用,不需要對其進行聲明,也不用打開或關閉它們。

表22-1 標準流

文件指針默認的含義
stdin標準輸入鍵盤
stdout標準輸出屏幕
stderr標準誤差屏幕

前面章節使用過的函數(printfscanfputchargetcharputsgets)都是通過stdin獲得輸入,并且用stdout進行輸出的。默認情況下,stdin表示鍵盤,stdoutstderr表示屏幕。然而,許多操作系統允許通過一種稱為重定向(redirection)的機制來改變這些默認的含義。

通常,我們可以強制程序從文件而不是從鍵盤獲得輸入,方法是在命令行中放上文件的名字,并在前面加上字符<

demo <in.dat

這種方法叫作輸入重定向(input redirection),它本質上是使stdin流表示文件(此例中為文件in.dat)而非鍵盤。重定向的絕妙之處在于,demo程序不會意識到正在從文件in.dat中讀取數據,它會認為從stdin獲得的任何數據都是從鍵盤輸入的

輸出重定向(output redirection)與之類似。對stdout流的重定向通常是通過在命令行中放置文件名,并在前面加上字符>實現的:

demo >out.da

現在所有寫入stdout的數據都將進入out.dat文件中,而不是出現在屏幕上。

順便說一下,我們還可以把輸出重定向和輸入重定向結合使用:

demo <in.dat >out.da

字符<>不需要與文件名相鄰,重定向文件的順序也是無關緊要的,所以下面的例子是等效的:

demo < in.dat > out.dat 
demo >out.dat <in.d

輸出重定向的一個問題是,會把寫入stdout的所有內容都放入文件中。如果程序運行失常并且開始寫出錯消息,那么我們在看文件的時候才會知道,而這些應該是出現在stderr中的。通過把出錯消息寫到stderr而不是stdout中,可以保證即使在對stdout進行重定向時,這些出錯消息仍能出現在屏幕上。(不過,操作系統通常也允許對stderr進行重定向。)


22.1.3 文本文件與二進制文件

<stdio.h>支持兩種類型的文件:文本文件二進制文件。在文本文件(text file)中,字節表示字符,這使人們可以檢查或編輯文件。例如,C程序的源代碼是存儲在文本文件中的。另外,在二進制文件(binary file)中,字節不一定表示字符,字節組還可以表示其他類型的數據,比如整數和浮點數。如果試圖查看可執行C程序的內容,你會立刻意識到它是存儲在二進制文件中的。

文本文件具有2種二進制文件沒有的特性:

  • 文本文件分為若干行。文本文件的每一行通常以一兩個特殊字符結尾, 特殊字符的選擇與操作系統有關。在Windows中,行末的標記是回車符('\x0d')與一個緊跟其后的回行符('\x0a')。在UNIXMacintosh操作系統(Mac OS)的較新版本中,行末的標記是一個單獨的回行符。舊版本的Mac OS使用一個單獨的換行符。
  • 文本文件可以包含一個特殊的“文件末尾”標記。一些操作系統允許在文本文件的末尾使用一個特殊的字節作為標記。在Windows中,標記為'\x1a'(Ctrl+Z)Ctrl+Z不是必需的,但如果存在,它就標志著文件的結束,其后的所有字節都會被忽略。使用Ctrl+Z的這一習慣繼承自DOS,而DOS中的這一習慣又是從CP/M(早期用于個人計算機的一種操作系統)來的。大多數其他操作系統(包括UNIX)沒有專門的文件末尾字符。

二進制文件不分行,也沒有行末標記和文件末尾標記所有字節都是平等對待

向文件寫入數據時,我們需要考慮是按文本格式存儲還是按二進制格式來存儲。為了搞清楚其中的差別,考慮在文件中存儲數32767的情況。一種選擇是以文本的形式把該數按字符32767寫入。假設字符集為ASCII,那么就可以得到下列5個字節:

0011001100110010001101110011011000110111
‘3’‘2’‘7’‘6’‘7’

另一種選擇是以二進制的形式存儲此數,這種方法只會占用2個字節:

01111111 11111111

[在按小端順序(20.3節)存儲數據的系統中,這兩個字節的順序相反。]從上述示例可以看出,用二進制形式存儲數可以節省相當大的空間。

編寫用來讀寫文件的程序時,需要考慮該文件是文本文件還是二進制文件。在屏幕上顯示文件內容的程序可能要把文件視為文本文件。但是,文件復制程序就不能認為要復制的文件是文本文件。如果那樣做,就不能完全復制含有文件末尾字符的二進制文件了。在無法確定文件是文本形式還是二進制形式時,安全的做法是把文件假定為二進制文件


22.2 文件操作

簡單性是輸入和輸出重定向的魅力之一:不需要打開文件、關閉文件或者執行任何其他的顯式文件操作。可惜的是,重定向在許多應用程序中受到限制。當程序依賴重定向時,它無法控制自己的文件,甚至無法知道這些文件的名字。更糟糕的是,如果程序需要在同一時間讀入兩個文件或者寫出兩個文件,重定向都無法做到

當重定向無法滿足需要時,我們將使用<stdio.h>提供的文件操作。本節將探討這些文件操作,包括打開文件、關閉文件、改變緩沖文件的方式、刪除文件以及重命名文件。


22.2.1 打開文件

FILE *fopen(const char * restrict filename, const char * restrict mode);

如果要把文件用作流,打開時就需要調用fopen函數。fopen函數的第一個參數是含有要打開文件名的字符串。(“文件名”可能包含關于文件位置的信息,如驅動器符或路徑。)第二個參數是“模式字符串”,它用來指定打算對文件執行的操作。例如,字符串"r"表明將從文件讀入數據,但不會向文件寫入數據。

注意!!在fopen函數的原型中,restrict關鍵字(17.8節)出現了兩次。restrict是從C99開始引入的關鍵字,表明filenamemode所指向的字符串的內存單元不共享C89中的fopen原型不包含restrict,但也有這樣的要求。restrictfopen的行為沒有影響,因此通常可以忽略。

請注意!!提醒Windows程序員:在fopen函數調用的文件名中含有字符\時,一定要小心。這是因為C語言會把字符\看作轉義序列(7.3節)的開始標志。

fopen("c:\project\test1.dat", "r");

以上調用會失敗,因為編譯器會把\t看作轉義字符。(\p不是有效的轉義字符,但看上去像。根據C標準,\p的含義是未定義的。)有兩種方法可以避免這一問題。一種方法是用``\代替\

fopen("c:\\project\\test1.dat", "r"); //另一種方法更簡單,只要用/代替\就可以了:
fopen("c:/project/test1.dat", "r");

Windows會把/認作目錄分隔符。

fopen函數返回一個文件指針。程序可以(且通常)把此指針存儲在一個變量中,稍后在需要對文件進行操作時使用它。fopen函數的常見調用形式如下所示,其中fpFILE*類型的變量:

fp = fopen("in.dat", "r");  /* opens in.dat for reading */ 

當程序稍后調用輸入函數從文件in.dat中讀數據時,會把fp作為一個實際參數。

當無法打開文件時,fopen函數會返回空指針。這可能是因為文件不存在,也可能是因為文件的位置不對,還可能是因為我們沒有打開文件的權限

請注意!!永遠不要假設可以打開文件,每次都要測試fopen函數的返回值以確保不是空指針。


22.2.2 模式

fopen函數傳遞哪種模式字符串不僅依賴于稍后將要對文件采取的操作,還取決于文件中的數據是文本形式還是二進制形式。要打開一個文本文件,可以采用表22-2中的一種模式字符串:

表22-2 用于文本文件的模式字符串

字符串含義
“r”打開文件用于讀
“w”打開文件用于寫(文件不需要存在)
“wx”創建文件用于寫(文件不能已經存在)①
“w+x”創建文件用于更新(文件不能已經存在)①
“a”打開文件用于追加(文件不需要存在)
“r+”打開文件用于讀和寫,從文件頭開始
“w+”打開文件用于讀和寫(如果文件存在就截去)
“a+”打開文件用于讀和寫(如果文件存在就追加)

① 從C11開始引入的模式(獨占的創建-打開模式)。

當使用fopen打開二進制文件時,需要在模式字符串中包含字母b表22-3列出了用于二進制文件的模式字符串。

表22-3 用于二進制文件的模式字符串

字符串含義
“rb”打開文件用于讀
“wb”打開文件用于寫(文件不需要存在)
“wbx”創建文件用于寫(文件不能已經存在)①
“ab”打開文件用于追加(文件不需要存在)
“r+b"或者"rb+”打開文件用于讀和寫,從文件頭開始
“w+b"或者"wb+”打開文件用于讀和寫(如果文件存在就截去)
“w+bx"或者"wb+x”創建文件用于更新(文件不能已經存在)①
“a+b"或者"ab+”打開文件用于讀和寫(如果文件存在就追加)

① 從C11開始引入的模式(獨占的創建-打開模式)。

表22-2表22-3可以看出<stdio.h>對寫數據和追加數據進行了區分。當給文件寫數據時,通常會對先前的內容進行覆蓋。然而,當為追加打開文件時,向文件寫入的數據添加在文件末尾,因而可以保留文件的原始內容。另外,帶有字母“x”的打開模式是從C11才開始引入的,這個字母表示獨占模式。在這種模式下,如果文件已經存在或者無法創建,fopen函數將執行失敗;否則文件將以獨占(非共享)模式打開。

順便說一下,當打開文件用于讀和寫(模式字符串包含字符+)時,有一些特殊的規則。如果沒有先調用一個文件定位函數(22.7節),那么就不能從讀模式轉換成寫模式,除非讀操作遇到了文件的末尾。類似地,如果既沒有調用fflush函數(稍后會介紹)也沒有調用文件定位函數,那么就不能從寫模式轉換成讀模式。


22.2.3 關閉文件

int fclose(FILE *stream);

fclose函數允許程序關閉不再使用的文件。fclose函數的參數必須是文件指針,此指針來自fopen函數或freopen函數(本節稍后會介紹)的調用。如果成功關閉了文件,fclose函數會返回零;否則,它會返回錯誤代碼EOF(在<stdio.h>中定義的宏)。

為了說明如何在實踐中使用fopen函數和fclose函數,下面給出了一個程序的框架。此程序打開文件example.dat進行讀操作,并要檢查打開是否成功,然后在程序終止前再把文件關閉:

#include <stdio.h> 
#include <stdlib.h> #define FILE_NAME "example.dat" int main(void) 
{ FILE *fp; fp = fopen(FILE_NAME, "r"); if (fp == NULL) { printf("Can’t open %s\n", FILE_NAME); exit(EXIT_FAILURE); } ... fclose(fp); return 0; 
}

當然,按照C程序員的編寫習慣,通常也可以把fopen函數的調用和fp的聲明結合在一起使用:

FILE *fp = fopen(FILE_NAME, "r");

還可以把函數調用與NULL判定相結合:

if ((fp = fopen(FILE_NAME, "r")) == NULL) ...

22.2.4 為打開的流附加文件

FILE *freopen(const char * restrict filename, const char * restrict mode, FILE * restrict stream); 

freopen函數為已經打開的流附加一個不同的文件。最常見的用法是把文件和一個標準流(stdin、stdout 或stderr)相關聯。例如,為了使程序開始往文件foo中寫數據,可以使用下列形式的freopen函數調用:

if (freopen("foo", "w", stdout) == NULL) { /* error; foo can’t be opened */ 
} 

在關閉了先前(通過命令行重定向或者之前的freopen函數調用)與stdout相關聯的所有文件之后,freopen函數將打開文件foo,并將其與stdout相關聯。

freopen函數的返回值通常是它的第三個參數(一個文件指針)。如果無法打開新文件,那么freopen函數會返回空指針。(如果無法關閉舊的文件,那么freopen函數會忽略錯誤。)

C99開始新增了一種機制。如果filename是空指針,freopen會試圖把流的模式修改為mode參數指定的模式。不過,具體的實現可以不支持這種特性;如果支持,則可以限定能進行哪些模式改變。


22.2.5 從命令行獲取文件名

當正在編寫的程序需要打開文件時,馬上會出現一個問題:如何把文件名提供給程序呢?把文件名嵌入程序自身的做法不太靈活,而提示用戶輸入文件名的做法也很笨拙。通常,最好的解決方案是讓程序從命令行獲取文件的名字。例如,當執行名為demo的程序時,可以通過把文件名放入命令行的方法為程序提供文件名:

demo names.dat dates.dat 

13.7節中,我們了解到如何通過定義帶有兩個形式參數的main函數來訪問命令行參數:

int main(int argc, char *argv[]) 
{ ...  
}

argc是命令行參數的數量,而argv是指向參數字符串的指針數組。argv[0]指向程序的名字,從argv[1]argv[argc-1]都指向剩余的實際參數,而argv[argc]是空指針。在上述例子中,argc3argv[0]指向含有程序名的字符串,argv[1]指向字符串"names.dat",而argv[2]則指向字符串"dates.dat"

下面舉例一個程序,該程序判斷文件是否存在,如果存在,則判斷它是否可以打開并讀入。在運行程序時,用戶將給出要檢查的文件的名字:

canopen file

然后程序將顯示出file can be opened或者顯示出file can't be opened。如果在命令行中輸入的實際參數的數量不對,那么程序將顯示出消息usage: canopen filename來提醒用戶canopen需要一個文件名。

/*
canopen.c 
--Checks whether a file can be opened for reading 
*/
#include <stdio.h> 
#include <stdlib.h> int main(int argc, char *argv[]) 
{ FILE *fp;if (argc != 2) { printf("usage: canopen filename\n"); exit(EXIT_FAILURE); }if ((fp = fopen(argv[1], "r")) == NULL) { printf("%s can’t be opened\n", argv[1]); exit(EXIT_FAILURE); } printf("%s can be opened\n", argv[1]); fclose(fp); return 0; 
}

注意!!可以使用重定向來丟棄canopen的輸出,并簡單地測試它返回的狀態值。


22.2.6 臨時文件

FILE *tmpfile(void);    
char *tmpnam(char *s); 

現實世界中的程序經常需要產生臨時文件,即只在程序運行時存在的文件。例如,C編譯器就常常產生臨時文件。編譯器可能先把C程序翻譯成一些存儲在文件中的中間形式,稍后把程序翻譯成目標代碼時,編譯器會讀取這些文件。一旦程序完全通過了編譯,就不再需要保留那些含有程序中間形式的文件了。<stdio.h>提供了兩個函數用來處理臨時文件,即tmpfile函數和tmpnam函數。

tmpfile函數創建一個臨時文件(用"wb+"模式打開),該臨時文件將一直存在,除非關閉它或程序終止。tmpfile函數的調用會返回文件指針,此指針可以用于稍后訪問該文件:

FILE *tempptr; 
... 
tempptr = tmpfile();   /* creates a temporary file */ 
//如果創建文件失敗,tmpfile函數會返回空指針。

雖然tmpfile函數很易于使用,但它有兩個缺點:

  • 無法知道tmpfile函數創建的文件名是什么
  • 無法在以后使文件變為永久的。如果這些缺陷導致了問題,備選的解決方案就是用fopen函數產生臨時文件。當然,我們不希望此文件擁有和前面已經存在的文件相同的名字,因此需要一種方法來產生新的文件名。這也是tmpnam函數出現的原因。

tmpnam函數為臨時文件產生名字。如果它的實際參數是空指針,那么tmpnam函數會把文件名存儲到一個靜態變量中,并且返回指向此變量的指針:

char *filename; 
... 
filename = tmpnam(NULL); /* creates a temporary file name */ 

否則,tmpnam函數會把文件名復制到程序員提供的字符數組中:

char filename[L_tmpnam]; 
... 
tmpnam(filename);   /* creates a temporary file name */ 

在后一種情況下,tmpnam函數也會返回指向數組第一個字符的指針。L_tmpnam<stdio.h>中的一個宏,它指明了保存臨時文件名的字符數組的長度。

請注意!!確保tmpnam函數所指向的數組至少有L_tmpnam個字符。此外,還要當心不能過于頻繁地調用tmpnam函數。宏TMP_MAX(在<stdio.h>中定義)指明了程序執行期間由tmpnam函數產生的臨時文件名的最大數量。如果生成文件名失敗,tmpnam返回空指針。


22.2.7 文件緩沖

int fflush(FILE *stream);                           
void setbuf(FILE * restrict stream, char * restrict buf);             
int setvbuf(FILE * restrict stream, char * restrict buf, int mode, size_t size);

向磁盤驅動器傳入數據或者從磁盤驅動器傳出數據都是相對較慢的操作。因此,在每次程序想讀或寫字符時都直接訪問磁盤文件是不可行的。獲得較好性能的訣竅就是緩沖(buffering):把寫入流的數據存儲在內存的緩沖區域內;當緩沖區滿了(或者關閉流)時,對緩沖區進行“清洗”(寫入實際的輸出設備)。輸入流可以用類似的方法進行緩沖:緩沖區包含來自輸入設備的數據;從緩沖區讀數據而不是從設備本身讀數據緩沖可以大幅提升效率,因為從緩沖區讀字符或者在緩沖區內存儲字符幾乎不花什么時間。當然,把緩沖區的內容傳遞給磁盤,或者從磁盤傳遞給緩沖區是需要花時間的,但是一次大的“塊移動”比多次小字節移動要快很多

<stdio.h>中的函數會在緩沖有用時自動進行緩沖操作。緩沖是在后臺發生的,我們通常不需要關心它的操作。然而,極少的情況下我們可能需要更主動。如果真是如此,可以使用fflush函數、setbuf函數和setvbuf函數。

當程序向文件中寫輸出時,數據通常先放入緩沖區中。當緩沖區滿了或者關閉文件時,緩沖區會自動清洗。然而,通過調用fflush函數,程序可以按我們所希望的頻率來清洗文件的緩沖區。調用

fflush(fp);    /* flushes buffer for fp */

為和fp相關聯的文件清洗了緩沖區。調用

fflush(NULL);  /* flushes all buffers */

清洗了全部輸出流。如果調用成功,fflush函數會返回零;如果發生錯誤,則返回EOF

setvbuf函數允許改變緩沖流的方法,并且允許控制緩沖區的大小和位置。函數的第三個實際參數指明了期望的緩沖類型,該參數應為以下三個宏之一:

  • _IOFBF(滿緩沖)。當緩沖區為空時,從流讀入數據;當緩沖區滿時,向流寫入數據。
  • _IOLBF(行緩沖)。每次從流讀入一行數據或者向流寫入一行數據。
  • _IONBF(無緩沖)。直接從流讀入數據或者直接向流寫入數據,而沒有緩沖區。

(所有這三種宏都在<stdio.h>中進行了定義。)對于沒有與交互式設備相連的流來說,滿緩沖是默認設置。

setvbuf函數的第二個參數(如果它不是空指針的話)是期望緩沖區的地址。緩沖區可以有靜態存儲期、自動存儲期,甚至可以是動態分配的。使緩沖區具有自動存儲期可以在塊退出時自動為其重新申請空間。動態分配緩沖區可以在不需要時釋放緩沖區。setvbuf函數的最后一個參數是緩沖區內字節的數量。較大的緩沖區可以提供更好的性能,而較小的緩沖區可以節省空間。

例如,下面這個setvbuf函數的調用利用buffer數組中的N個字節作為緩沖區,而把stream的緩沖變成了滿緩沖:

char buffer[N]; 
... 
setvbuf(stream, buffer, _IOFBF, N);

請注意!!setvbuf函數的調用必須在打開stream之后(流在前,緩沖在后),在對其執行任何其他操作之前。

用空指針作為第二個參數來調用setvbuf也是合法的,這樣做就要求setvbuf創建一個指定大小的緩沖區。如果調用成功,那么setvbuf函數返回零。如果mode參數無效或者要求無法滿足,那么setvbuf函數會返回非零值。

setbuf函數是一個較早期的函數,它設定了緩沖模式和緩沖區大小的默認值。如果buf是空指針,那么setbuf(stream, buf)調用就等價于

(void) setvbuf(stream, NULL, _IONBF, 0); 

否則,它就等價于

(void) setvbuf(stream, buf, _IOFBF, BUFSIZ);

這里的BUFSIZ是在<stdio.h>中定義的宏。我們把setbuf函數看作陳舊的內容,不建議大家在新程序中使用

請注意!!使用setvbuf函數或者setbuf函數時,一定要確保在釋放緩沖區之前已經關閉了流流在前,緩沖在后)。特別是,如果緩沖區是局部于函數的,并且具有自動存儲期,一定要確保在函數返回之前關閉流。


22.2.8 其他文件操作

int remove(const char *filename);                
int rename(const char *old, const char *new);

remove函數和rename函數允許程序執行基本的文件管理操作。不同于本節中大多數其他函數,remove函數和rename函數對文件名(而不是文件指針)進行處理。如果調用成功,那么這兩個函數都返回零;否則,都返回非零值。

remove函數刪除文件:

remove("foo");  /* deletes the file named "foo" */

如果程序使用fopen函數(而不是tmpfile函數)來創建臨時文件,那么它可以使用remove函數在程序終止前刪除此文件。一定要確保已經關閉了要移除的文件,因為對于當前打開的文件,移除文件的效果是由實現定義的。

rename函數改變文件的名字:

rename("foo", "bar");  /* renames "foo" to "bar" */

對于用fopen函數創建的臨時文件,如果程序需要使文件變為永久的,那么用rename函數改名是很方便的。如果具有新名字的文件已經存在了,改名的效果會由實現定義。

請注意!!如果打開了要改名的文件,那么一定要確保在調用rename函數之前關閉此文件。對打開的文件執行改名操作會失敗


22.3 格式化的輸入/輸出

本節將介紹使用格式串來控制讀/寫的庫函數。這些庫函數包括已經知道的printf函數和scanf函數,它們可以在輸入時把字符格式的數據轉換為數值格式的數據,并且可以在輸出時把數值格式的數據再轉換成字符格式的數據。其他的輸入/輸出函數不能完成這樣的轉換。


22.3.1 …printf函數

int fprintf(FILE * restrict stream, const char * restrict format, ...);  
int printf(const char * restrict format, ...);

fprintf函數和printf函數向輸出流中寫入可變數量的數據項,并且利用格式串來控制輸出的形式。這兩個函數的原型都是以...符號(省略號26.1節)結尾的,表明后面還有可變數量的實際參數。這兩個函數的返回值是寫入的字符數,若出錯則返回一個負值。

fprintf函數和printf函數唯一的不同就是printf函數始終向stdout(標準輸出流)寫入內容,而fprintf函數則向它自己的第一個實際參數指定的流中寫入內容:

printf("Total: %d\n", total);   /* writes to stdout */ 
fprintf(fp, "Total: %d\n", total); /* writes to fp */

printf函數的調用等價于fprintf函數把stdout作為第一個實際參數而進行的調用。

但是,不要以為fprintf函數只是把數據寫入磁盤文件的函數。<stdio.h>中的許多函數一樣,fprintf函數可以用于任何輸出流。事實上,fprintf函數最常見的應用之一(向標準誤差流stderr寫入出錯消息)和磁盤文件沒有任何關系。下面就是這類調用的一個示例:

fprintf(stderr, "Error: data file can’t be opened.\n");

stderr寫入消息可以保證消息能出現在屏幕上,即使用戶重定向stdout也沒關系。

<stdio.h>中還有另外兩個函數也可以向流寫入格式化的輸出。這兩個函數很不常見,一個是vfprintf函數,另一個是vprintf函數(26.1節)。它們都依賴于<stdarg.h>中定義的va_list類型,因此將和<stdarg.h>一起討論。


22.3.2 …printf轉換說明

printf函數和fprintf函數都要求格式串包含普通字符或轉換說明。普通字符會原樣輸出,而轉換說明則描述了如何把剩余的實參轉換為字符格式顯示出來。3.1節簡要介紹了轉換說明,其后的章節中還添加了一些細節。現在,我們將對已知的轉換說明內容進行回顧,并且把剩余的內容補充完整。

...printf函數的轉換說明由字符%和跟隨其后的最多5個不同的選項構成。假設格式串為%#012.5Lg,分析如下:

標志最小欄寬精度長度指定符轉換指定符
%#012.5Lg

下面對上述這些選項進行詳細的描述,選項的順序必須與上面一致:

  • 標志(可選項,允許多于一個)。標志導致在欄內左對齊,而其他標志則會影響數的顯示形式。表22-4給出了標志的完整列表。

表22-4 用于…printf函數的標志

標志含義
-在欄內左對齊(默認右對齊)
+有符號轉換得到的數總是以+-開頭(通常,只有負數前面附上-
空格有符號轉換得到的非負數前面加空格(+標志優先于空格標志)
#0開頭的八進制數,以0x0X開頭的十六進制非零數。浮點數始終有小數點。不能刪除由gG轉換輸出的數的尾部零
0(零)前導零在數的欄寬內進行填充。如果轉換是d、i、o、u、xX,而且指定了精度,那么可以忽略標志0-標志優先于0標志)
  • 最小欄寬(可選項)。如果數據項太小以至于無法達到這一寬度,那么會進行填充。(默認情況下會在數據項的左側添加空格,從而使其在欄內右對齊。)如果數據項過大以至于超過了這個寬度,那么會完整地顯示數據項。欄寬既可以是整數也可以是字符*。如果是字符*,那么欄寬由下一個參數決定。如果這個參數為負,它會被視為前面帶-標志的正數。

  • 精度(可選項)。精度的含義依賴于轉換指定符:如果轉換指定符是d、i、o、u、x、X,那么精度表示最少位數(如果位數不夠,則添加前導零);如果轉換指定符是a、A、e、E、f、F,那么精度表示小數點后的位數;如果轉換指定符是g、G,那么精度表示有效數字的個數;如果轉換指定符是s,那么精度表示最大字節數。精度是由小數點(.)后跟一個整數或字符*構成的。如果出現字符*,那么精度由下一個參數決定。(如果這個參數為負,效果與不指定精度一樣。)如果只有小數點,那么精度為零。

  • 長度指定符(可選項)。長度指定符配合轉換指定符,共同指定傳入的實際參數的類型(例如,%d通常表示一個int值,%hd用于顯示short int值,%ld用于顯示long int值)。表22-5列出了每一個長度指定符、可以使用的轉換說明以及兩者相結合時的類型(表中沒有給出的長度指定符和轉換指定符的結合會引起未定義的行為)。

表22-5 用于…printf函數的長度指定符

長度指定符轉換指定符含義
hh①d、i、o、u、x、Xsigned char, unsigned char
hh①nsigned char *
hd、i、o、u、x、Xshort int, unsigned short int
hnshort int *
l(ell)d、i、o、u、x、Xlong int, unsigned long int
l(ell)nlong int *
l(ell)cwint_t
l(ell)swchar_t *
l(ell)a、A、e、E、f、F、g、G無作用
ll①(ell-ell)d、i、o、u、x、Xlong long int, unsigned long long int
ll①(ell-ell)nlong long int *
j①d、i、o、u、x、Xintmax_t, uintmax_t
j①nintmax_t *
z①d、i、o、u、x、Xsize_t
z①nsize_t *
t①d、i、o、u、x、Xptrdiff_t
t①nptrdiff_t *
La、A、e、E、f、F、g、Glong double

①僅C99及之后的標準才有。

  • 轉換指定符。轉換指定符必須是表22-6中列出的某一種字符。注意f、F、e、E、g、G、aA全部設計用來輸出double類型的值,但把它們用于float類型的值也可以:由于有默認實參提升(9.3節)float類型實參在傳遞給帶有可變數量實參的函數時會自動轉換為double類型。類似地,傳遞給...printf函數的字符也會自動轉換為int類型,所以可以正常使用轉換指定符c

表22-6 …printf 函數的轉換指定符

轉換指定符含義
d、iint類型值轉換為十進制形式
o、u、x、X把無符號整數轉換為八進制(o)十進制(u)十六進制(x、X)形式。x表示用小寫字母a~f來顯示十六進制數,X表示用大寫字母A~F來顯示十六進制數
f、F①double類型值轉換為十進制形式,并且把小數點放置在正確的位置上。如果沒有指定精度,那么在小數點后面顯示6個數字
e、Edouble類型值轉換為科學記數法形式。如果沒有指定精度,那么在小數點后面顯示6個數字。如果選擇e,那么要把字母e放在指數前面;如果選擇E,那么要把字母E放在指數前面
g、Gg會把double類型值轉換為f形式或者e形式。當數值的指數部分小于-4,或者指數部分大于等于精度值時,會選擇e形式顯示。尾部的零不顯示(除非使用了#標志),且小數點僅在后邊跟有數字時才顯示出來。G會在F形式和E形式之間進行選擇
a①、A①使用格式[-]0xh.hhhhp±d的格式把double類型值轉換為十六進制科學記數法形式。其中[-]是可選的負號,h代表十六進制數位,±是正號或者負號,d是指數。d為十進制數,表示2的冪。如果沒有指定精度,在小數點后將顯示足夠的數位來表示準確的數值(如果可能的話)。a表示用小寫形式顯示a~fA表示用大寫形式顯示A~F。選擇a還是A也會影響字母xp的情況
c顯示無符號字符的int類型值
s寫出由實參指向的字符。當達到精度值(如果存在)或者遇到空字符時,停止寫操作
pvoid *類型值轉換為可打印形式
n相應的實參必須是指向int類型對象的指針。在該對象中存儲...printf函數調用已經輸出的字符數量,不產生輸出
%寫字符%

①僅C99及之后的標準才有。

請注意!!請認真遵守上述規則。使用無效的轉換說明會導致未定義的行為。


22.3.3. C99對…printf轉化說明的修改(C99)

C99printf函數和fprintf函數的轉換說明做了不少修改:

  • 增加了長度指定符C99中增加了hhlljzt長度指定符。hhll提供了額外的長度選項,j允許輸出最大寬度整數(27.1節)zt分別使對size_tptrdiff_t類型值的輸出變得更方便了。

  • 增加了轉換指定符C99中增加了F、aA轉換指定符。Ff一樣,區別在于書寫無窮數和NaN(見下面的討論)的方式。aA轉換指定符很少使用,它們和十六進制浮點常量相關,后者在第7章末尾的“問與答”部分討論過。

  • 允許輸出無窮數和NaNIEEE 754浮點標準允許浮點運算的結果為正無窮數負無窮數NaN(非數)。例如,1.0除以0.0會產生正無窮數,-1.0除以0.0會產生負無窮數,而0.0除以0.0會產生NaN(因為該結果在數學上是無定義的)。在C99中,轉換指定符a、A、e、E、f、F、gG能把這些特殊值轉換為可顯示的格式。a、e、fg將正無窮數轉換為infinfinity(都是合法的),將負無窮數轉換為-inf-infinity,將NaN轉換為nan-nan(后面可能跟著一對圓括號,圓括號里面有一系列的字符)。A、E、FGa、e、fg是等價的,區別僅在于使用大寫字母(INFINFINITYNAN)。

  • 支持寬字符。從C99開始的另一個特性是使用fprintf來輸出寬字符。%lc轉換說明用于輸出一個寬字符,%ls用于輸出一個由寬字符組成的字符串。

  • 之前未定義的轉換指定符現在允許使用了。在C89中,使用%le%lE%lf%lg以及%lG的效果是未定義的。這些轉換說明在C99及其之后都是合法的(l長度指定符被忽略)。


22.3.4 …printf轉換說明示例

現在來看一些示例。在前面的章節中我們已經看過大量日常轉換說明的例子了,所以下面將集中說明一些更高級的應用示例。與前面的章節一樣,這里將用·表示空格字符。

我們首先來看看標志作用于%d轉換的效果(對其他轉換的效果也是類似的)。表22-7的第一行顯示了不帶任何標志的%8d的效果。接下來的四行分別顯示了帶有標志-+空格以及0的效果(標志#從不用于%d)。剩下的幾行顯示了標志組合所產生的效果。

表22-7 標志作用于%d轉換的效果

轉換說明對123應用轉換說明的結果對-123應用轉換說明的結果
%8d?????123????-123
%-8d123?????-123????
%+8d????+123????-123
% 8d?????123????-123
%08d00000123-0000123
%-+8d+123????-123????
%- 8d?123????-123????
%+08d+0000123-0000123
% 08d?0000123-0000123

表22-8說明了標志#作用于oxXgG轉換的效果。

表22-8 標志#的效果

轉換說明對123應用轉換說明的結果對123.0應用轉換說明的結果
%8o?????173
%#8o????0173
%8x??????7b
%#8x????0x7b
%8X??????7B
%#8X????0X7B
%8g?????123
%#8g?123.000
%8G?????123
%#8G?123.000

在前面的章節中,表示數值時已經使用過最小欄寬和精度了,所以這里不再給出更多的示例,只在表22-9中給出最小欄寬和精度作用于%s轉換的效果。

表22-9 最小欄寬和精度作用于轉換%s的效果

轉換說明對"bogus"應用轉換說明的結果對"buzzword"應用轉換說明的結果
%6s?bogusbuzzword
%-6sbogus?buzzword
%.4sbogubuzz
%6.4s??bogu??buzz
%-6.4sbogu??buzz??

表22-10說明了%g轉換如何以%e%f的格式顯示數。表中的所有數都用轉換說明%.4g進行了書寫。前兩個數的指數至少為4,因此它們是按照%e的格式顯示的。接下來的8個數是按照%f的格式顯示的。最后兩個數的指數小于-4,所以也用%e的格式來顯示。

表22-10 %g轉換的示例

對數應用轉換%.4g的結果
123456.000000000001.235e+05
12345.600000000001.235e+04
1234.560000000001235
123.45600000000123.5
12.3456000000012.35
1.234560000001.235
0.123456000000.1235
0.012345600000.01235
0.001234560000.001235
0.000123456000.0001235
0.000012345601.235e-05
0.000001234561.235e-06

過去,我們假設最小欄寬和精度都是嵌在格式串中的常量。用字符*取代最小欄寬或精度通常可以把它們作為格式串之后的實際參數加以指定。例如,下列printf函數的調用都產生相同的輸出:

printf("%6.4d", i); 
printf("%*.4d", 6, i); 
printf("%6.*d", 4, i); 
printf("%*.*d", 6, 4, i)

注意!!為字符*填充的值剛好出現在待顯示的值之前。順便說一句,字符*的主要優勢就是它允許使用宏來指定欄寬或精度:

printf("%*d", WIDTH, i)

我們甚至可以在程序執行期間計算欄寬或精度:

printf("%*d", page_width / num_cols, i)

最不常見的轉換說明是%p%n%p轉換允許顯示指針的值:

printf("%p", (void *) ptr); /* displays value of ptr 

雖然在調試時%p偶爾有用,但它不是大多數程序員日常使用的特性。C標準沒有指定用%p顯示指針的形式,但很可能會以八進制十六進制數的形式顯示。

轉換%n用來找出到目前為止由...printf函數調用所顯示的字符數量。例如,在調用:

printf("%d%n\n", 123, &len)

之后len的值將為3,因為在執行轉換%n的時候printf函數已經顯示3個字符(123)了。注意,在len前面必須要有&(因為%n要求指針),這樣就不會顯示len自身的值。


22.3.5 …scanf函數

int fscanf(FILE * restrict stream, const char * restrict format, ...);  
int scanf(const char * restrict format, ...);

fscanf函數和scanf函數從輸入流讀入數據,并且使用格式串來指明輸入的格式。格式串的后邊可以有任意數量的指針(每個指針指向一個對象)作為額外的實際參數。輸入的數據項根據格式串中的轉換說明進行轉換并且存儲在指針指向的對象中。

scanf函數始終從標準輸入流stdin中讀入內容,而fscanf函數則從它的第一個參數所指定的流中讀入內容:

scanf("%d%d", &i, &j);  /* reads from stdin */ 
fscanf(fp, "%d%d", &i, &j);  /* reads from fp */

scanf函數的調用等價于以stdin作為第一個實際參數的fscanf函數調用。

如果發生輸入失敗(即沒有輸入字符可以讀)或者匹配失敗(即輸入字符和格式串不匹配),那么...scanf函數會提前返回。(在C99中,輸入失敗還可能由編碼錯誤導致。編碼錯誤意味著我們試圖按多字節字符的方式讀取輸入,但輸入字符不是有效的多字節字符。)這兩個函數都返回讀入并且賦值給對象的數據項的數量。如果在讀取任何數據項之前發生輸入失敗,那么會返回EOF

C程序中測試scanf函數的返回值的循環很普遍。例如,下列循環逐個讀取一串整數,在首個遇到問題的符號處停止:

//慣用法
while (scanf("%d", &i) == 1) { ...  
}

22.3.6 …scanf格式串

...scanf函數的調用類似于...printf函數的調用。然而,這種相似可能會產生誤導,實際上...scanf函數的工作原理完全不同于...printf函數。我們應該把scanf函數和fscanf函數看作“模式匹配”函數。格式串表示的就是...scanf函數在讀取輸入時試圖匹配的模式。如果輸入和格式串不匹配,那么一旦發現不匹配函數就會返回。不匹配的輸入字符將被“放回”留待以后讀取

...scanf函數的格式串可能含有三種信息:

  • 轉換說明...scanf函數格式串中的轉換說明類似于...printf函數格式串中的轉換說明。大多數轉換說明(%[%c%n例外)會跳過輸入項開始處的空白字符(3.2節)但是,轉換說明不會跳過尾部的空白字符。如果輸入含有·123¤,那么轉換說明%d會讀取·123,但是留下¤不讀取。(這里使用·表示空格符,用¤表示換行符。)
  • 空白字符...scanf函數格式串中的一個或多個連續的空白字符與輸入流中的零個或多個空白字符相匹配。
  • 非空白字符。除了%之外的非空白字符和輸入流中的相同字符相匹配。

例如,格式串"ISBN %d-%d-%ld-%d"說明輸入由下列這些內容構成:字母ISBN,可能有一些空白字符,一個整數,字符-,一個整數(前面可能有空白字符),字符-,一個長整數(前面可能有空白字符),字符-和一個整數(前面可能有空白字符)。


22.3.7 …scanf轉換說明

用于...scanf函數的轉換說明實際上比用于...printf函數的轉換說明簡單一些。...scanf函數的轉換說明由字符%和跟隨其后的下列選項(按照出現的順序)構成。

  • 字符*(可選項)。字符*的出現意味著賦值屏蔽(assignment suppression):讀入此數據項,但是不會把它賦值給對象。用*匹配的數據項不包含在...scanf函數返回的計數中。
  • 最大欄寬(可選項)。最大欄寬限制了輸入項中的字符數量。如果達到了這個最大值,那么此數據項的轉換將結束。轉換開始處跳過的空白字符不進行統計。
  • 長度指定符(可選項)。長度指定符表明用于存儲輸入數據項的對象的類型與特定轉換說明中的常見類型長度不一致。表22-11列出了每一個長度指定符、可以使用的轉換說明以及兩者相結合時的類型(表中沒有給出的長度指定符和轉換指定符的結合會引起未定義的行為)。

表22-11 用于...scanf函數的長度指定符

長度指定符轉換指定符含義
hh①d、i、o、u、x、X、nsigned char , unsigned char
hd、i、o、u、x、X、nshort int , unsigned short int
l(ell)d、i、o、u、x、X、nlong int , unsigned long int
l(ell)a、A、e、E、f、F、g、Gdouble *
l(ell)c、s、[wchar_t *
ll①(ell-ell)d、i、o、u、x、X、nlong long int , unsigned long long int
j①d、i、o、u、x、X、nintmax_t , uintmax_t
z①d、i、o、u、x、X、nsize_t *
t①d、i、o、u、x、X、nptrdiff_t *
La、A、e、E、f、F、g、Glong double *

① 僅C99及之后的標準才有。

  • 轉換指定符。轉換指定符必須是表22-12中列出的某一種字符。

表22-12 用于...scanf函數的轉換指定符

轉換指定符含義
d匹配十進制整數,假設相應的實參是int *類型
i匹配整數,假設相應的實參是int *類型。假定數是十進制形式的,除非它以0開頭(說明是八進制形式),或者以0x0X開頭(十六進制形式)
o匹配八進制整數。假設相應的實參是unsigned int *類型
u匹配十進制整數。假設相應的實參是unsigned int *類型
x、X匹配十六進制整數。假設相應的實參是unsigned int *類型
a①、A①、e、E、f、F①、g、G匹配浮點數。假設相應的實參是float *類型。在C99中,該數可以是無窮大或NaN
c匹配n個字符,這里的n是最大欄寬。如果沒有指定欄寬,那么就匹配一個字符。假設相應的實參是指向字符數組的指針(如果沒有指定欄寬,就指向字符對象)。不在末尾添加空字符
s匹配一串非空白字符,然后在末尾添加空字符。假設相應的實參是指向字符數組的指針
[匹配來自掃描集合的非空字符序列,然后在末尾添加空字符。假設相應的實參是指向字符數組的指針
p...printf函數的輸出格式匹配指針值。假設相應的實參是指向void*對象的指針
n相應的實參必須指向int類型的對象。把到目前為止讀入的字符數量存儲到此對象中。沒有輸入會被吸收進去,而且...scanf函數的返回值也不會受到影響
%匹配字符%

① 僅C99及之后的標準才有。

數值型數據項可以始終用符號(+-)作為開頭。然而,說明符ouxX把數據項轉換成無符號的形式,所以通常不用這些說明符來讀取負數。

說明符[是說明符s更加復雜(且更加靈活)的版本。使用[的完整轉換說明格式是%[集合]或者%[^集合],這里的集合可以是任意字符集。(但是,如果]是集合中的一個字符,那么它必須首先出現。)%[集合]匹配集合(即掃描集合)中的任意字符序列。%[^集合]匹配不在集合中的任意字符序列(換句話說,構成掃描集合的全部字符都不在集合中)。例如,%[abc]匹配的是只含有字母abc的任何字符串,而%[^abc]匹配的是不含有字母abc的任何字符串。

...scanf函數的許多轉換指定符和<stdlib.h>中的數值轉換函數(26.2節)有著緊密的聯系。這些函數把字符串(如"-297")轉換成與其等價的數值(-297)。例如,說明符d尋找可選的+號或-號,后邊跟著一串十進制的數字。這樣就與把字符串轉換成十進制數的strtol函數所要求的格式完全一樣了。表22-13展示了轉換指定符和數值轉換函數之間的對應關系。

表22-13 ...scanf轉換指定符和數值轉換函數之間的對應關系

轉換指定符字符串轉換函數
d10作為基數的strtol函數
i0作為基數的strtol函數
o8作為基數的strtoul函數
u10作為基數的strtoul函數
x、X16作為基數的strtoul函數
a、A、e、E、f、F、g、Gstrtod函數

請注意!!編寫scanf函數的調用時需要十分小心。scanf格式串中無效的轉換說明就像printf格式串中的無效轉換說明一樣糟糕,都會導致未定義的行為。


22.3.8 C99對…scanf轉換說明的改變(C99)

C99開始的標準對scanffscanf的轉換說明做了一些改變,但沒有...printf函數那么多。

  • 增加了長度指定符。從C99開始增加了hhlljzt長度指定符,它們與...printf轉換說明中的長度指定符相對應。
  • 增加了轉換指定符。從C99開始增加了FaA轉換指定符,提供這些轉換指定符是為了與...printf相一致。...scanf函數把它們與eEfgG等同看待。
  • 具有讀無窮數和NaN的能力。正如...printf函數可以輸出無窮數和NaN一樣,...scanf函數可以讀這些值。為了能夠正確讀出,這些數的形式應該與...printf函數相同,忽略大小寫(例如,INFinf都會被認為是無窮數)。
  • 支持寬字符...scanf函數能夠讀多字節字符,并在存儲時將之轉換為寬字符。%lc轉換說明用于讀出單個的多字節字符或者一系列多字節字符;%ls用于讀取由多字節字符組成的字符串(在結尾添加空字符)。%l[集合]%l[^集合]轉換說明也可以讀取多字節字符串。

22.3.9 scanf示例

下面三個表格包含了scanf的調用示例。每個示例都把scanf函數應用于它右側的輸入字符。用高亮顯示的字符會被調用吸收。調用后變量的值會出現在輸入的右側。

表22-14中的示例說明了把轉換說明、空白字符以及非空白字符組合在一起的效果。在這三種情況下沒有對j賦值,所以j的值在scanf調用前后保持不變。表22-15中的示例顯示了賦值屏蔽和指定欄寬的效果。表22-16中的示例描述了更加深奧的轉換指定符(即i[n)。

表22-14 scanf示例(第一組)

scanf函數的調用輸入變量
n = scanf(“%d%d”, &i, &j);12?,?34¤n:1 i:12 j:不變
n = scanf(“%d,%d”, &i, &j);12?,?34¤n:1 i:12 j:不變
n = scanf(“%d ,%d”, &i, &j);12?,?34¤n:2 i:12 j:34
n = scanf(“%d, %d”, &i, &j);12?,?34¤n:1 i:12 j:不變

表22-15 scanf示例(第二組)

scanf函數的調用輸入變量
n = scanf(“%*d%d”, &i);12?34¤n:1 i:34
n = scanf(“%*s%s”, str);My?Fair?Lady¤n:1 str:“Fair”
n = scanf(“%1d%2d%3d”, &i, &j, &k);12345¤n:3 i:1 j:23 k:45
n = scanf(“%2d%2s%2d”, &i, str, &j);123456¤n:3 i:12 str:“34” j:56

表22-16 scanf示例(第三組)

scanf函數的調用輸入變量
n = scanf(“%i%i%i”, &i, &j, &k);12?012?0x12¤n:3 i:12 j:10 k:18
n = scanf(“%[0123456789]”, str);123abc¤n:1 Str: “123”
n = scanf(“%[0123456789]”, str);abc123¤n:0 str:不變
n = scanf(“%[^0123456789]”, str);abc123¤n:1 Str: “abc”
n = scanf(“%*d%d%n”, &i, &j);10?20?30¤n:1 i:20 j:5

22.3.10 檢測文件末尾和錯誤條件

void clearerr(FILE *stream);    
int feof(FILE *stream); 
int ferror(FILE *stream);

如果要求...scanf函數讀入并存儲n個數據項,那么希望它的返回值就是n。如果返回值小于n,那么一定是出錯了。一共有三種可能情況:

  • 文件末尾。函數在完全匹配格式串之前遇到了文件末尾。
  • 讀取錯誤。函數不能從流中讀取字符。
  • 匹配失敗。數據項的格式是錯誤的。例如,函數可能在搜索整數的第一個數字時遇到了一個字母。

但是如何知道遇到的是哪種情況呢?在許多情況下,這是無關緊要的,程序出問題了,可以把它舍棄。然而,有時候需要查明失敗的原因。

每個流都有與之相關的兩個指示器:錯誤指示器(error indicator)文件末尾指示器(end-of-file indicator),當打開流時會清除這些指示器。遇到文件末尾就設置文件末尾指示器,遇到讀錯誤就設置錯誤指示器。(輸出流上發生寫錯誤時也會設置錯誤指示器。)匹配失敗不會改變任何一個指示器。

一旦設置了錯誤指示器或者文件末尾指示器,它就會保持這種狀態直到被顯式地清除(可能通過clearerr函數的調用)。clearerr會同時清除文件末尾指示器和錯誤指示器

clearerr(fp);  /* clears eof and error indicators for fp *///某些其他庫函數因為副作用可以清除某種指示器或兩種都可以清除,
//所以不需要經常使用clearerr函數。

我們可以調用feof函數和ferror函數來測試流的指示器,從而確定出先前在流上的操作失敗的原因。如果為與fp相關的流設置了文件末尾指示器,那么feof(fp)函數調用就會返回非零值。如果設置了錯誤指示器,那么ferror(fp)函數的調用也會返回非零值。而其他情況下,這兩個函數都會返回零。

scanf函數返回小于預期的值時,可以使用feof函數和ferror函數來確定原因。如果feof函數返回了非零的值,那么就說明已經到達了輸入文件的末尾。如果ferror函數返回了非零的值,那么就表示在輸入過程中產生了讀錯誤。如果兩個函數都沒有返回非零值,那么一定是發生了匹配失敗。不管問題是什么,scanf函數的返回值都會告訴我們在問題產生前所讀入的數據項的數量。

為了明白feof函數和ferror函數可能的使用方法,現在來編寫一個函數。此函數用來搜索文件中以整數起始的行。下面是預計的函數調用方式:

n = find_int("foo");

其中,"foo"是要搜索的文件的名字,函數返回找到的整數的值并將其賦給n。如果出現問題(文件無法打開或者發生讀錯誤,再或者沒有以整數起始的行),find_int函數將返回一個錯誤代碼(分別是-1-2-3)。我們假設文件中沒有以負整數起始的行。

int find_int(const char *filename) 
{ FILE *fp = fopen(filename, "r"); int n; if (fp == NULL) return1;   /* can’t open file */ while (fscanf(fp, "%d", &n) != 1) { if (ferror(fp)) { fclose(fp); return2;    /* input error */ } if (feof(fp)) { fclose(fp); return3;    /* integer not found */ } fscanf(fp, "%*[^\n]"); /* skips rest of line */ } fclose(fp); return n; 
} 

while循環的控制表達式調用fscanf函數的目的是從文件中讀取整數。如果嘗試失敗了(fscanf函數返回的值不為1),那么find_int函數就會調ferror函數和feof函數來了解是發生了讀錯誤還是遇到了文件末尾。如果都不是,那么fscanf函數一定是由于匹配錯誤而失敗的,因此find_int函數會跳過當前行的剩余字符并嘗試下一行。請注意用轉換說明%*[^\n]跳過全部字符直到下一個換行符為止的用法。(我們對掃描集合已有所了解,可以拿出來顯擺一下了!)


22.4 字符的輸入/輸出

本節將討論用于讀和寫單個字符的庫函數。這些函數可以處理文本流和二進制流。

請注意!!本節中的函數把字符作為int類型而非char類型的值來處理。這樣做的原因之一就是,輸入函數是通過返回EOF來說明文件末尾(或錯誤)情況的,而EOF又是一個負的整型常量。


22.4.1 輸出函數

int fputc(int c, FILE *stream); 
int putc(int c, FILE *stream); 
int putchar(int c); 

putchar函數向標準輸出流stdout寫一個字符:

putchar(ch);  /* writes ch to stdout */ 

fputc函數和putc函數是putchar函數向任意流寫字符的更通用的版本:

fputc(ch, fp); /* writes ch to fp */ 
putc(ch, fp); /* writes ch to fp */

雖然putc函數和fputc函數做的工作相同,但是putc通常作為宏來實現(也有函數實現),而fputc函數則只作為函數實現。putchar本身通常也定義為宏:

#define putchar(c) putc((c), stdout)

標準庫既提供putc又提供fputc,看起來很奇怪。但是,正如在14.3節看到的那樣,宏有幾個潛在的問題。C標準允許putc宏對stream參數多次求值,而fputc則不可以。雖然程序員通常偏好使用putc,因為它的速度較快,但fputc作為備選也是可用的。

如果出現了寫錯誤,那么上述這3個函數都會為流設置錯誤指示器并且返回EOF。否則,它們都會返回寫入的字符。


22.4.2 輸入函數

int fgetc(FILE *stream); 
int getc(FILE *stream); 
int getchar(void); 
int ungetc(int c, FILE *stream);

getchar函數從標準輸入流stdin中讀入一個字符:

ch = getchar(); /* reads a character from stdin */

fgetc函數和getc函數從任意流中讀入一個字符:

ch = fgetc(fp); /* reads a character from fp */ 
ch = getc(fp); /* reads a character from fp */ 

3個函數都把字符看作unsigned char類型的值(返回之前轉換成int類型)。因此,它們不會返回EOF之外的負值。

getcfgetc之間的關系類似于putcfputc之間的關系。getc通常作為宏來實現(也有函數實現),而fgetc則只作為函數實現。getchar本身通常也定義為宏:

#define getchar() getc(stdin)

對于從文件中讀取字符來說,程序員通常喜歡getc勝過fgetc。因為getc一般是宏的形式,所以它執行起來的速度較快。如果getc不合適,那么可以用fgetc作為備選。(標準允許getc宏對參數多次求值,這可能會有問題。)

如果出現問題,那么這3個函數的行為是一樣的。如果遇到了文件末尾,那么這3個函數都會設置流的文件末尾指示器,并且返回EOF。如果產生了讀錯誤,則它們都會設置流的錯誤指示器,并且返回EOF。為了區分這兩種情況,可以調用feof函數或者ferror函數。

fgetc函數、getc函數和getchar函數最常見的用法之一就是從文件中逐個讀入字符,直到遇到文件末尾。一般習慣使用下列while循環來實現此目的:

//慣用法
while ((ch = getc(fp)) != EOF) { ... 
}

在從與fp相關的文件中讀入字符并且把它存儲到變量ch(它必須是int類型的)之中后,判定條件會把chEOF進行比較。如果ch不等于EOF,則表示還未到達文件末尾,就可以執行循環體。如果ch等于EOF,則循環終止。

請注意!!始終要把fgetcgetcgetchar函數的返回值存儲在int類型的變量中,而不是char類型的變量中。把char類型變量與EOF進行比較可能會得到錯誤的結果。

還有另外一種字符輸入函數,即ungetc函數。此函數把從流中讀入的字符“放回”并清除流的文件末尾指示器。如果在輸入過程中需要往前多看一個字符,那么這種能力可能會非常有效。比如,為了讀入一系列數字,并且在遇到首個非數字時停止操作,可以寫成

while (isdigit(ch = getc(fp))) { ... 
} 
ungetc(ch, fp); /* pushes back last character read */ 

通過持續調用ungetc函數而放回的字符數量(不干涉讀操作)依賴于實現和所含的流類型。只有第一次的ungetc函數調用保證會成功。調用文件定位函數(即fseekfsetposrewind)(22.7節)會導致放回的字符丟失。

ungetc返回要求放回的字符。如果試圖放回EOF或者試圖放回超過最大允許數量的字符數,則ungetc會返回EOF

22.4.2.1 程序——復制文件

下面的程序用來進行文件的復制操作。當程序執行時,會在命令行上指定原始文件名和新文件名。例如,為了把文件f1.c復制給文件f2.c,可以使用命令:

fcopy f1.c f2.c

如果命令行上的文件名不是兩個,或者至少有一個文件無法打開,那么程序fcopy將產生出錯消息。

/*
fcopy.c
--Copies a file
*/
#include <stdio.h> 
#include <stdlib.h> int main(int argc, char *argv[]) 
{ FILE *source_fp, *dest_fp; int ch; if (argc != 3) { fprintf(stderr, "usage: fcopy source dest\n"); exit(EXIT_FAILURE); } if ((source_fp = fopen(argv[1], "rb")) == NULL) { fprintf(stderr, "Can't open %s\n", argv[1]); exit(EXIT_FAILURE); } if ((dest_fp = fopen(argv[2], "wb")) == NULL) { fprintf(stderr, "Can't open %s\n", argv[2]); fclose(source_fp); exit(EXIT_FAILURE); } while ((ch = getc(source_fp)) != EOF) putc(ch, dest_fp); fclose(source_fp); fclose(dest_fp); return 0; 
}

采用"rb""wb"作為文件模式,使fcopy程序既可以復制文本文件也可以復制二進制文件。如果用"r""w"來代替,那么程序將無法復制二進制文件。


22.5 行的輸入/輸出

下面將介紹讀和寫行的庫函數。雖然這些函數也可有效地用于二進制的流,但是它們多數用于文本流


22.5.1 輸出函數

int fputs(const char * restrict s, FILE * restrict stream); 
int puts(const char *s);

我們在13.3節已經見過puts函數,它是用來向標準輸出流stdout寫入字符串的:

puts("Hi, there!"); /* writes to stdout */

在寫入字符串中的字符以后,puts函數總會添加一個換行符。

fputs函數是puts函數的更通用版本。此函數的第二個實參指明了輸出要寫入的流:

fputs("Hi, there!", fp);  /* writes to fp */

不同于puts函數,fputs函數不會自己寫入換行符,除非字符串中本身含有換行符。

當出現寫錯誤時,上面這兩種函數都會返回EOF。否則,它們都會返回一個非負的數。


22.5.2 輸入函數

char *fgets(char * restrict s, int n, FILE * restrict stream); 

13.3節中已經見過在新標準中廢棄的gets函數了。

fgets函數是gets函數的更通用版本,它可以從任意流中讀取信息。fgets函數也比gets函數更安全,因為它會限制將要存儲的字符的數量。下面是使用fgets函數的方法,假設str是字符數組的名字:

fgets(str, sizeof(str), fp); /* reads a line from fp */

此調用將導致fgets函數逐個讀入字符,直到遇到首個換行符時或者已經讀入了sizeof(str)-1個字符時結束操作,這兩種情況哪種先發生都可以。如果fgets函數讀入了換行符,那么它會把換行符和其他字符一起存儲。(因此,gets函數從來不存儲換行符,而fgets函數有時會存儲換行符。)

如果出現了讀錯誤,或者是在存儲任何字符之前達到了輸入流的末尾,那么gets函數和fgets函數都會返回空指針。(通常,可以使用feof函數或ferror函數來確定出現的是哪種情況。)否則,兩個函數都會返回自己的第一個實參(指向保存輸入的數組的指針)。與預期一樣,兩個函數都會在字符串的末尾存儲空字符。

現在已經學習了fgets函數,那么建議大家用fgets函數來代替gets函數。對于gets函數而言,接收數組的下標總有可能越界,所以只有在保證讀入的字符串正好適合數組大小時使用gets函數才是安全的。在沒有保證的時候(通常是沒有的),使用fgets函數要安全得多。注意!!如果把stdin作為第三個實參進行傳遞,那么fgets函數就會從標準輸入流中讀取:

fgets(str, sizeof(str), stdin);

22.6 塊的輸入/輸出

size_t fread(void * restrict ptr, size_t size, size_t nmemb, FILE * restrict stream); 
size_t fwrite(const void * restrict ptr, size_t size, size_t nmemb, FILE * restrict stream);

fread函數和fwrite函數允許程序在單步中讀和寫大的數據塊。如果小心使用,fread函數和fwrite函數可以用于文本流,但是它們主要還是用于二進制的流

fwrite函數用來把內存中的數組復制給流。fwrite函數調用中第一個參數是數組的地址,第二個參數是每個數組元素的大小(以字節為單位),第三個參數是要寫的元素數量,第四個參數是文件指針,此指針說明了要寫的數據位置。例如,為了寫整個數組a的內容,就可以使用下列fwirte函數調用:

fwrite(a, sizeof(a[0]), sizeof(a) / sizeof(a[0]), fp); 

沒有規定必須寫入整個數組,數組任何區間的內容都可以輕松地寫入。fwrite函數返回實際寫入的元素(不是字節)的數量。如果出現寫入錯誤,那么此數就會小于第三個實參。

fread函數將從流讀入數組的元素。fread函數的參數類似于fwrite函數的參數:數組的地址、每個元素的大小(以字節為單位)、要讀的元素數量以及文件指針。為了把文件的內容讀入數組a,可以使用下列fread函數調用:

n = fread(a, sizeof(a[0]), sizeof(a) / sizeof(a[0]), fp);

檢查fread函數的返回值是非常重要的。此返回值說明了實際讀的元素(不是字節)的數量。此數應該等于第三個參數,除非達到了輸入文件末尾或者出現了錯誤。可以用feof函數和ferror函數來確定出問題的原因。

請注意!!不要把fread函數的第二個參數和第三個參數搞混了。思考下面這個fread函數調用:

fread(a, 1, 100, fp);

這里要求fread函數讀入100個元素,且每個元素占1字節,所以它返回0~100范圍內的某個值。下面的調用則要求fread函數讀入一個有100字節的塊:

fread(a, 100, 1, fp);

此情況中fread函數的返回值不是0就是1

當程序需要在終止之前把數據存儲到文件中時,使用fwrite函數是非常方便的。以后程序(或者另外的程序)可以使用fread函數把數據讀回內存中來。不考慮形式的話,數據不一定要是數組格式的。fread函數和fwrite函數都可以用于所有類型的變量,特別是可以用fread函數讀結構或者用fwrite函數寫結構。例如,為了把結構變量s寫入文件,可以使用下列形式的fwrite函數調用:

fwrite(&s, sizeof(s), 1, fp);

請注意!!使用fwrite輸出包含指針值的結構時需要小心。讀回時不能保證這些值一定有效。


22.7 文件定位

int fgetpos(FILE * restrict stream, fpos_t * restrict pos); 
int fseek(FILE *stream, long int offset, int whence); 
int fsetpos(FILE *stream, const fpos_t *pos); 
long int ftell(FILE *stream); 
void rewind(FILE *stream);

每個流都有相關聯的文件位置(file position)。打開文件時,會將文件位置設置在文件的起始處。(但如果文件按“追加”模式打開,初始的文件位置可以在文件起始處,也可以在文件末尾,這依賴于具體的實現。)然后,在執行讀或寫操作時,文件位置會自動推進,并且允許按照順序貫穿整個文件。

雖然對許多應用程序來說順序訪問是很好的,但是某些程序需要具有在文件中跳躍的能力,即可以在這里訪問一些數據,然后到別處訪問其他數據。例如,如果文件包含一系列記錄,我們可能希望直接跳到特定的記錄處,并對其進行讀或更新。<stdio.h>通過提供5個函數來支持這種形式的訪問,這些函數允許程序確定當前的文件位置或者改變文件的位置。

fseek函數改變與第一個參數(即文件指針)相關的文件位置。第三個參數說明新位置是根據文件的起始處、當前位置還是文件末尾來計算。<stdio.h>為此定義了3種宏:

  • SEEK_SET:文件的起始處。
  • SEEK_CUR:文件的當前位置。
  • SEEK_END:文件的末尾處。

第二個參數是個(可能為負的)字節計數。例如,為了移動到文件的起始處,搜索的方向將為SEEK_SET,而且字節計數為0

fseek(fp, 0L, SEEK_SET);   /* moves to beginning of file */

為了移動到文件的末尾,搜索的方向應該是SEEK_END

fseek(fp, 0L, SEEK_END);   /* moves to end of file */

為了往回移動10個字節,搜索的方向應該是SEEK_CUR,并且字節計數為-10

fseek(fp, -10L, SEEK_CUR);  /* moves back 10 bytes */

注意!!字節計數是long int類型的,所以這里用0L-10L作為實參。(當然,用0-10也可以,因為參數會自動轉換為正確的類型。)

通常情況下,fseek函數返回0。如果產生錯誤(例如,要求的位置不存在),那么fseek函數就會返回非零值。

順便提一句,文件定位函數最適用于二進制流。C語言不禁止程序對文本流使用這些定位函數,但考慮到操作系統的差異,要小心使用fseek函數對流是文本的還是二進制的很敏感。對于文本流而言,要么offsetfseek的第二個參數)必須為0,要么whencefseek的第三個參數)必須是SEEK_SET,且offset的值通過前面的ftell函數調用獲得。(換句話說,我們只可以利用fseek函數移動到文件的起始處或者文件的末尾處,或者返回前面訪問過的位置。)對于二進制流而言,fseek函數不要求支持whenceSEEK_END的調用。

ftell函數以長整數返回當前文件位置。[如果發生錯誤,ftell函數會返回-1L,并且把錯誤碼存儲到errno(24.2節)中。]ftell可能會存儲返回的值并且稍后將其提供給fseek函數調用,這也使返回前面的文件位置成為可能:

long file_pos; 
... 
file_pos = ftell(fp);    /* saves current position */ 
... 
fseek(fp, file_pos, SEEK_SET);  /* returns to old position */ 

如果fp是二進制流,那么ftell(fp)調用會以字節計數來返回當前文件位置,其中0表示文件的起始處。但是,如果fp是文本流,ftell(fp)返回的值不一定是字節計數,因此最好不要對ftell函數返回的值進行算術運算。例如,為了查看兩個文件位置的距離而把ftell返回的值相減不是個好做法。

rewind函數會把文件位置設置在起始處。調用rewind(fp)幾乎等價于fseek(fp, 0L, SEEK_SET),兩者的差異是rewind函數不返回值,但會為fp清除錯誤指示器。

fseek函數和ftell函數都有一個問題:它們只能用于文件位置可以存儲在長整數中的文件。為了用于非常大的文件,C語言提供了另外兩個函數:fgetpos函數和fsetpos函數。這兩個函數可以用于處理大型文件,因為它們用fpos_t類型的值來表示文件位置。fpos_t類型值不一定就是整數,比如,它可以是結構。

調用fgetpos(fp, &file_pos)會把與fp相關的文件位置存儲到file_pos變量中。調用fsetpos(fp, &file_pos)會為fp設置文件的位置,此位置是存儲在file_pos中的值。(此值必須通過前面的fgetpos調用獲得。)如果fgetpos函數或者fsetpos函數調用失敗,那么都會把錯誤碼存儲到errno中。當調用成功時,這兩個函數都會返回0;否則,都會返回非零值。

下面是使用fgetpos函數和fsetpos函數保存文件位置并且稍后返回該位置的方法:

fpos_t file_pos; 
... 
fgetpos(fp, &file_pos);  /* saves current position */ 
... 
fsetpos(fp, &file_pos);  /* returns to old position */

22.7.1 程序——修改零件記錄文件

下面這個程序打開包含part結構的二進制文件,把結構讀到數組中,把每個結構的成員on_hand置為0,然后再把此結構寫回到文件中。注意,程序用"rb+"模式打開文件,因此既可讀又可寫:

/*
invclear.c
--Modifies a file of part records by setting the quantity 
on hand to zero for all records
*/
#include <stdio.h> 
#include <stdlib.h> #define NAME_LEN 25 
#define MAX_PARTS 100 struct part { int number; char name[NAME_LEN+1]; int on_hand; 
} inventory[MAX_PARTS]; int num_parts; int main(void) 
{ FILE *fp; int i; if ((fp = fopen("inventory.dat", "rb+")) == NULL) { fprintf(stderr,"Can’t open inventory file\n"); exit(EXIT_FAILURE); } num_parts = fread(inventory, sizeof(struct part), MAX_PARTS, fp); for (i = 0; i < num_parts; i++) inventory[i].on_hand = 0; rewind(fp);fwrite(inventory, sizeof(struct part), num_parts, fp); fclose(fp); return 0; 
} 

順便說一下,這里調用rewind函數是很關鍵的。在調用完fread函數之后,文件位置是在文件的末尾。如果沒有先調用rewind函數,就調用fwrite函數,那么fwrite函數將在文件末尾添加新數據,而不會覆蓋舊數據。


22.8 字符串的輸入/輸出

本節里描述的函數有一點不同,因為它們與數據流或文件并沒有什么關系。相反,它們允許我們使用字符串作為流讀寫數據sprintfsnprintf函數將按和寫到數據流一樣的方式寫字符到字符串,sscanf函數從字符串中讀出數據就像從數據流中讀數據一樣。這些函數非常類似于printfscanf函數,也都是非常有用的。sprintfsnprintf函數可以讓我們使用printf的格式化能力,不需要真的往流中寫入數據。類似地,sscanf函數也可以讓我們使用scanf函數強大的模式匹配能力。下面將詳細講解sprintfsnprintfsscanf函數。

3個相似的函數(vsprintfvsnprintfvsscanf)也屬于<stdio.h>頭,但這些函數依賴于在<stdarg.h>中聲明的va_list類型。我們將推遲到26.1節討論該頭時再來介紹這3個函數。


22.8.1 輸出函數

int sprintf(char * restrict s, const char * restrict format, ...); 
int snprintf(char *restrict s, size_t n, const char * restrict format, ...); //C99新增

sprintf函數類似于printf函數和fprintf函數,唯一的不同就是sprintf函數把輸出寫入(第一個實參指向的)字符數組而不是流中。sprintf函數的第二個參數是格式串,這與printf函數和fprintf函數所用的一樣。例如,函數調用

sprintf(date, "%d/%d/%d", 9, 20, 2010); 

會把"9/20/2010"復制到date中。當完成向字符串寫入的時候,sprintf函數會添加一個空字符,并且返回所存儲字符的數量(不計空字符)。如果遇到錯誤(寬字符不能轉換成有效的多字節字符),sprintf返回負值。

sprintf函數有著廣泛的應用。例如,有些時候可能希望對輸出數據進行格式化,但不是真的要把數據寫出。這時就可以使用sprintf函數來實現格式化,然后把結果存儲在字符串中,直到需要產生輸出的時候再寫出。sprintf函數還可以用于把數轉換成字符格式。

snprintf函數與sprintf一樣,但多了一個參數n。寫入字符串的字符不會超過n-1,結尾的空字符不算;只要n不是0,就會有空字符。(我們也可以這樣說:snprintf最多向字符串中寫入n個字符,最后一個是空字符。)例如,函數調用

snprintf(name, 13, "%s, %s", "Einstein", "Albert");

會把"Einstein, Al"寫入到name中。

如果沒有長度限制,snprintf函數返回需要寫入的字符數(不包括空字符)。如果出現編碼錯誤,snprintf函數返回負值。為了查看snprintf函數是否有空間寫入所有要求的字符,可以測試其返回值是否非負且小于n


22.8.2 輸入函數

int sscanf(const char * restrict s, const char * restrict format, ... ); 

sscanf函數與scanf函數和fscanf函數都很類似,唯一的不同就是sscanf函數是從(第一個參數指向的)字符串而不是流中讀取數據。sscanf函數的第二個參數是格式串,這與scanf函數和fscanf函數所用的一樣。

sscanf函數對于從由其他輸入函數讀入的字符串中提取數據非常方便。例如,可以使用fgets函數來獲取一行輸入,然后把此行數據傳遞給sscanf函數進一步處理:

fgets(str, sizeof(str), stdin);  /* reads a line of input */ 
sscanf(str, "%d%d", &i, &j);   /* extracts two integers */ 

sscanf函數代替scanf函數或者fscanf函數的好處之一就是,可以按需多次檢測輸入行,而不再只是一次,這樣使識別替換的輸入格式和從錯誤中恢復都變得更加容易了。下面思考一下讀取日期的問題。讀取的日期既可以是月/日/年的格式,也可以是月-日-年的格式。假設str包含一行輸入,那么可以按如下方法提取出月、日和年的信息:

if (sscanf(str, "%d /%d /%d", &month, &day, &year) == 3) printf("Month: %d, day: %d, year: %d\n", month, day, year); 
else if (sscanf(str, "%d -%d -%d", &month, &day, &year) == 3) printf("Month: %d, day: %d, year: %d\n", month, day, year); 
else printf("Date not in the proper form\n");

scanf函數和fscanf函數一樣,sscanf函數也返回成功讀入并存儲的數據項的數量。如果在找到第一個數據項之前到達了字符串的末尾(用空字符標記),那么sscanf函數會返回EOF


問與答

問1:如果我使用輸入重定向或輸出重定向,那么重定向的文件名會作為命令行參數顯示出來嗎?

答:不會。操作系統會把這些文件名從命令行中移走。假設用下列輸入運行程序:

demo foo <in_file bar >out_file baz 

argc的值為4argv[0]將指向程序名,argv[1]會指向"foo"argv[2]會指向"bar"argv[3]會指向"baz"

問2:我一直認為行的末尾都是以換行符標記的,現在你說行末標記根據操作系統的不同而不同。如何解釋這種差異呢?

答:C庫函數使得每一行看起來都是以一個換行符結束的。不管輸入文件有回車符、回行符,還是兩者都有,getc等庫函數都只會返回一個換行符。輸出函數執行相反的操作。如果程序調用庫函數向文件中輸出換行符,函數會把該字符轉換成恰當的行末標記。C語言的這種實現使得程序的可移植性更好,也更易編寫。我們處理文本文件時不需要擔心行的末尾到底是怎么表示的。注意,對以二進制模式打開的文件進行輸入/輸出操作時,不需要進行字符轉換——回車符、回行符跟其他字符同等對待。

問3:我正打算編寫一個需要在文件中存儲數據的程序,該文件可供其他程序讀取。就數據的存儲格式而言,文本格式和二進制格式哪種更好呢?

答:這要看情況。如果數據全部是文本,那么用哪種格式存儲沒有太大的差異。然而,如果數據包含數,那么決定就比較困難一些了。

通常二進制格式更可取,因為此種格式的讀和寫都非常快。當存儲到內存中時,數已經是二進制格式了,所以將它們復制給文件是非常容易的。用文本格式寫數據相對就會慢許多,因為每個數必須要轉換成字符格式(通常用fprintf函數)。以后讀取文件同樣要花費更多的時間,因為必須要把數從文本格式轉換回二進制格式。此外,就像在22.1節看到的那樣,以二進制格式存儲數據常常能節省空間。

然而,二進制文件有兩個缺點。一是很難閱讀,這也就妨礙了調試過程;二是二進制文件通常無法從一個系統移植到另一個系統,因為不同類型的計算機存儲數據的方式是不同的。比如,有些機器用2字節存儲整數,而有些機器則用4字節來存儲。字節順序(大端/小端)也是一個問題。

問4:用于UNIX系統的C程序好像從不在模式字符串中使用字母b,即使待打開的文件是二進制格式也是如此。這是什么原因呢?

答:在UNIX系統中,文本文件和二進制文件具有完全相同的格式,所以不需要使用字母b。但是,UNIX程序員仍應該包含字母b,這樣他們的程序將更容易移植到其他操作系統上。

問5:我已經看過調用fopen函數并且把字母t放在模式字符串中的程序了。字母t意味著什么呢?

答:C標準允許其他的字符在模式字符串中出現,但是它們要跟在rwab+的后邊。有些編譯器允許使用t來說明待打開的文件是文本模式而不是二進制模式。當然,無論如何文本模式都是默認的,所以字母t沒有任何作用。在可能的情況下,最好避免使用字母t和其他不可移植的特性

問6:為什么要調用fclose函數來關閉文件呢?當程序終止時,所有打開的文件都會自動關閉,難道不是這樣嗎?

答:通常情況下是這樣的,但如果調用abort函數(26.2節)來終止程序就不是了。即使在不用abort函數的時候,調用fclose函數仍有許多理由。首先,這樣會減少打開文件的數量。操作系統對程序每次可以打開的文件數量有限制,而大規模的程序可能會與此種限制相沖突。(定義在<stdio.h>中的宏FOPEN_MAX指定了可以同時打開的文件的最少數量。)其次,這樣做使程序更易于理解和修改。通過尋找fclose函數,讀者更容易確定不再使用此文件的位置。最后,這樣做很安全。關閉文件可以確保正確地更新文件的內容和目錄項。如果將來程序崩潰了,至少該文件不會受到影響。

問7:我正在編寫的程序會提示用戶輸入文件的名字。我要設置多長的字符數組才可以存儲這個文件名字呢?

答:這與使用的操作系統有關。好在你可以使用宏FILENAME_MAX(定義在<stdio.h>中)來指定數組的大小。FILENAME_MAX是字符串的長度,這個字符串用于存儲保證可以打開的最長的文件名。

問8fflush可以清除同時為讀和寫而打開的流嗎?

答:根據C標準,當流(1)為輸出打開,或者(2)為更新打開并且最后一個操作不是讀時,調用fflush的結果才有定義。在其他所有情況下,調用fflush函數的結果是未定義的。當傳遞空指針給fflush函數時,它會清除所有滿足(1)(2)的流。

問9:在...printf函數或...scanf函數調用中,格式串可以是變量嗎?

答:當然。它可以是char *類型的任意表達式。這個性質使...printf函數和...scanf函數比我們想象的更加多樣。請看下面這個來自KernighanRitchie所著的《C程序設計語言》一書的經典示例。此示例顯示程序的命令行參數,以空格分隔:

while (--argc > 0)  printf((argc  >  1)  ?  "%s " : "%s", *++argv);

這里的格式串是表達式(argc > 1) ? "%s " : "%s",其結果是除了最后一個參數以外,對其他所有命令行參數都會使用"%s "

問10:除了clearerr函數,哪些庫函數可以清除流的錯誤指示器和文件末尾指示器?

答:調用rewind函數可以清除這兩種指示器,就好像打開或重新打開流一樣;調用ungetc函數、fseek函數或者fsetpos函數僅可以清除文件末尾指示器。

問11:我無法使feof函數工作。這是因為即使到了文件末尾,它好像還是返回0。我做錯了什么嗎?

答:當前面的讀操作失敗時,feof函數只會返回一個非零值。在嘗試讀之前,不能使用feof函數來檢查文件末尾。相反,你應該首先嘗試讀,然后檢查來自輸入函數的返回值。如果返回的值表明操作不成功,那么你可以隨后使用feof函數來確定失敗是不是因為到了文件末尾。換句話說,最好不要認為調用feof函數是檢測文件末尾的方法,而應把它看作確認讀取操作失敗是因為到了文件末尾的方法

問12:我始終不明白為什么輸入/輸出庫除了提供名為fputcfgetc的函數以外,還提供名為putcgetc的宏。依據21.1節的介紹,putcgetc已經有兩種版本了(宏和函數)。如果需要真正的函數而不是宏,我們可以通過取消宏的定義來顯示putc函數或getc函數。那么,為什么要有fputcfgetc存在呢?

答:這是歷史原因造成的。在標準化以前,C語言沒有規則要求用真正的函數在庫中備份每個帶參數的宏。putc函數和getc函數傳統上只作為宏來實現,而fputc函數和fgetc函數則只作為函數來實現。

問13:把fgetc函數、getc函數或者getchar函數的返回值存儲到char類型變量中會有什么問題?我不明白為什么判斷char類型變量的值是否為EOF會得到錯誤的結果。

答:有兩種情況可能導致該判定得出錯誤的結果。為了使下面的討論更具體,這里假設使用二進制補碼存儲方式。

首先,假定char類型是無符號類型。(回憶一下,有些編譯器把char作為有符號類型來處理,而有些編譯器則把它看成無符號類型的。)現在假設getc函數返回EOF,把該返回值存儲在名為chchar類型變量中。如果EOF表示-1(通常如此),那么ch的值將為255。把ch(無符號字符)與EOF(有符號整數)進行比較就要求把ch轉換為有符號整數(在這個例子中是255)。因為255不等于-1,所以與EOF的比較失敗了。

反之,現在假設char是有符號類型。如果getc函數從二進制流中讀取了一個含有值255的字節,這樣會產生什么情況呢?因為ch是有符號字符,所以把255存儲在char類型變量中將為它賦值-1。如果判斷ch是否等于EOF,則會(錯誤地)產生真結果。

問1422.4節描述的字符輸入函數要求在讀取用戶輸入之前看到回車鍵。如何編寫能直接響應鍵盤輸入的程序?

答:我們注意到,getcfgetcgetchar都是分配緩沖區的,這些函數在用戶按下回車鍵時才開始讀取輸入。為了實時讀取鍵盤輸入(這對某些類型的程序很重要),需要使用適合你的操作系統的非標準庫。例如,UNIX中的curses庫通常提供這一功能。

問15:正在讀取用戶輸入時,如何跳過當前輸入行中剩下的全部字符呢?

答:一種可能是編寫一個小函數來讀入并且忽略第一個換行符之前的所有字符(包含換行符):

void skip_line(void) 
{ while (getchar() != '\n') ; 
}

另外一種可能是要求scanf函數跳過第一個換行符前的所有字符:

scanf("%*[^\n]");  /* skips characters up to new-line */ 

scanf函數將讀取第一個換行符之前的所有字符,但是不會把它們存儲下來(*表示賦值屏蔽)。使用scanf函數的唯一問題是它會留下換行符不讀,所以可能需要單獨丟棄換行符。

無論做什么,都不要調用fflush函數:

fflush(stdin); /* effect is undefined */

雖然某些實現允許使用fflush函數來“清洗”未讀取的輸入,但是這樣做并不好。fflush函數是用來清洗輸出流的。C標準規定fflush函數對輸入流的效果是未定義的

問16:為什么把fread函數和fwrite函數用于文本流是不好的呢?

答:困難之一是,在某些操作系統中對文本文件執行寫操作時,會把換行符變成一對字符(詳細內容見22.1節)。我們必須考慮這種擴展,否則就很可能搞錯數據的位置。例如,如果使用fwrite函數來寫含有80個字符的塊,因為換行符可能被擴展,所以有些塊可能會占用多于80字節的空間。

問17:為什么有兩套文件定位函數(即fseek/ftellfsetpos/fgetpos)呢?一套函數難道不夠嗎?

答:fseek函數和ftell函數作為C庫的一部分已有些年頭了,但它們有一個缺點:它們假定文件位置能夠用long int類型的值表示。由于long int通常是32位的類型,當文件大小超過2147483647字節時,fseek函數和ftell函數可能無法使用。針對這個問題,創建C89標準時在<stdio.h>中增加了fsetposfgetpos。這兩個函數不要求把文件位置看作數,因此就沒有long int的限制了。但是也不要認為必須使用fsetposfgetpos,如果你的實現支持64位的long int類型,即使對很大的文件也可以使用fseekftell

問18:為什么本章不討論屏幕控制,即移動光標、改變屏幕上字符顏色等呢?

答:C語言沒有提供用于屏幕控制的標準函數。標準只發布那些通過廣泛的計算機和操作系統可以合理標準化的問題,而屏幕控制超出了這個范疇。在UNIX中解決這個問題的習慣做法是使用curses庫,這個庫支持不依賴終端方式的屏幕控制。

類似地,也沒有標準函數可以用來構建帶有圖形用戶界面的程序。不過,可以用C函數調用來訪問操作系統中的窗口API(應用程序接口)


寫在最后

本文是博主閱讀《C語言程序設計:現代方法(第2版·修訂版)》時所作筆記,日后會持續更新后續章節筆記。歡迎各位大佬閱讀學習,如有疑問請及時聯系指正,希望對各位有所幫助,Thank you very much!

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/165954.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/165954.shtml
英文地址,請注明出處:http://en.pswp.cn/news/165954.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Windows | 模仿網易云任務欄實現自定義按鈕及縮略圖

前言 最近更新網易云發現任務欄按鈕中除了播放相關的按鈕&#xff0c;多了一個喜歡的按鈕&#xff1a; 之前我一直以為網易云任務欄的按鈕只是 Windows 為音樂軟件專門提供的&#xff0c;于是我又看了一眼系統自帶的播放器&#xff0c;發現并沒有愛心按鈕&#xff1a; 這時我就…

計算給定字符串中各個數字的和的平均值…… ← Python 列表

【題目描述】 給定字符串 s"9876543210"。 請編程計算給定字符串中各個數字的和的平均值&#xff0c;并統計大于平均值的數字個數。【算法分析】 ◆ alist("abcd") # Create a list with characters a, b, c, d◆ eval(a[i]) # Converts characters to i…

C在國內就業已經拉胯,ChatGPT告訴我的

文章目錄 一、前言二、ChatGPT查到的數據三、數據亮點 1.C語言近3年數據大跌2.招聘數量每年都在劇增的是全棧工程師3.薪資漲幅最高的是全棧和網安 四、結語 一、前言 不僅前在微信群里搭建了一個ChatGPT 5.0做智能助手&#xff0c;讓他來幫我回答群問題&#xff0c; 搭建好…

數十億美元商機!英國數字基礎設施公司Equinix與法國量子計算公司Alice Bob 合作

?&#xff08;圖片來源&#xff1a;網絡&#xff09; 近日&#xff0c;全球數字基礎設施公司Equinix宣布與全球領先的法國量子計算公司Alice & Bob合作&#xff0c;旨在共同開發市場上最為可靠的量子處理器之一。此次合作將使Equinix公司的客戶通過使用Equinix Metal和Eq…

好的程序員有什么特質呢?

程序員想要提升自己&#xff0c;一定要關注到工作中的方方面面。而一個好的程序員&#xff0c;一般都有這些特質&#xff1a; 弱者抱怨環境&#xff0c;強者改變環境 不要試圖通過抱怨環境來獲得工作環境上的改變&#xff0c;這不僅不會給你帶來任何實質性的改變&#xff0c;…

自定義字符-攝氏度漢字一

本文為博主 日月同輝&#xff0c;與我共生&#xff0c;csdn原創首發。希望看完后能對你有所幫助&#xff0c;不足之處請指正&#xff01;一起交流學習&#xff0c;共同進步&#xff01; > 發布人&#xff1a;日月同輝,與我共生_單片機-CSDN博客 > 歡迎你為獨創博主日月同…

springboot+vue項目如何集成onlyoffice開源文檔組件

一、onlyoffice是什么 ONLYOFFICE 是一個開源的辦公套件&#xff0c;適合多人在線協作。由總部位于總部在拉脫維亞的 IT 公司Acensio System SIA 開發。它提供在線協作文檔編輯器&#xff08;包括文檔、電子表格、演示文稿和表單&#xff09;&#xff0c;適用于 Windows、Linu…

python tkinter使用(五)

python tkinter使用(五) 本篇文章講述tkinter 中treeview的使用 Treeview是一個多列列表框&#xff0c;可以顯示層次數據。 #!/usr/bin/python3 # -*- coding: UTF-8 -*- """Author: zhTime 2023/11/23 下午8:28 .Email:Describe: treeview 使用 "&quo…

項目經理面試題持續更新

1.項目中常用的文檔有哪些&#xff1f; 1、可行性報告 可行性報告的目的是調查和展示任務要求&#xff0c;并確定項目是否值得和可行。可行性由五個主要因素驗證——技術和系統、成本、法律、運營和進度。次要可行性因素包括市場、資源和文化因素。 2、項目章程 項目章程是證明…

Linux上自動掛載windows下的網絡共享文件夾

比如我們想在ubuntu上掛載一個windows的共享文件夾&#xff0c;我們可以用如下方式實現。 首先我們將windows下的文件夾右鍵選擇【屬性】&#xff0c;然后選擇【共享】。 選擇需要共享的用戶&#xff0c;然后設置權限級別。 點擊共享&#xff0c;然后點擊完成。 這樣我們在wi…

Go語言網絡爬蟲工程經驗分享:pholcus庫演示抓取頭條新聞的實例

網絡爬蟲是一種自動從互聯網上獲取數據的程序&#xff0c;它可以用于各種目的&#xff0c;如數據分析、信息檢索、競爭情報等。網絡爬蟲的實現方式有很多&#xff0c;不同的編程語言和框架都有各自的優勢和特點。在本文中&#xff0c;我將介紹一種使用Go語言和pholcus庫的網絡爬…

基于opencv+ImageAI+tensorflow的智能動漫人物識別系統——深度學習算法應用(含python、JS、模型源碼)+數據集(一)

目錄 前言總體設計系統整體結構圖系統流程圖 運行環境爬蟲1.安裝Anaconda2.安裝Python3.63.更換pip源4.安裝Python包5.下載phantomjs 模型訓練1.安裝依賴2.安裝lmageAl 實際應用1.前端2.安裝Flask3.安裝Nginx 相關其它博客工程源代碼下載其它資料下載 前言 本項目通過爬蟲技術…

Word怎么看字數?簡單教程分享!

“我在寫文章時&#xff0c;總是想看看寫了多少字。但是我發現我的Word無法看到字數。在Word中應該怎么查看字數呢&#xff1f;請幫幫我&#xff01;” Word是一個廣泛使用的文檔編輯工具。在我們編輯文章時&#xff0c;如果想查看寫了多少字&#xff0c;也是可以輕松完成的。 …

leetcode:環形鏈表的入環點

題目描述 題目鏈接:力扣&#xff08;LeetCode&#xff09;官網 - 全球極客摯愛的技術成長平臺 題目分析 我們假設起點到環的入口點的距離是L&#xff0c;入口點到相遇點的距離是X&#xff0c;環的長度是C 那么畫圖我們可以得知&#xff1a; 從開始到相遇時slow走的距離是LX從…

Adobe的組織工具程序Bridge 2024 版本下載與安裝

目錄 前言一、Bridge 2024安裝二、使用配置總結 前言 Adobe Bridge是由 Adobe 公司開發的一款用于管理和組織創意資產的工具。它是Adobe Creative Cloud 套件的一部分&#xff0c;為設計師、攝影師和其他創意專業人員提供了一個集中管理和瀏覽其多媒體文件的平臺。注&#xff…

Ubuntu開機顯示No bootable devices found

Ubuntu開機報錯&#xff0c;顯示顯示No bootable devices found&#xff0c;如下圖所示&#xff1a; 解決方案如下&#xff1a; 1. F2進入BIOS (1) 重啟開啟&#xff0c;按F2進入BIOS系統。 (2) 進入Boot Sequence&#xff0c;目前系統選擇了UEFI&#xff0c;而Legacy選項為…

Android : AlertDialog對話框、單選、多選、適配器-簡單應用

示例圖&#xff1a; 1 &#xff1a;創建 AlertDialog.Builder 對象&#xff1b; 2 &#xff1a;調用 setIcon() 設置圖標&#xff0c; setTitle() 或 setCustomTitle() 設置標題&#xff1b; 3 &#xff1a;設置對話框的內容&#xff1a; setMessage() 還有其他方法來指定顯示…

【每日一題】2824. 統計和小于目標的下標對數目-2023.11.24

題目&#xff1a; 2824. 統計和小于目標的下標對數目 給你一個下標從 0 開始長度為 n 的整數數組 nums 和一個整數 target &#xff0c;請你返回滿足 0 < i < j < n 且 nums[i] nums[j] < target 的下標對 (i, j) 的數目。 示例 1&#xff1a; 輸入&#xff1…

雙12電視盒子什么牌子好?數碼小編力薦目前最強的電視盒子

最近想買電視盒子的網友非常多&#xff0c;小編收到了很多關于電視盒子方面的咨詢&#xff0c;因此我特意整理了今年測評過的電視盒子&#xff0c;總結了五款目前最強的電視盒子&#xff0c;想知道雙十二買電視盒子什么牌子好就趕緊收藏起來吧。 推薦一&#xff1a;泰捷WEBOX新…

01 LM 算法及 Cpp 實現

文章目錄 01 LM 算法及 Cpp 實現1.1 應用1.2 阻尼法推導1.3 Cpp 算法實現 01 LM 算法及 Cpp 實現 1.1 應用 LM 算法用于解決非線性最小二乘問題 min ? x F ( x ) 1 2 ∥ f ( x ) ∥ 2 2 (1) \min _x F(x)\frac{1}{2}\|f(\boldsymbol{x})\|_2^2 \tag{1} xmin?F(x)21?∥f(x…