我們已經多次用到了檔案,例如源檔案、目標檔案、執行檔、庫檔案等,現在學習如何用C標準庫對檔案進行讀寫操作,對檔案的讀寫也屬於I/O操作的一種,本節介紹的大部分函數在標頭檔stdio.h
中聲明,稱為標準I/O庫函數。
檔案可分為文本檔案(Text File)和二進制檔案(Binary File)兩種,源檔案是文本檔案,而目標檔案、執行檔和庫檔案是二進制檔案。文本檔案是用來保存字元的,檔案中的位元組都是字元的某種編碼(例如ASCII或UTF-8),用cat
命令可以查看其中的字元,用vi
可以編輯其中的字元,而二進制檔案不是用來保存字元的,檔案中的位元組表示其它含義,例如執行檔中有些位元組表示指令,有些位元組表示各Section和Segment在檔案中的位置,有些位元組表示各Segment的加載地址。
在第 5.1 節 “目標檔案”中我們用hexdump
命令查看過一個二進制檔案。我們再做一個小實驗,用vi
編輯一個檔案textfile
,在其中輸入5678
然後保存退出,用ls -l
命令可以看到它的長度是5:
$ ls -l textfile -rw-r--r-- 1 akaedu akaedu 5 2009-03-20 10:58 textfile
5678
四個字元各占一個位元組,vi
會自動在檔案末尾加一個換行符,所以檔案長度是5。用od
命令查看該檔案的內容:
$ od -tx1 -tc -Ax textfile 000000 35 36 37 38 0a 5 6 7 8 \n 000005
-tx1
選項表示將檔案中的位元組以十六進制的形式列出來,每組一個位元組,-tc
選項表示將檔案中的ASCII碼以字元形式列出來。和hexdump
類似,輸出結果最左邊的一列是檔案中的地址,預設以八進制顯示,-Ax
選項要求以十六進制顯示檔案中的地址。這樣我們看到,這個檔案中保存了5個字元,以ASCII碼保存。ASCII碼的範圍是0~127,所以ASCII碼文本檔案中每個位元組只用到低7位,最高位都是0。以後我們會經常用到od
命令。
文本檔案是一個模糊的概念。有些時候說文本檔案是指用vi
可以編輯出來的檔案,例如/etc
目錄下的各種配置檔案,這些檔案中只包含ASCII碼中的可見字元,而不包含像'\0'
這種不可見字元,也不包含最高位是1的非ASCII碼位元組。從廣義上來說,只要是專門保存字元的檔案都算文本檔案,包含不可見字元的也算,採用其它字元編碼(例如UTF-8編碼)的也算。
在操作檔案之前要用fopen
打開檔案,操作完畢要用fclose
關閉檔案。打開檔案就是在操作系統中分配一些資源用於保存該檔案的狀態信息,並得到該檔案的標識,以後用戶程序就可以用這個標識對檔案做各種操作,關閉檔案則釋放檔案在操作系統中占用的資源,使檔案的標識失效,用戶程序就無法再操作這個檔案了。
#include <stdio.h> FILE *fopen(const char *path, const char *mode); 返回值:成功返回檔案指針,出錯返回NULL並設置errno
path
是檔案的路徑名,mode
表示打開方式。如果檔案打開成功,就返回一個FILE *
檔案指針來標識這個檔案。以後調用其它函數對檔案做讀寫操作都要提供這個指針,以指明對哪個檔案進行操作。FILE
是C標準庫中定義的結構體類型,其中包含該檔案在內核中標識(在第 2 節 “C標準I/O庫函數與Unbuffered I/O函數”將會講到這個標識叫做檔案描述符)、I/O緩衝區和當前讀寫位置等信息,但調用者不必知道FILE
結構體都有哪些成員,我們很快就會看到,調用者只是把檔案指針在庫函數介面之間傳來傳去,而檔案指針所指的FILE
結構體的成員在庫函數內部維護,調用者不應該直接訪問這些成員,這種編程思想在面向對象方法論中稱為封裝(Encapsulation)。像FILE *
這樣的指針稱為不透明指針(Opaque Pointer)或者叫句柄(Handle),FILE *
指針就像一個把手(Handle),抓住這個把手就可以打開門或抽屜,但用戶只能抓這個把手,而不能直接抓門或抽屜。
下面說說參數path
和mode
,path
可以是相對路徑也可以是絶對路徑,mode
表示打開方式是讀還是寫。比如fp = fopen("/tmp/file2", "w");
表示打開絶對路徑/tmp/file2
,只做寫操作,path
也可以是相對路徑,比如fp = fopen("file.a", "r");
表示在當前工作目錄下打開檔案file.a
,只做讀操作,再比如fp = fopen("../a.out", "r");
只讀打開當前工作目錄上一層目錄下的a.out
,fp = fopen("Desktop/file3", "w");
只寫打開當前工作目錄下子目錄Desktop
下的file3
。相對路徑是相對於當前工作目錄(Current Working Directory)的路徑,每個進程都有自己的當前工作目錄,Shell進程的當前工作目錄可以用pwd
命令查看:
$ pwd /home/akaedu
通常Linux發行版都把Shell配置成在提示符前面顯示當前工作目錄,例如~$
表示當前工作目錄是主目錄,/etc$
表示當前工作目錄是/etc
。用cd
命令可以改變Shell進程的當前工作目錄。在Shell下敲命令啟動新的進程,則該進程的當前工作目錄繼承自Shell進程的當前工作目錄,該進程也可以調用chdir(2)
函數改變自己的當前工作目錄。
mode
參數是一個字元串,由rwatb+
六個字元組合而成,r
表示讀,w
表示寫,a
表示追加(Append),在檔案末尾追加數據使檔案的尺寸增大。t
表示文本檔案,b
表示二進制檔案,有些操作系統的文本檔案和二進制檔案格式不同,而在UNIX系統中,無論文本檔案還是二進制檔案都是由一串位元組組成,t
和b
沒有區分,用哪個都一樣,也可以省略不寫。如果省略t
和b
,rwa+
四個字元有以下6種合法的組合:
在打開一個檔案時如果出錯,fopen
將返回NULL
並設置errno
,errno
稍後介紹。在程序中應該做出錯處理,通常這樣寫:
if ( (fp = fopen("/tmp/file1", "r")) == NULL) { printf("error open file /tmp/file1!\n"); exit(1); }
比如/tmp/file1
這個檔案不存在,而r
打開方式又不會創建這個檔案,fopen
就會出錯返回。
再說說fclose
函數。
#include <stdio.h> int fclose(FILE *fp); 返回值:成功返回0,出錯返回EOF並設置errno
把檔案指針傳給fclose
可以關閉它所標識的檔案,關閉之後該檔案指針就無效了,不能再使用了。如果fclose
調用出錯(比如傳給它一個無效的檔案指針)則返回EOF
並設置errno
,errno
稍後介紹,EOF
在stdio.h
中定義:
/* End of file character. Some things throughout the library rely on this being -1. */ #ifndef EOF # define EOF (-1) #endif
它的值是-1。fopen
調用應該和fclose
調用配對,打開檔案操作完之後一定要記得關閉。如果不調用fclose
,在進程退出時系統會自動關閉檔案,但是不能因此就忽略fclose
調用,如果寫一個長年累月運行的程序(比如網絡伺服器程序),打開的檔案都不關閉,堆積得越來越多,就會占用越來越多的系統資源。
我們經常用printf
打印到屏幕,也用過scanf
讀鍵盤輸入,這些也屬於I/O操作,但不是對檔案做I/O操作而是對終端設備做I/O操作。所謂終端(Terminal)是指人機交互的設備,也就是可以接受用戶輸入並輸出信息給用戶的設備。在計算機剛誕生的年代,終端是電傳打字機和打印機,現在的終端通常是鍵盤和顯示器。終端設備和檔案一樣也需要先打開後操作,終端設備也有對應的路徑名,/dev/tty
就表示和當前進程相關聯的終端設備(在第 1.1 節 “終端的基本概念”會講到這叫進程的控制終端)。也就是說,/dev/tty
不是一個普通的檔案,它不表示磁碟上的一組數據,而是表示一個設備。用ls
命令查看這個檔案:
$ ls -l /dev/tty crw-rw-rw- 1 root dialout 5, 0 2009-03-20 19:31 /dev/tty
開頭的c
表示檔案類型是字元設備。中間的5, 0
是它的設備號,主設備號5,次設備號0,主設備號標識內核中的一個設備驅動程式,次設備號標識該設備驅動程式管理的一個設備。內核通過設備號找到相應的驅動程式,完成對該設備的操作。我們知道常規檔案的這一列應該顯示檔案尺寸,而設備檔案的這一列顯示設備號,這表明設備檔案是沒有檔案尺寸這個屬性的,因為設備檔案在磁碟上不保存數據,對設備檔案做讀寫操作並不是讀寫磁碟上的數據,而是在讀寫設備。UNIX的傳統是Everything is a file,鍵盤、顯示器、串口、磁碟等設備在/dev
目錄下都有一個特殊的設備檔案與之對應,這些設備檔案也可以像普通檔案一樣打開、讀、寫和關閉,使用的函數介面是相同的。本書中不嚴格區分“檔案”和“設備”這兩個概念,遇到“檔案”這個詞,讀者可以根據上下文理解它是指普通檔案還是設備,如果需要強調是保存在磁碟上的普通檔案,本書會用“常規檔案”(Regular File)這個詞。
那為什麼printf
和scanf
不用打開就能對終端設備進行操作呢?因為在程序啟動時(在main
函數還沒開始執行之前)會自動把終端設備打開三次,分別賦給三個FILE *
指針stdin
、stdout
和stderr
,這三個檔案指針是libc
中定義的全局變數,在stdio.h
中聲明,printf
向stdout
寫,而scanf
從stdin
讀,後面我們會看到,用戶程序也可以直接使用這三個檔案指針。這三個檔案指針的打開方式都是可讀可寫的,但通常stdin
只用於讀操作,稱為標準輸入(Standard Input),stdout
只用於寫操作,稱為標準輸出(Standard Output),stderr
也只用於寫操作,稱為標准錯誤輸出(Standard Error),通常程序的運行結果打印到標準輸出,而錯誤提示(例如gcc
報的警告和錯誤)打印到標准錯誤輸出,所以fopen
的錯誤處理寫成這樣更符合慣例:
if ( (fp = fopen("/tmp/file1", "r")) == NULL) { fputs("Error open file /tmp/file1\n", stderr); exit(1); }
fputs
函數將在稍後詳細介紹。不管是打印到標準輸出還是打印到標准錯誤輸出效果是一樣的,都是打印到終端設備(也就是屏幕)了,那為什麼還要分成標準輸出和標准錯誤輸出呢?以後我們會講到重定向操作,可以把標準輸出重定向到一個常規檔案,而標准錯誤輸出仍然對應終端設備,這樣就可以把正常的運行結果和錯誤提示分開,而不是混在一起打印到屏幕了。
很多系統函數在錯誤返回時將錯誤原因記錄在libc
定義的全局變數errno
中,每種錯誤原因對應一個錯誤碼,請查閲errno(3)
的Man Page瞭解各種錯誤碼,errno
在標頭檔errno.h
中聲明,是一個整型變數,所有錯誤碼都是正整數。
如果在程序中打印錯誤信息時直接打印errno
變數,打印出來的只是一個整數值,仍然看不出是什麼錯誤。比較好的辦法是用perror
或strerror
函數將errno
解釋成字元串再打印。
#include <stdio.h> void perror(const char *s);
perror
函數將錯誤信息打印到標准錯誤輸出,首先打印參數s
所指的字元串,然後打印:號,然後根據當前errno
的值打印錯誤原因。例如:
例 25.4. perror
#include <stdio.h> #include <stdlib.h> int main(void) { FILE *fp = fopen("abcde", "r"); if (fp == NULL) { perror("Open file abcde"); exit(1); } return 0; }
如果檔案abcde
不存在,fopen
返回-1並設置errno
為ENOENT
,緊接着perror
函數讀取errno
的值,將ENOENT
解釋成字元串No such file or directory
並打印,最後打印的結果是Open file abcde: No such file or directory
。雖然perror
可以打印出錯誤原因,傳給perror
的字元串參數仍然應該提供一些額外的信息,以便在看到錯誤信息時能夠很快定位是程序中哪裡出了錯,如果在程序中有很多個fopen
調用,每個fopen
打開不同的檔案,那麼在每個fopen
的錯誤處理中打印檔案名就很有幫助。
如果把上面的程序改成這樣:
#include <stdio.h> #include <stdlib.h> #include <errno.h> int main(void) { FILE *fp = fopen("abcde", "r"); if (fp == NULL) { perror("Open file abcde"); printf("errno: %d\n", errno); exit(1); } return 0; }
則printf
打印的錯誤號並不是fopen
產生的錯誤號,而是perror
產生的錯誤號。errno
是一個全局變數,很多系統函數都會改變它,fopen
函數Man Page中的ERRORS部分描述了它可能產生的錯誤碼,perror
函數的Man Page中沒有ERRORS
部分,說明它本身不產生錯誤碼,但它調用的其它函數也有可能改變errno
變數。大多數系統函數都有一個Side Effect,就是有可能改變errno
變數(當然也有少數例外,比如strcpy
),所以一個系統函數錯誤返回後應該馬上檢查errno
,在檢查errno
之前不能再調用其它系統函數。
strerror
函數可以根據錯誤號返回錯誤原因字元串。
#include <string.h> char *strerror(int errnum); 返回值:錯誤碼errnum所對應的字元串
這個函數返回指向靜態內存的指針。以後學綫程庫時我們會看到,有些函數的錯誤碼並不保存在errno
中,而是通過返回值返回,就不能調用perror
打印錯誤原因了,這時strerror
就派上了用場:
fputs(strerror(n), stderr);
1、在系統標頭檔中找到各種錯誤碼的宏定義。
2、做幾個小練習,看看fopen
出錯有哪些常見的原因。
打開一個沒有訪問權限的檔案。
fp = fopen("/etc/shadow", "r"); if (fp == NULL) { perror("Open /etc/shadow"); exit(1); }
fopen
也可以打開一個目錄,傳給fopen
的第一個參數目錄名末尾可以加/
也可以不加/
,但只允許以只讀方式打開。試試如果以可寫的方式打開一個存在的目錄會怎麼樣呢?
fp = fopen("/home/akaedu/", "r+"); if (fp == NULL) { perror("Open /home/akaedu"); exit(1); }
請讀者自己設計幾個實驗,看看你還能測試出哪些錯誤原因?
fgetc
函數從指定的檔案中讀一個位元組,getchar
從標準輸入讀一個位元組,調用getchar()
相當於調用fgetc(stdin)
。
#include <stdio.h> int fgetc(FILE *stream); int getchar(void); 返回值:成功返回讀到的位元組,出錯或者讀到檔案末尾時返回EOF
注意在Man Page的函數原型中FILE *
指針參數有時會起名叫stream
,這是因為標準I/O庫操作的檔案有時也叫做流(Stream),檔案由一串位元組組成,每次可以讀或寫其中任意數量的位元組,以後介紹TCP協議時會對流這個概念做更詳細的解釋。
對於fgetc函數的使用有以下幾點說明:
要用fgetc
函數讀一個檔案,該檔案的打開方式必須是可讀的。
系統對於每個打開的檔案都記錄著當前讀寫位置在檔案中的地址(或者說距離檔案開頭的位元組數),也叫偏移量(Offset)。當檔案打開時,讀寫位置是0,每調用一次fgetc
,讀寫位置向後移動一個位元組,因此可以連續多次調用fgetc
函數依次讀取多個位元組。
fgetc
成功時返回讀到一個位元組,本來應該是unsigned char
型的,但由於函數原型中返回值是int
型,所以這個位元組要轉換成int
型再返回,那為什麼要規定返回值是int
型呢?因為出錯或讀到檔案末尾時fgetc
將返回EOF
,即-1,保存在int
型的返回值中是0xffffffff,如果讀到位元組0xff,由unsigned char
型轉換為int
型是0x000000ff,只有規定返回值是int
型才能把這兩種情況區分開,如果規定返回值是unsigned char
型,那麼當返回值是0xff時無法區分到底是EOF
還是位元組0xff。如果需要保存fgetc
的返回值,一定要保存在int
型變數中,如果寫成unsigned char c = fgetc(fp);
,那麼根據c
的值又無法區分EOF
和0xff位元組了。注意,fgetc
讀到檔案末尾時返回EOF
,只是用這個返回值表示已讀到檔案末尾,並不是說每個檔案末尾都有一個位元組是EOF
(根據上面的分析,EOF並不是一個位元組)。
fputc
函數向指定的檔案寫一個位元組,putchar
向標準輸出寫一個位元組,調用putchar(c)
相當於調用fputc(c, stdout)
。
#include <stdio.h> int fputc(int c, FILE *stream); int putchar(int c); 返回值:成功返回寫入的位元組,出錯返回EOF
對於fputc
函數的使用也要說明幾點:
要用fputc
函數寫一個檔案,該檔案的打開方式必須是可寫的(包括追加)。
每調用一次fputc
,讀寫位置向後移動一個位元組,因此可以連續多次調用fputc
函數依次寫入多個位元組。但如果檔案是以追加方式打開的,每次調用fputc
時總是將讀寫位置移到檔案末尾然後把要寫入的位元組追加到後面。
下面的例子演示了這四個函數的用法,從鍵盤讀入一串字元寫到一個檔案中,再從這個檔案中讀出這些字元打印到屏幕上。
例 25.5. 用fputc/fget讀寫檔案和終端
#include <stdio.h> #include <stdlib.h> int main(void) { FILE *fp; int ch; if ( (fp = fopen("file2", "w+")) == NULL) { perror("Open file file2\n"); exit(1); } while ( (ch = getchar()) != EOF) fputc(ch, fp); rewind(fp); while ( (ch = fgetc(fp)) != EOF) putchar(ch); fclose(fp); return 0; }
從終端設備讀有點特殊。當調用getchar()
或fgetc(stdin)
時,如果用戶沒有輸入字元,getchar
函數就阻塞等待,所謂阻塞是指這個函數調用不返回,也就不能執行後面的代碼,這個進程阻塞了,操作系統可以調度別的進程執行。從終端設備讀還有一個特點,用戶輸入一般字元並不會使getchar
函數返回,仍然阻塞着,只有當用戶輸入回車或者到達檔案末尾時getchar
才返回[34]。這個程序的執行過程分析如下:
$ ./a.out hello(輸入hello並回車,這時第一次調用getchar返回,讀取字元h存到檔案中,然後連續調用getchar五次,讀取ello和換行符存到檔案中,第七次調用getchar又阻塞了) hey(輸入hey並回車,第七次調用getchar返回,讀取字元h存到檔案中,然後連續調用getchar三次,讀取ey和換行符存到檔案中,第11次調用getchar又阻塞了) (這時輸入Ctrl-D,第11次調用getchar返回EOF,跳出循環,進入下一個循環,回到檔案開頭,把檔案內容一個位元組一個位元組讀出來打印,直到檔案結束) hello hey
從終端設備輸入時有兩種方法表示檔案結束,一種方法是在一行的開頭輸入Ctrl-D(如果不在一行的開頭則需要連續輸入兩次Ctrl-D),另一種方法是利用Shell的Heredoc語法:
$ ./a.out <<END > hello > hey > END hello hey
<<END
表示從下一行開始是標準輸入,直到某一行開頭出現END
時結束。<<
後面的結束符可以任意指定,不一定得是END
,只要和輸入的內容能區分開就行。
在上面的程序中,第一個while
循環結束時fp
所指檔案的讀寫位置在檔案末尾,然後調用rewind
函數把讀寫位置移到檔案開頭,再進入第二個while
循環從頭讀取檔案內容。
我們在上一節的例子中看到rewind
函數把讀寫位置移到檔案開頭,本節介紹另外兩個操作讀寫位置的函數,fseek
可以任意移動讀寫位置,ftell
可以返回當前的讀寫位置。
#include <stdio.h> int fseek(FILE *stream, long offset, int whence); 返回值:成功返回0,出錯返回-1並設置errno long ftell(FILE *stream); 返回值:成功返回當前讀寫位置,出錯返回-1並設置errno void rewind(FILE *stream);
fseek
的whence
和offset
參數共同決定了讀寫位置移動到何處,whence
參數的含義如下:
SEEK_SET
從檔案開頭移動offset
個位元組
SEEK_CUR
從當前位置移動offset
個位元組
SEEK_END
從檔案末尾移動offset
個位元組
offset
可正可負,負值表示向前(向檔案開頭的方向)移動,正值表示向後(向檔案末尾的方向)移動,如果向前移動的位元組數超過了檔案開頭則出錯返回,如果向後移動的位元組數超過了檔案末尾,再次寫入時將增大檔案尺寸,從原來的檔案末尾到fseek
移動之後的讀寫位置之間的位元組都是0。
先前我們創建過一個檔案textfile
,其中有五個位元組,5678
加一個換行符,現在我們拿這個檔案做實驗。
例 25.6. fseek
#include <stdio.h> #include <stdlib.h> int main(void) { FILE* fp; if ( (fp = fopen("textfile","r+")) == NULL) { perror("Open file textfile"); exit(1); } if (fseek(fp, 10, SEEK_SET) != 0) { perror("Seek file textfile"); exit(1); } fputc('K', fp); fclose(fp); return 0; }
運行這個程序,然後查看檔案textfile
的內容:
$ ./a.out $ od -tx1 -tc -Ax textfile 000000 35 36 37 38 0a 00 00 00 00 00 4b 5 6 7 8 \n \0 \0 \0 \0 \0 K 00000b
fseek(fp, 10, SEEK_SET)
將讀寫位置移到第10個位元組處(其實是第11個位元組,從0開始數),然後在該位置寫入一個字元K,這樣textfile
檔案就變長了,從第5到第9個位元組自動被填充為0。
fgets
從指定的檔案中讀一行字元到調用者提供的緩衝區中,gets
從標準輸入讀一行字元到調用者提供的緩衝區中。
#include <stdio.h> char *fgets(char *s, int size, FILE *stream); char *gets(char *s); 返回值:成功時s指向哪返回的指針就指向哪,出錯或者讀到檔案末尾時返回NULL
gets
函數無需解釋,Man Page的BUGS部分已經說得很清楚了:Never use gets()。gets
函數的存在只是為了兼容以前的程序,我們寫的代碼都不應該調用這個函數。gets
函數的介面設計得很有問題,就像strcpy
一樣,用戶提供一個緩衝區,卻不能指定緩衝區的大小,很可能導致緩衝區溢出錯誤,這個函數比strcpy
更加危險,strcpy
的輸入和輸出都來自程序內部,只要程序員小心一點就可以避免出問題,而gets
讀取的輸入直接來自程序外部,用戶可能通過標準輸入提供任意長的字元串,程序員無法避免gets
函數導致的緩衝區溢出錯誤,所以唯一的辦法就是不要用它。
現在說說fgets
函數,參數s
是緩衝區的首地址,size
是緩衝區的長度,該函數從stream
所指的檔案中讀取以'\n'
結尾的一行(包括'\n'
在內)存到緩衝區s
中,並且在該行末尾添加一個'\0'
組成完整的字元串。
如果檔案中的一行太長,fgets
從檔案中讀了size-1
個字元還沒有讀到'\n'
,就把已經讀到的size-1
個字元和一個'\0'
字元存入緩衝區,檔案中剩下的半行可以在下次調用fgets
時繼續讀。
如果一次fgets
調用在讀入若干個字元後到達檔案末尾,則將已讀到的字元串加上'\0'
存入緩衝區並返回,如果再次調用fgets
則返回NULL
,可以據此判斷是否讀到檔案末尾。
注意,對於fgets
來說,'\n'
是一個特別的字元,而'\0'
並無任何特別之處,如果讀到'\0'
就當作普通字元讀入。如果檔案中存在'\0'
字元(或者說0x00位元組),調用fgets
之後就無法判斷緩衝區中的'\0'
究竟是從檔案讀上來的字元還是由fgets
自動添加的結束符,所以fgets
只適合讀文本檔案而不適合讀二進制檔案,並且文本檔案中的所有字元都應該是可見字元,不能有'\0'
。
fputs
向指定的檔案寫入一個字元串,puts
向標準輸出寫入一個字元串。
#include <stdio.h> int fputs(const char *s, FILE *stream); int puts(const char *s); 返回值:成功返回一個非負整數,出錯返回EOF
緩衝區s
中保存的是以'\0'
結尾的字元串,fputs
將該字元串寫入檔案stream
,但並不寫入結尾的'\0'
。與fgets
不同的是,fputs
並不關心的字元串中的'\n'
字元,字元串中可以有'\n'
也可以沒有'\n'
。puts
將字元串s
寫到標準輸出(不包括結尾的'\0'
),然後自動寫一個'\n'
到標準輸出。
#include <stdio.h> size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream); size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream); 返回值:讀或寫的記錄數,成功時返回的記錄數等於nmemb,出錯或讀到檔案末尾時返回的記錄數小於nmemb,也可能返回0
fread
和fwrite
用於讀寫記錄,這裡的記錄是指一串固定長度的位元組,比如一個int
、一個結構體或者一個定長數組。參數size
指出一條記錄的長度,而nmemb
指出要讀或寫多少條記錄,這些記錄在ptr
所指的內存空間中連續存放,共占size * nmemb
個位元組,fread
從檔案stream
中讀出size * nmemb
個位元組保存到ptr
中,而fwrite
把ptr
中的size * nmemb
個位元組寫到檔案stream
中。
nmemb
是請求讀或寫的記錄數,fread
和fwrite
返回的記錄數有可能小於nmemb
指定的記錄數。例如當前讀寫位置距檔案末尾只有一條記錄的長度,調用fread
時指定nmemb
為2,則返回值為1。如果當前讀寫位置已經在檔案末尾了,或者讀檔案時出錯了,則fread
返回0。如果寫檔案時出錯了,則fwrite
的返回值小於nmemb
指定的值。下面的例子由兩個程序組成,一個程序把結構體保存到檔案中,另一個程序和從檔案中讀出結構體。
例 25.7. fread/fwrite
/* writerec.c */ #include <stdio.h> #include <stdlib.h> struct record { char name[10]; int age; }; int main(void) { struct record array[2] = {{"Ken", 24}, {"Knuth", 28}}; FILE *fp = fopen("recfile", "w"); if (fp == NULL) { perror("Open file recfile"); exit(1); } fwrite(array, sizeof(struct record), 2, fp); fclose(fp); return 0; }
/* readrec.c */ #include <stdio.h> #include <stdlib.h> struct record { char name[10]; int age; }; int main(void) { struct record array[2]; FILE *fp = fopen("recfile", "r"); if (fp == NULL) { perror("Open file recfile"); exit(1); } fread(array, sizeof(struct record), 2, fp); printf("Name1: %s\tAge1: %d\n", array[0].name, array[0].age); printf("Name2: %s\tAge2: %d\n", array[1].name, array[1].age); fclose(fp); return 0; }
$ gcc writerec.c -o writerec $ gcc readrec.c -o readrec $ ./writerec $ od -tx1 -tc -Ax recfile 000000 4b 65 6e 00 00 00 00 00 00 00 00 00 18 00 00 00 K e n \0 \0 \0 \0 \0 \0 \0 \0 \0 030 \0 \0 \0 000010 4b 6e 75 74 68 00 00 00 00 00 00 00 1c 00 00 00 K n u t h \0 \0 \0 \0 \0 \0 \0 034 \0 \0 \0 000020 $ ./readrec Name1: Ken Age1: 24 Name2: Knuth Age2: 28
我們把一個struct record
結構體看作一條記錄,由於結構體中有填充位元組,每條記錄占16位元組,把兩條記錄寫到檔案中共占32位元組。該程序生成的recfile
檔案是二進制檔案而非文本檔案,因為其中不僅保存着字元型數據,還保存着整型數據24和28(在od
命令的輸出中以八進制顯示為030和034)。注意,直接在檔案中讀寫結構體的程序是不可移植的,如果在一種平台上編譯運行writebin.c
程序,把生成的recfile
檔案拷到另一種平台並在該平台上編譯運行readbin.c
程序,則不能保證正確讀出檔案的內容,因為不同平台的大小端可能不同(因而對整型數據的存儲方式不同),結構體的填充方式也可能不同(因而同一個結構體所占的位元組數可能不同,age
成員在name
成員之後的什麼位置也可能不同)。
現在該正式講一下printf
和scanf
函數了,這兩個函數都有很多種形式。
#include <stdio.h> int printf(const char *format, ...); int fprintf(FILE *stream, const char *format, ...); int sprintf(char *str, const char *format, ...); int snprintf(char *str, size_t size, const char *format, ...); #include <stdarg.h> int vprintf(const char *format, va_list ap); int vfprintf(FILE *stream, const char *format, va_list ap); int vsprintf(char *str, const char *format, va_list ap); int vsnprintf(char *str, size_t size, const char *format, va_list ap); 返回值:成功返回格式化輸出的位元組數(不包括字元串的結尾'\0'),出錯返回一個負值
printf
格式化打印到標準輸出,而fprintf
打印到指定的檔案stream
中。sprintf
並不打印到檔案,而是打印到用戶提供的緩衝區str
中並在末尾加'\0'
,由於格式化後的字元串長度很難預計,所以很可能造成緩衝區溢出,用snprintf
更好一些,參數size
指定了緩衝區長度,如果格式化後的字元串長度超過緩衝區長度,snprintf
就把字元串截斷到size-1
位元組,再加上一個'\0'
寫入緩衝區,也就是說snprintf
保證字元串以'\0'
結尾。snprintf
的返回值是格式化後的字元串長度(不包括結尾的'\0'
),如果字元串被截斷,返回的是截斷之前的長度,把它和實際緩衝區中的字元串長度相比較就可以知道是否發生了截斷。
上面列出的後四個函數在前四個函數名的前面多了個v
,表示可變參數不是以...
的形式傳進來,而是以va_list
類型傳進來。下面我們用vsnprintf
包裝出一個類似printf
的帶格式化字元串和可變參數的函數。
例 25.8. 實現格式化打印錯誤的err_sys函數
#include <stdio.h> #include <stdlib.h> #include <errno.h> #include <stdarg.h> #include <string.h> #define MAXLINE 80 void err_sys(const char *fmt, ...) { int err = errno; char buf[MAXLINE+1]; va_list ap; va_start(ap, fmt); vsnprintf(buf, MAXLINE, fmt, ap); snprintf(buf+strlen(buf), MAXLINE-strlen(buf), ": %s", strerror(err)); strcat(buf, "\n"); fputs(buf, stderr); va_end(ap); exit(1); } int main(int argc, char *argv[]) { FILE *fp; if (argc != 2) { fputs("Usage: ./a.out pathname\n", stderr); exit(1); } fp = fopen(argv[1], "r"); if (fp == NULL) err_sys("Line %d - Open file %s", __LINE__, argv[1]); printf("Open %s OK\n", argv[1]); fclose(fp); return 0; }
有了err_sys
函數,不僅簡化了main
函數的代碼,而且可以把fopen
的錯誤提示打印得非常清楚,有原始碼行號,有打開檔案的路徑名,一看就知道哪裡出錯了。
現在總結一下printf
格式化字元串中的轉換說明的有哪些寫法。在這裡只列舉幾種常用的格式,其它格式請參考Man Page。每個轉換說明以%
號開頭,以轉換字元結尾,我們以前用過的轉換說明僅包含%
號和轉換字元,例如%d
、%s
,其實在這兩個字元中間還可以插入一些可選項。
表 25.1. printf轉換說明的可選項
選項 | 描述 | 舉例 |
---|---|---|
# | 八進制前面加0(轉換字元為o ),十六進制前面加0x(轉換字元為x )或0X(轉換字元為X )。 | printf("%#x", 0xff) 打印0xff ,printf("%x", 0xff) 打印ff 。 |
- | 格式化後的內容居左,右邊可以留空格。 | 見下面的例子 |
寬度 | 用一個整數指定格式化後的最小長度,如果格式化後的內容沒有這麼長,可以在左邊留空格,如果前面指定了- 號就在右邊留空格。寬度有一種特別的形式,不指定整數值而是寫成一個* 號,表示取一個int 型參數作為寬度。 | printf("-%10s-", "hello") 打印-␣␣␣␣␣hello- ,printf("-%-*s-", 10, "hello") 打印-hello␣␣␣␣␣- 。 |
. | 用於分隔上一條提到的最小長度和下一條要講的精度。 | 見下面的例子 |
精度 | 用一個整數表示精度,對於字元串來說指定了格式化後保留的最大長度,對於浮點數來說指定了格式化後小數點右邊的位數,對於整數來說指定了格式化後的最小位數。精度也可以不指定整數值而是寫成一個* 號,表示取下一個int 型參數作為精度。 | printf("%.4s", "hello") 打印hell ,printf("-%6.4d-", 100) 打印-␣␣0100- ,printf("-%*.*f-", 8, 4, 3.14) 打印-␣␣3.1400- 。 |
字長 | 對於整型參數,hh 、h 、l 、ll 分別表示是char 、short 、long 、long long 型的字長,至於是有符號數還是無符號數則取決於轉換字元;對於浮點型參數,L 表示long double 型的字長。 | printf("%hhd", 255) 打印-1 。 |
常用的轉換字元有:
表 25.2. printf的轉換字元
轉換字元 | 描述 | 舉例 |
---|---|---|
d i | 取int 型參數格式化成有符號十進製表示,如果格式化後的位數小於指定的精度,就在左邊補0。 | printf("%.4d", 100) 打印0100 。 |
o u x X | 取unsigned int 型參數格式化成無符號八進制(o)、十進制(u)、十六進制(x或X)表示,x表示十六進制數字用小寫abcdef,X表示十六進制數字用大寫ABCDEF,如果格式化後的位數小於指定的精度,就在左邊補0。 | printf("%#X", 0xdeadbeef) 打印0XDEADBEEF ,printf("%hhu", -1) 打印255 。 |
c | 取int 型參數轉換成unsigned char 型,格式化成對應的ASCII碼字元。 | printf("%c", 256+'A') 打印A 。 |
s | 取const char * 型參數所指向的字元串格式化輸出,遇到'\0' 結束,或者達到指定的最大長度(精度)結束。 | printf("%.4s", "hello") 打印hell 。 |
p | 取void * 型參數格式化成十六進製表示。相當於%#x 。 | printf("%p", main) 打印main 函數的首地址0x80483c4 。 |
f | 取double 型參數格式化成[-]ddd.ddd 這樣的格式,小數點後的預設精度是6位。 | printf("%f", 3.14) 打印3.140000 ,printf("%f", 0.00000314) 打印0.000003 。 |
e E | 取double 型參數格式化成[-]d.ddde±dd (轉換字元是e)或[-]d.dddE±dd (轉換字元是E)這樣的格式,小數點後的預設精度是6位,指數至少是兩位。 | printf("%e", 3.14) 打印3.140000e+00 。 |
g G | 取double 型參數格式化,精度是指有效數字而非小數點後的數字,預設精度是6。如果指數小於-4或大於等於精度就按%e (轉換字元是g)或%E (轉換字元是G)格式化,否則按%f 格式化。小數部分的末尾0去掉,如果沒有小數部分,小數點也去掉。 | printf("%g", 3.00) 打印3 ,printf("%g", 0.00001234567) 打印1.23457e-05 。 |
% | 格式化成一個% 。 | printf("%%") 打印一個% 。 |
我們在第 6 節 “可變參數”講過可變參數的原理,printf
並不知道實際參數的類型,只能按轉換說明指出的參數類型從棧幀上取參數,所以如果實際參數和轉換說明的類型不符,結果可能會有些意外,上面也舉過幾個這樣的例子。另外,如果s
指向一個字元串,用printf(s)
打印這個字元串可能得到錯誤的結果,因為字元串中可能包含%
號而被printf
當成轉換說明,printf
並不知道後面沒有傳其它參數,照樣會從棧幀上取參數。所以比較保險的辦法是printf("%s", s)
。
下面看scanf
函數的各種形式。
#include <stdio.h> int scanf(const char *format, ...); int fscanf(FILE *stream, const char *format, ...); int sscanf(const char *str, const char *format, ...); #include <stdarg.h> int vscanf(const char *format, va_list ap); int vsscanf(const char *str, const char *format, va_list ap); int vfscanf(FILE *stream, const char *format, va_list ap); 返回值:返回成功匹配和賦值的參數個數,成功匹配的參數可能少於所提供的賦值參數,返回0表示一個都不匹配,出錯或者讀到檔案或字元串末尾時返回EOF並設置errno
scanf
從標準輸入讀字元,按格式化字元串format
中的轉換說明解釋這些字元,轉換後賦給後面的參數,後面的參數都是傳出參數,因此必須傳地址而不能傳值。fscanf
從指定的檔案stream
中讀字元,而sscanf
從指定的字元串str
中讀字元。後面三個以v
開頭的函數的可變參數不是以...
的形式傳進來,而是以va_list
類型傳進來。
現在總結一下scanf
的格式化字元串和轉換說明,這裡也只列舉幾種常用的格式,其它格式請參考Man Page。scanf
用輸入的字元去匹配格式化字元串中的字元和轉換說明,如果成功匹配一個轉換說明,就給一個參數賦值,如果讀到檔案或字元串末尾就停止,或者如果遇到和格式化字元串不匹配的地方(比如轉換說明是%d
卻讀到字元A
)就停止。如果遇到不匹配的地方而停止,scanf
的返回值可能小於賦值參數的個數,檔案的讀寫位置指向輸入中不匹配的地方,下次調用庫函數讀檔案時可以從這個位置繼續。
格式化字元串中包括:
空格或Tab,在處理過程中被忽略。
普通字元(不包括%
),和輸入字元中的非空白字元相匹配。輸入字元中的空白字元是指空格、Tab、\r
、\n
、\v
、\f
。
轉換說明,以%
開頭,以轉換字元結尾,中間也有若干個可選項。
轉換說明中的可選項有:
*
號,表示這個轉換說明只是用來匹配一段輸入字元,但匹配結果並不賦給後面的參數。
用一個整數指定的寬度N。表示這個轉換說明最多匹配N個輸入字元,或者匹配到輸入字元中的下一個空白字元結束。
對於整型參數可以指定字長,有hh
、h
、l
、ll
(也可以寫成一個L
),含義和printf
相同。但l
和L
還有一層含義,當轉換字元是e
、f
、g
時,表示賦值參數的類型是float *
而非double *
,這一點跟printf
不同(結合以前講的類型轉換規則思考一下為什麼不同),這時前面加上l
或L
分別表示double *
或long double *
型。
常用的轉換字元有:
表 25.3. scanf的轉換字元
轉換字元 | 描述 |
---|---|
d | 匹配十進制整數(開頭可以有負號),賦值參數的類型是int * 。 |
i | 匹配整數(開頭可以有負號),賦值參數的類型是int * ,如果輸入字元以0x或0X開頭則匹配十六進制整數,如果輸入字元以0開頭則匹配八進制整數。 |
o u x | 匹配八進制、十進制、十六進制整數(開頭可以有負號),賦值參數的類型是unsigned int * 。 |
c | 匹配一串字元,字元的個數由寬度指定,預設寬度是1,賦值參數的類型是char * ,末尾不會添加'\0' 。如果輸入字元的開頭有空白字元,這些空白字元並不被忽略,而是保存到參數中,要想跳過開頭的空白字元,可以在格式化字元串中用一個空格去匹配。 |
s | 匹配一串非空白字元,從輸入字元中的第一個非空白字元開始匹配到下一個空白字元之前,或者匹配到指定的寬度,賦值參數的類型是char * ,末尾自動添加'\0' 。 |
e f g | 匹配符點數(開頭可以有負號),賦值參數的類型是float * ,也可以指定double * 或long double * 的字長。 |
% | 轉換說明%% 匹配一個字元% ,不做賦值。 |
下面幾個例子出自[K&R]。第一個例子,讀取用戶輸入的浮點數累加起來。
例 25.9. 用scanf實現簡單的計算器
#include <stdio.h> int main(void) /* rudimentary calculator */ { double sum, v; sum = 0; while (scanf("%lf", &v) == 1) printf("\t%.2f\n", sum += v); return 0; }
如果我們要讀取25 Dec 1988
這樣的日期格式,可以這樣寫:
char *str = "25 Dec 1988"; int day, year; char monthname[20]; sscanf(str, "%d %s %d", &day, monthname, &year);
如果str
中的空白字元再多一些,比如" 25 Dec 1998"
,仍然可以正確讀取。如果格式化字元串中的空格和Tab再多一些,比如"%d %s %d "
,也可以正確讀取。scanf
函數是很強大的,但是要用對了不容易,需要多練習,通過練習體會空白字元的作用。
如果要讀取12/25/1998
這樣的日期格式,就需要在格式化字元串中用/
匹配輸入字元中的/
:
int day, month, year; scanf("%d/%d/%d", &month, &day, &year);
scanf
把換行符也看作空白字元,僅僅當作欄位之間的分隔符,如果輸入中的欄位個數不確定,最好是先用fgets
按行讀取,然後再交給sscanf
處理。如果我們的程序需要同時識別以上兩種日期格式,可以這樣寫:
while (fgets(line, sizeof(line), stdin) > 0) { if (sscanf(line, "%d %s %d", &day, monthname, &year) == 3) printf("valid: %s\n", line); /* 25 Dec 1988 form */ else if (sscanf(line, "%d/%d/%d", &month, &day, &year) == 3) printf("valid: %s\n", line); /* mm/dd/yy form */ else printf("invalid: %s\n", line); /* invalid form */ }
用戶程序調用C標準I/O庫函數讀寫檔案或設備,而這些庫函數要通過系統調用把讀寫請求傳給內核(以後我們會看到與I/O相關的系統調用),最終由內核驅動磁碟或設備完成I/O操作。C標準庫為每個打開的檔案分配一個I/O緩衝區以加速讀寫操作,通過檔案的FILE
結構體可以找到這個緩衝區,用戶調用讀寫函數大多數時候都在I/O緩衝區中讀寫,只有少數時候需要把讀寫請求傳給內核。以fgetc
/fputc
為例,當用戶程序第一次調用fgetc
讀一個位元組時,fgetc
函數可能通過系統調用進入內核讀1K位元組到I/O緩衝區中,然後返回I/O緩衝區中的第一個位元組給用戶,把讀寫位置指向I/O緩衝區中的第二個字元,以後用戶再調fgetc
,就直接從I/O緩衝區中讀取,而不需要進內核了,當用戶把這1K位元組都讀完之後,再次調用fgetc
時,fgetc
函數會再次進入內核讀1K位元組到I/O緩衝區中。在這個場景中用戶程序、C標準庫和內核之間的關係就像在第 5 節 “Memory Hierarchy”中CPU、Cache和內存之間的關係一樣,C標準庫之所以會從內核預讀一些數據放在I/O緩衝區中,是希望用戶程序隨後要用到這些數據,C標準庫的I/O緩衝區也在用戶空間,直接從用戶空間讀取數據比進內核讀數據要快得多。另一方面,用戶程序調用fputc
通常只是寫到I/O緩衝區中,這樣fputc
函數可以很快地返回,如果I/O緩衝區寫滿了,fputc
就通過系統調用把I/O緩衝區中的數據傳給內核,內核最終把數據寫回磁碟。有時候用戶程序希望把I/O緩衝區中的數據立刻傳給內核,讓內核寫回設備,這稱為Flush操作,對應的庫函數是fflush
,fclose
函數在關閉檔案之前也會做Flush操作。
下圖以fgets
/fputs
示意了I/O緩衝區的作用,使用fgets
/fputs
函數時在用戶程序中也需要分配緩衝區(圖中的buf1
和buf2
),注意區分用戶程序的緩衝區和C標準庫的I/O緩衝區。
C標準庫的I/O緩衝區有三種類型:全緩衝、行緩衝和無緩衝。當用戶程序調用庫函數做寫操作時,不同類型的緩衝區具有不同的特性。
如果緩衝區寫滿了就寫回內核。常規檔案通常是全緩衝的。
如果用戶程序寫的數據中有換行符就把這一行寫回內核,或者如果緩衝區寫滿了就寫回內核。標準輸入和標準輸出對應終端設備時通常是行緩衝的。
用戶程序每次調庫函數做寫操作都要通過系統調用寫回內核。標准錯誤輸出通常是無緩衝的,這樣用戶程序產生的錯誤信息可以儘快輸出到設備。
下面通過一個簡單的例子證明標準輸出對應終端設備時是行緩衝的。
#include <stdio.h> int main() { printf("hello world"); while(1); return 0; }
運行這個程序,會發現hello world
並沒有打印到屏幕上。用Ctrl-C終止它,去掉程序中的while(1);
語句再試一次:
$ ./a.out hello world$
hello world
被打印到屏幕上,後面直接跟Shell提示符,中間沒有換行。
我們知道main
函數被啟動代碼這樣調用:exit(main(argc, argv));
。main
函數return
時啟動代碼會調用exit
,exit
函數首先關閉所有尚未關閉的FILE *
指針(關閉之前要做Flush操作),然後通過_exit
系統調用進入內核退出當前進程[35]。
在上面的例子中,由於標準輸出是行緩衝的,printf("hello world");
打印的字元串中沒有換行符,所以只把字元串寫到標準輸出的I/O緩衝區中而沒有寫回內核(寫到終端設備),如果敲Ctrl-C,進程是異常終止的,並沒有調用exit
,也就沒有機會Flush I/O緩衝區,因此字元串最終沒有打印到屏幕上。如果把打印語句改成printf("hello world\n");
,有換行符,就會立刻寫到終端設備,或者如果把while(1);
去掉也可以寫到終端設備,因為程序退出時會調用exit
Flush所有I/O緩衝區。在本書的其它例子中,printf
打印的字元串末尾都有換行符,以保證字元串在printf
調用結束時就寫到終端設備。
我們再做個實驗,在程序中直接調用_exit
退出。
#include <stdio.h> #include <unistd.h> int main() { printf("hello world"); _exit(0); }
結果也不會把字元串打印到屏幕上,如果把_exit
調用改成exit
就可以打印到屏幕上。
除了寫滿緩衝區、寫入換行符之外,行緩衝還有一種情況會自動做Flush操作。如果:
用戶程序調用庫函數從無緩衝的檔案中讀取
或者從行緩衝的檔案中讀取,並且這次讀操作會引發系統調用從內核讀取數據
那麼在讀取之前會自動Flush所有行緩衝。例如:
#include <stdio.h> #include <unistd.h> int main() { char buf[20]; printf("Please input a line: "); fgets(buf, 20, stdin); return 0; }
雖然調用printf
並不會把字元串寫到設備,但緊接着調用fgets
讀一個行緩衝的檔案(標準輸入),在讀取之前會自動Flush所有行緩衝,包括標準輸出。
如果用戶程序不想完全依賴于自動的Flush操作,可以調fflush
函數手動做Flush操作。
#include <stdio.h> int fflush(FILE *stream); 返回值:成功返回0,出錯返回EOF並設置errno
對前面的例子再稍加改動:
#include <stdio.h> int main() { printf("hello world"); fflush(stdout); while(1); }
雖然字元串中沒有換行,但用戶程序調用fflush
強制寫回內核,因此也能在屏幕上打印出字元串。fflush
函數用於確保數據寫回了內核,以免進程異常終止時丟失數據。作為一個特例,調用fflush(NULL)
可以對所有打開檔案的I/O緩衝區做Flush操作。
1、編程讀寫一個檔案test.txt
,每隔1秒向檔案中寫入一行記錄,類似於這樣:
1 2009-7-30 15:16:42 2 2009-7-30 15:16:43
該程序應該無限循環,直到按Ctrl-C終止。下次再啟動程序時在test.txt
檔案末尾追加記錄,並且序號能夠接續上次的序號,比如:
1 2009-7-30 15:16:42 2 2009-7-30 15:16:43 3 2009-7-30 15:19:02 4 2009-7-30 15:19:03 5 2009-7-30 15:19:04
這類似於很多系統服務維護的日誌檔案,例如在我的機器上系統服務進程acpid
維護一個日誌檔案/var/log/acpid
,就像這樣:
$ cat /var/log/acpid [Sun Oct 26 08:44:46 2008] logfile reopened [Sun Oct 26 10:11:53 2008] exiting [Sun Oct 26 18:54:39 2008] starting up ...
每次系統啟動時acpid
進程就以追加方式打開這個檔案,當有事件發生時就追加一條記錄,包括事件發生的時刻以及事件描述信息。
獲取當前的系統時間需要調用time(2)
函數,返回的結果是一個time_t
類型,其實就是一個大整數,其值表示從UTC(Coordinated Universal Time)時間1970年1月1日00:00:00(稱為UNIX系統的Epoch時間)到當前時刻的秒數。然後調用localtime(3)
將time_t
所表示的UTC時間轉換為本地時間(我們是+8區,比UTC多8個小時)並轉成struct tm
類型,該類型的各數據成員分別表示年月日時分秒,具體用法請查閲Man Page。調用sleep(3)
函數可以指定程序睡眠多少秒。
2、INI檔案是一種很常見的配置檔案,很多Windows程序都採用這種格式的配置檔案,在Linux系統中Qt程序通常也採用這種格式的配置檔案。比如:
;Configuration of http [http] domain=www.mysite.com port=8080 cgihome=/cgi-bin ;Configuration of db [database] server = mysql user = myname password = toopendatabase
一個配置檔案由若干個Section組成,由[]括號括起來的是Section名。每個Section下面有若干個key = value
形式的鍵值對(Key-value Pair),等號兩邊可以有零個或多個空白字元(空格或Tab),每個鍵值對占一行。以;號開頭的行是註釋。每個Section結束時有一個或多個空行,空行是僅包含零個或多個空白字元(空格或Tab)的行。INI檔案的最後一行後面可能有換行符也可能沒有。
現在XML興起了,INI檔案顯得有點土。現在要求編程把INI檔案轉換成XML檔案。上面的例子經轉換後應該變成這樣:
<!-- Configuration of http --> <http> <domain>www.mysite.com</domain> <port>8080</port> <cgihome>/cgi-bin</cgihome> </http> <!-- Configuration of db --> <database> <server>mysql</server> <user>myname</user> <password>toopendatabase</password> </database>
3、實現類似gcc
的-M
選項的功能,給定一個.c
檔案,列出它直接和間接包含的所有標頭檔,例如有一個main.c
檔案:
#include <errno.h> #include "stack.h" int main() { return 0; }
你的程序讀取這個檔案,打印出其中包含的所有標頭檔的絶對路徑:
$ ./a.out main.c /usr/include/errno.h /usr/include/features.h /usr/include/bits/errno.h /usr/include/linux/errno.h ... /home/akaedu/stack.h: cannot find
如果有的標頭檔找不到,就像上面例子那樣打印/home/akaedu/stack.h: cannot find
。首先複習一下第 2.2 節 “標頭檔”講過的標頭檔查找順序,本題目不必考慮-I
選項指定的目錄,只在.c
檔案所在的目錄以及系統目錄/usr/include
中查找。