文章目錄
- 第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
個函數(vfprintf
、vprintf
、vsprintf
、vsnprintf
、vfscanf
、vscanf
和vsscanf
)。這些函數依賴于va_list
類型,該類型在26.1節
介紹。在
C89
中,所有的標準輸入/輸出函數都屬于<stdio.h>
。但從C99
開始有所不同,有些輸入/輸出函數在<wchar.h>頭(25.5節)
中聲明。<wchar.h>
中的函數用于處理寬字符而不是普通字符,但大多數函數與<stdio.h>
中的函數緊密相關。<stdio.h>
中用于讀或寫數據的函數稱為字節輸入/輸出函數,而<wchar.h>
中的類似函數則稱為寬字符輸入/輸出函數。
22.1 流
在
C
語言中,術語流(stream)
表示任意輸入的源或任意輸出的目的地。許多小型程序(就像前面章節中介紹的那些)都是通過一個流(通常和鍵盤
相關)獲得全部的輸入,并且通過另一個流(通常和屏幕
相關)寫出全部的輸出。
較大規模的程序可能會需要額外的流。這些流常常表示存儲在不同介質(如硬盤驅動器、CD
、DVD
和閃存)上的文件,但也很容易和不存儲文件的設備(如網絡端口、打印機等)相關聯。這里將集中討論文件,因為它們常見且容易理解。但是,請千萬記住一點:<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 | 標準誤差 | 屏幕 |
前面章節使用過的函數(printf
、scanf
、putchar
、getchar
、puts
和gets
)都是通過stdin
獲得輸入,并且用stdout
進行輸出的。默認情況下,stdin
表示鍵盤,stdout
和stderr
表示屏幕。然而,許多操作系統允許通過一種稱為重定向(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')
。在UNIX
和Macintosh
操作系統(Mac OS
)的較新版本中,行末的標記是一個單獨的回行符。舊版本的Mac OS
使用一個單獨的換行符。 - 文本文件可以包含一個特殊的“文件末尾”標記。一些操作系統允許在文本文件的末尾使用一個特殊的字節作為標記。在
Windows
中,標記為'\x1a'(Ctrl+Z)
。Ctrl+Z
不是必需的,但如果存在,它就標志著文件的結束,其后的所有字節都會被忽略。使用Ctrl+Z
的這一習慣繼承自DOS
,而DOS
中的這一習慣又是從CP/M
(早期用于個人計算機的一種操作系統)來的。大多數其他操作系統(包括UNIX
)沒有專門的文件末尾字符。
二進制文件不分行
,也沒有行末標記和文件末尾標記
,所有字節都是平等對待
的。
向文件寫入數據時,我們需要考慮是按文本格式存儲還是按二進制格式來存儲。為了搞清楚其中的差別,考慮在文件中存儲數
32767
的情況。一種選擇是以文本的形式把該數按字符3
、2
、7
、6
、7
寫入。假設字符集為ASCII
,那么就可以得到下列5
個字節:
00110011 | 00110010 | 00110111 | 00110110 | 00110111 |
---|---|---|---|---|
‘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
開始引入的關鍵字,表明filename
和mode
所指向的字符串的內存單元不共享。C89
中的fopen
原型不包含restrict
,但也有這樣的要求。restrict
對fopen
的行為沒有影響,因此通常可以忽略。
請注意!!提醒
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
函數的常見調用形式如下所示,其中fp
是FILE*
類型的變量:
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]
是空指針。在上述例子中,argc
是3
,argv[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
,分析如下:
標志 | 最小欄寬 | 精度 | 長度指定符 | 轉換指定符 | |
---|---|---|---|---|---|
% | #0 | 12 | .5 | L | g |
下面對上述這些選項進行詳細的描述,選項的順序必須與上面一致:
- 標志(可選項,允許多于一個)。標志
—
導致在欄內左對齊,而其他標志則會影響數的顯示形式。表22-4
給出了標志的完整列表。
表22-4 用于…printf函數的標志
標志 | 含義 |
---|---|
- | 在欄內左對齊(默認右對齊) |
+ | 有符號轉換得到的數總是以+ 或- 開頭(通常,只有負數前面附上- ) |
空格 | 有符號轉換得到的非負數前面加空格(+ 標志優先于空格標志) |
# | 以0 開頭的八進制數,以0x 或0X 開頭的十六進制非零數。浮點數始終有小數點。不能刪除由g 或G 轉換輸出的數的尾部零 |
0(零) | 用前導零在數的欄寬內進行填充。如果轉換是d、i、o、u、x 或X ,而且指定了精度,那么可以忽略標志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、X | signed char, unsigned char |
hh① | n | signed char * |
h | d、i、o、u、x、X | short int, unsigned short int |
h | n | short int * |
l(ell) | d、i、o、u、x、X | long int, unsigned long int |
l(ell) | n | long int * |
l(ell) | c | wint_t |
l(ell) | s | wchar_t * |
l(ell) | a、A、e、E、f、F、g、G | 無作用 |
ll①(ell-ell) | d、i、o、u、x、X | long long int, unsigned long long int |
ll①(ell-ell) | n | long long int * |
j① | d、i、o、u、x、X | intmax_t, uintmax_t |
j① | n | intmax_t * |
z① | d、i、o、u、x、X | size_t |
z① | n | size_t * |
t① | d、i、o、u、x、X | ptrdiff_t |
t① | n | ptrdiff_t * |
L | a、A、e、E、f、F、g、G | long double |
①僅C99
及之后的標準才有。
- 轉換指定符。轉換指定符必須是
表22-6
中列出的某一種字符。注意f、F、e、E、g、G、a
和A
全部設計用來輸出double
類型的值,但把它們用于float
類型的值也可以:由于有默認實參提升(9.3節)
,float
類型實參在傳遞給帶有可變數量實參的函數時會自動轉換為double
類型。類似地,傳遞給...printf
函數的字符也會自動轉換為int
類型,所以可以正常使用轉換指定符c
。
表22-6 …printf 函數的轉換指定符
轉換指定符 | 含義 |
---|---|
d、i | 把int 類型值轉換為十進制形式 |
o、u、x、X | 把無符號整數轉換為八進制(o) 、十進制(u) 或十六進制(x、X) 形式。x 表示用小寫字母a~f 來顯示十六進制數,X 表示用大寫字母A~F 來顯示十六進制數 |
f、F① | 把double 類型值轉換為十進制形式,并且把小數點放置在正確的位置上。如果沒有指定精度,那么在小數點后面顯示6 個數字 |
e、E | 把double 類型值轉換為科學記數法形式。如果沒有指定精度,那么在小數點后面顯示6 個數字。如果選擇e ,那么要把字母e 放在指數前面;如果選擇E ,那么要把字母E 放在指數前面 |
g、G | g 會把double 類型值轉換為f 形式或者e 形式。當數值的指數部分小于-4 ,或者指數部分大于等于精度值時,會選擇e 形式顯示。尾部的零不顯示(除非使用了# 標志),且小數點僅在后邊跟有數字時才顯示出來。G 會在F 形式和E 形式之間進行選擇 |
a①、A① | 使用格式[-]0xh.hhhhp±d 的格式把double 類型值轉換為十六進制科學記數法形式。其中[-] 是可選的負號,h 代表十六進制數位,± 是正號或者負號,d 是指數。d 為十進制數,表示2 的冪。如果沒有指定精度,在小數點后將顯示足夠的數位來表示準確的數值(如果可能的話)。a 表示用小寫形式顯示a~f ,A 表示用大寫形式顯示A~F 。選擇a 還是A 也會影響字母x 和p 的情況 |
c | 顯示無符號字符的int 類型值 |
s | 寫出由實參指向的字符。當達到精度值(如果存在)或者遇到空字符時,停止寫操作 |
p | 把void * 類型值轉換為可打印形式 |
n | 相應的實參必須是指向int 類型對象的指針。在該對象中存儲...printf 函數調用已經輸出的字符數量,不產生輸出 |
% | 寫字符% |
①僅C99
及之后的標準才有。
請注意!!請認真遵守上述規則。使用無效的轉換說明會導致未定義的行為。
22.3.3. C99對…printf轉化說明的修改(C99)
C99
對printf
函數和fprintf
函數的轉換說明做了不少修改:
-
增加了長度指定符。
C99
中增加了hh
、ll
、j
、z
和t
長度指定符。hh
和ll
提供了額外的長度選項,j
允許輸出最大寬度整數(27.1節)
,z
和t
分別使對size_t
和ptrdiff_t
類型值的輸出變得更方便了。 -
增加了轉換指定符。
C99
中增加了F、a
和A
轉換指定符。F
和f
一樣,區別在于書寫無窮數和NaN
(見下面的討論)的方式。a
和A
轉換指定符很少使用,它們和十六進制浮點常量相關,后者在第7章
末尾的“問與答”
部分討論過。 -
允許輸出無窮數和NaN。
IEEE 754
浮點標準允許浮點運算的結果為正無窮數
、負無窮數
或NaN(非數)
。例如,1.0
除以0.0
會產生正無窮數,-1.0
除以0.0
會產生負無窮數,而0.0
除以0.0
會產生NaN
(因為該結果在數學上是無定義的)。在C99
中,轉換指定符a、A、e、E、f、F、g
和G
能把這些特殊值轉換為可顯示的格式。a、e、f
和g
將正無窮數轉換為inf
或infinity
(都是合法的),將負無窮數轉換為-inf
或-infinity
,將NaN
轉換為nan
或-nan
(后面可能跟著一對圓括號,圓括號里面有一系列的字符)。A、E、F
和G
與a、e、f
和g
是等價的,區別僅在于使用大寫字母(INF
、INFINITY
、NAN
)。 -
支持寬字符。從
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 |
%-8d | 123????? | -123???? |
%+8d | ????+123 | ????-123 |
% 8d | ?????123 | ????-123 |
%08d | 00000123 | -0000123 |
%-+8d | +123???? | -123???? |
%- 8d | ?123???? | -123???? |
%+08d | +0000123 | -0000123 |
% 08d | ?0000123 | -0000123 |
表22-8
說明了標志#
作用于o
、x
、X
、g
和G
轉換的效果。
表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 | ?bogus | buzzword |
%-6s | bogus? | buzzword |
%.4s | bogu | buzz |
%6.4s | ??bogu | ??buzz |
%-6.4s | bogu?? | buzz?? |
表22-10
說明了%g
轉換如何以%e
和%f
的格式顯示數。表中的所有數都用轉換說明%.4g
進行了書寫。前兩個數的指數至少為4
,因此它們是按照%e
的格式顯示的。接下來的8
個數是按照%f
的格式顯示的。最后兩個數的指數小于-4
,所以也用%e
的格式來顯示。
表22-10 %g
轉換的示例
數 | 對數應用轉換%.4g的結果 |
---|---|
123456.00000000000 | 1.235e+05 |
12345.60000000000 | 1.235e+04 |
1234.56000000000 | 1235 |
123.45600000000 | 123.5 |
12.34560000000 | 12.35 |
1.23456000000 | 1.235 |
0.12345600000 | 0.1235 |
0.01234560000 | 0.01235 |
0.00123456000 | 0.001235 |
0.00012345600 | 0.0001235 |
0.00001234560 | 1.235e-05 |
0.00000123456 | 1.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
會讀取·
、1
、2
和3
,但是留下¤
不讀取。(這里使用·
表示空格符,用¤
表示換行符。) - 空白字符。
...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、n | signed char , unsigned char |
h | d、i、o、u、x、X、n | short int , unsigned short int |
l(ell) | d、i、o、u、x、X、n | long int , unsigned long int |
l(ell) | a、A、e、E、f、F、g、G | double * |
l(ell) | c、s、[ | wchar_t * |
ll①(ell-ell) | d、i、o、u、x、X、n | long long int , unsigned long long int |
j① | d、i、o、u、x、X、n | intmax_t , uintmax_t |
z① | d、i、o、u、x、X、n | size_t * |
t① | d、i、o、u、x、X、n | ptrdiff_t * |
L | a、A、e、E、f、F、g、G | long double * |
① 僅C99
及之后的標準才有。
- 轉換指定符。轉換指定符必須是
表22-12
中列出的某一種字符。
表22-12 用于...scanf
函數的轉換指定符
轉換指定符 | 含義 |
---|---|
d | 匹配十進制整數,假設相應的實參是int * 類型 |
i | 匹配整數,假設相應的實參是int * 類型。假定數是十進制形式的,除非它以0 開頭(說明是八進制形式),或者以0x 或0X 開頭(十六進制形式) |
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
及之后的標準才有。
數值型數據項可以始終用符號(+
或-
)作為開頭。然而,說明符o
、u
、x
和X
把數據項轉換成無符號的形式,所以通常不用這些說明符來讀取負數。
說明符[
是說明符s
更加復雜(且更加靈活)的版本。使用[
的完整轉換說明格式是%[集合]
或者%[^集合]
,這里的集合可以是任意字符集。(但是,如果]
是集合中的一個字符,那么它必須首先出現。)%[集合]
匹配集合(即掃描集合)中的任意字符序列。%[^集合]
匹配不在集合中的任意字符序列(換句話說,構成掃描集合的全部字符都不在集合中)。例如,%[abc]
匹配的是只含有字母a
、b
和c
的任何字符串,而%[^abc]
匹配的是不含有字母a
、b
或c
的任何字符串。
...scanf
函數的許多轉換指定符和<stdlib.h>
中的數值轉換函數(26.2節)
有著緊密的聯系。這些函數把字符串(如"-297"
)轉換成與其等價的數值(-297
)。例如,說明符d
尋找可選的+
號或-
號,后邊跟著一串十進制的數字。這樣就與把字符串轉換成十進制數的strtol
函數所要求的格式完全一樣了。表22-13
展示了轉換指定符和數值轉換函數之間的對應關系。
表22-13 ...scanf
轉換指定符和數值轉換函數之間的對應關系
轉換指定符 | 字符串轉換函數 |
---|---|
d | 10 作為基數的strtol 函數 |
i | 0 作為基數的strtol 函數 |
o | 8 作為基數的strtoul 函數 |
u | 10 作為基數的strtoul 函數 |
x、X | 16 作為基數的strtoul 函數 |
a、A、e、E、f、F、g、G | strtod 函數 |
請注意!!編寫
scanf
函數的調用時需要十分小心。scanf
格式串中無效的轉換說明就像printf
格式串中的無效轉換說明一樣糟糕,都會導致未定義的行為。
22.3.8 C99對…scanf轉換說明的改變(C99)
從
C99
開始的標準對scanf
和fscanf
的轉換說明做了一些改變,但沒有...printf
函數那么多。
- 增加了長度指定符。從
C99
開始增加了hh
、ll
、j
、z
和t
長度指定符,它們與...printf
轉換說明中的長度指定符相對應。 - 增加了轉換指定符。從
C99
開始增加了F
、a
和A
轉換指定符,提供這些轉換指定符是為了與...printf
相一致。...scanf
函數把它們與e
、E
、f
、g
和G
等同看待。 - 具有讀無窮數和NaN的能力。正如
...printf
函數可以輸出無窮數和NaN
一樣,...scanf
函數可以讀這些值。為了能夠正確讀出,這些數的形式應該與...printf
函數相同,忽略大小寫(例如,INF
或inf
都會被認為是無窮數)。 - 支持寬字符。
...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); | 123 abc¤ | n:1 Str: “123” |
n = scanf(“%[0123456789]”, str); | abc123¤ | n:0 str:不變 |
n = scanf(“%[^0123456789]”, str); | abc 123¤ | 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) return –1; /* can’t open file */ while (fscanf(fp, "%d", &n) != 1) { if (ferror(fp)) { fclose(fp); return –2; /* input error */ } if (feof(fp)) { fclose(fp); return –3; /* 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
之外的負值。
getc
和fgetc
之間的關系類似于putc
和fputc
之間的關系。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
類型的)之中后,判定條件會把ch
與EOF
進行比較。如果ch
不等于EOF
,則表示還未到達文件末尾,就可以執行循環體。如果ch
等于EOF
,則循環終止。
請注意!!始終要把
fgetc
、getc
或getchar
函數的返回值存儲在int
類型的變量中,而不是char
類型的變量中。把char
類型變量與EOF
進行比較可能會得到錯誤的結果。還有另外一種字符輸入函數,即
ungetc
函數。此函數把從流中讀入的字符“放回”并清除流的文件末尾指示器。如果在輸入過程中需要往前多看一個字符,那么這種能力可能會非常有效。比如,為了讀入一系列數字,并且在遇到首個非數字時停止操作,可以寫成
while (isdigit(ch = getc(fp))) { ...
}
ungetc(ch, fp); /* pushes back last character read */
通過持續調用ungetc
函數而放回的字符數量(不干涉讀操作)依賴于實現和所含的流類型。只有第一次的ungetc
函數調用保證會成功。調用文件定位函數(即fseek
、fsetpos
或rewind
)(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
函數對流是文本的還是二進制的很敏感。對于文本流而言,要么offset
(fseek
的第二個參數)必須為0
,要么whence
(fseek
的第三個參數)必須是SEEK_SET
,且offset
的值通過前面的ftell
函數調用獲得。(換句話說,我們只可以利用fseek
函數移動到文件的起始處或者文件的末尾處,或者返回前面訪問過的位置。)對于二進制流而言,fseek
函數不要求支持whence
是SEEK_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 字符串的輸入/輸出
本節里描述的函數有一點不同,因為它們與數據流或文件并沒有什么關系。相反,它們允許我們使用
字符串作為流
讀寫數據。sprintf
和snprintf
函數將按和寫到數據流一樣的方式寫字符到字符串,sscanf
函數從字符串中讀出數據就像從數據流中讀數據一樣。這些函數非常類似于printf
和scanf
函數,也都是非常有用的。sprintf
和snprintf
函數可以讓我們使用printf
的格式化能力,不需要真的往流中寫入數據。類似地,sscanf
函數也可以讓我們使用scanf
函數強大的模式匹配能力。下面將詳細講解sprintf
、snprintf
和sscanf
函數。
3
個相似的函數(vsprintf
、vsnprintf
和vsscanf
)也屬于<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
的值為4
,argv[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
標準允許其他的字符在模式字符串中出現,但是它們要跟在r
、w
、a
、b
或+
的后邊。有些編譯器允許使用t
來說明待打開的文件是文本模式而不是二進制模式。當然,無論如何文本模式都是默認的,所以字母t
沒有任何作用。在可能的情況下,最好避免使用字母t
和其他不可移植的特性。
問6:為什么要調用
fclose
函數來關閉文件呢?當程序終止時,所有打開的文件都會自動關閉,難道不是這樣嗎?
答:通常情況下是這樣的,但如果調用abort函數(26.2節)
來終止程序就不是了。即使在不用abort
函數的時候,調用fclose
函數仍有許多理由。首先,這樣會減少打開文件的數量。操作系統對程序每次可以打開的文件數量有限制,而大規模的程序可能會與此種限制相沖突。(定義在<stdio.h>
中的宏FOPEN_MAX
指定了可以同時打開的文件的最少數量。)其次,這樣做使程序更易于理解和修改。通過尋找fclose
函數,讀者更容易確定不再使用此文件的位置。最后,這樣做很安全。關閉文件可以確保正確地更新文件的內容和目錄項。如果將來程序崩潰了,至少該文件不會受到影響。
問7:我正在編寫的程序會提示用戶輸入文件的名字。我要設置多長的字符數組才可以存儲這個文件名字呢?
答:這與使用的操作系統有關。好在你可以使用宏FILENAME_MAX
(定義在<stdio.h>
中)來指定數組的大小。FILENAME_MAX
是字符串的長度,這個字符串用于存儲保證可以打開的最長的文件名。
問8:
fflush
可以清除同時為讀和寫而打開的流嗎?
答:根據C
標準,當流(1)
為輸出打開,或者(2)
為更新打開并且最后一個操作不是讀時,調用fflush
的結果才有定義。在其他所有情況下,調用fflush
函數的結果是未定義的。當傳遞空指針給fflush
函數時,它會清除所有滿足(1)
或(2)
的流。
問9:在
...printf
函數或...scanf
函數調用中,格式串可以是變量嗎?
答:當然。它可以是char *
類型的任意表達式。這個性質使...printf
函數和...scanf
函數比我們想象的更加多樣。請看下面這個來自Kernighan
和Ritchie
所著的《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:我始終不明白為什么輸入/輸出庫除了提供名為
fputc
和fgetc
的函數以外,還提供名為putc
和getc
的宏。依據21.1節
的介紹,putc
和getc
已經有兩種版本了(宏和函數)。如果需要真正的函數而不是宏,我們可以通過取消宏的定義來顯示putc
函數或getc
函數。那么,為什么要有fputc
和fgetc
存在呢?
答:這是歷史原因造成的。在標準化以前,C
語言沒有規則要求用真正的函數在庫中備份每個帶參數的宏。putc
函數和getc
函數傳統上只作為宏來實現,而fputc
函數和fgetc
函數則只作為函數來實現。
問13:把
fgetc
函數、getc
函數或者getchar
函數的返回值存儲到char
類型變量中會有什么問題?我不明白為什么判斷char
類型變量的值是否為EOF
會得到錯誤的結果。
答:有兩種情況可能導致該判定得出錯誤的結果。為了使下面的討論更具體,這里假設使用二進制補碼存儲方式。
首先,假定char
類型是無符號類型。(回憶一下,有些編譯器把char
作為有符號類型來處理,而有些編譯器則把它看成無符號類型的。)現在假設getc
函數返回EOF
,把該返回值存儲在名為ch
的char
類型變量中。如果EOF
表示-1
(通常如此),那么ch
的值將為255
。把ch
(無符號字符)與EOF
(有符號整數)進行比較就要求把ch
轉換為有符號整數(在這個例子中是255
)。因為255
不等于-1
,所以與EOF
的比較失敗了。
反之,現在假設char
是有符號類型。如果getc
函數從二進制流中讀取了一個含有值255
的字節,這樣會產生什么情況呢?因為ch
是有符號字符,所以把255
存儲在char
類型變量中將為它賦值-1
。如果判斷ch
是否等于EOF
,則會(錯誤地)產生真結果。
問14:
22.4節
描述的字符輸入函數要求在讀取用戶輸入之前看到回車鍵。如何編寫能直接響應鍵盤輸入的程序?
答:我們注意到,getc
、fgetc
和getchar
都是分配緩沖區的,這些函數在用戶按下回車鍵時才開始讀取輸入。為了實時讀取鍵盤輸入(這對某些類型的程序很重要),需要使用適合你的操作系統的非標準庫。例如,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
/ftell
和fsetpos
/fgetpos
)呢?一套函數難道不夠嗎?
答:fseek
函數和ftell
函數作為C
庫的一部分已有些年頭了,但它們有一個缺點:它們假定文件位置能夠用long int
類型的值表示。由于long int
通常是32
位的類型,當文件大小超過2147483647
字節時,fseek
函數和ftell
函數可能無法使用。針對這個問題,創建C89
標準時在<stdio.h>
中增加了fsetpos
和fgetpos
。這兩個函數不要求把文件位置看作數,因此就沒有long int
的限制了。但是也不要認為必須使用fsetpos
和fgetpos
,如果你的實現支持64
位的long int
類型,即使對很大的文件也可以使用fseek
和ftell
。
問18:為什么本章不討論屏幕控制,即移動光標、改變屏幕上字符顏色等呢?
答:C
語言沒有提供用于屏幕控制的標準函數。標準只發布那些通過廣泛的計算機和操作系統可以合理標準化的問題,而屏幕控制超出了這個范疇。在UNIX
中解決這個問題的習慣做法是使用curses
庫,這個庫支持不依賴終端方式的屏幕控制。
類似地,也沒有標準函數可以用來構建帶有圖形用戶界面的程序。不過,可以用C
函數調用來訪問操作系統中的窗口API(應用程序接口)
。
寫在最后
本文是博主閱讀《C語言程序設計:現代方法(第2版·修訂版)》時所作筆記,日后會持續更新后續章節筆記。歡迎各位大佬閱讀學習,如有疑問請及時聯系指正,希望對各位有所幫助,Thank you very much!