1、fopen/fclose
操作文件之前要先用fopen打開文件,操作完畢要用fclose關閉文件。打開文件就是在操作系統中分配一些資源用於保存該文件的狀態信息,並得到該文件的標示,以後用戶程序就可以用這個標示對文件做各種操作,關閉文件則釋放文件在操作系統中占用的資源,使文件的表示失效,用戶程序就無法再操作這個文件了。
#include <stdio.h> FILE *fopen(const char *path, const char *mode); 返回值:成功返回文件指針,出錯返回NULL並設置errno
path是文件的路徑名,mode表示打開方式。如果文件打開成功,就返回一個FILE *文件指針來標示這個文件。FILE是C標准庫中定義的結構體類型,其中包含該文件在內核中標示、I/O緩沖區和當前讀寫位置等信息,但調用者不必知道FILE結構體都有哪些成員。像FILE *這樣的指針稱為不透明指針(Opaque Pointer)或者叫句柄(Handle)。
mode參數:
"r":只讀,文件必須已存在
"w":只寫,如果文件不存在則創建,如果文件已存在則把文件長度截斷(Truncate)為0字節再重新寫,也就是替換掉原來的文件內容
"a":只能在文件末尾追加數據,如果文件不存在則創建
"r+":允許讀和寫,文件必須已存在
"w+":允許讀和寫,如果文件不存在則創建,如果文件已存在則把文件長度截斷為0字節再重新寫
"a+":允許讀和追加數據,如果文件不存在則創建
fclose函數:
#include <stdio.h> int fclose(FILE *fp); 返回值:成功返回0,出錯返回EOF並設置errno
EOF在stdio.h中定義:
/* End of file character. Some things throughout the library rely on this being -1. */ #ifndef EOF # define EOF (-1) #endif
EOF的值是-1。
2、stdin/stdout/stderr
用printf和scanf這些都屬於IO操作,但不是對文件的操作而是對終端設備做IO操作。所謂終端是指人機交互的設備,也就是可以接收用戶輸入並輸出信息給用戶的設備。終端設備和文件一樣也需要先打開後操作,終端設備也有對應的路徑名,/dev/tty就表示和當前進程相關聯的終端設備,/dev/tty不是一個普通的文件,它不表示磁盤上的一組數據,而是表示一個設備,用ls查看這個文件:
開頭的c表示文件類型是字符設備,中間的5, 0是它的設備號,主設備號5,次設備號0,主設備號標示內核中的一個設備驅動程序,次設備號標示該設備驅動程序管理的一個設備。內核通過設備號找到相應的驅動程序,完成對設備的操作。設備文件沒有文件尺寸這個屬性,因為設備文件在磁盤上不保存數據,對設備文件做讀寫操作並不是讀寫磁盤上的數據,而是在讀寫設備。
程序啟動時(在main函數還沒開始執行之前)會自動把終端設備打開三次,分別賦給三個FILE *指針stdin、stdout和stderr,這三個文件指針是libc中定義的全局變量,stdio.h中聲明,printf向stdout寫,而scanf從stdin讀。用戶程序也可以使用這三個文件指針。這三個文件指針的打開方式都是可讀可寫的,但通常stdin只用於讀操作,稱為標准輸入,stdout只用於寫操作,稱為標准輸出,stderr也只用於寫操作,稱為標准錯誤輸出,通常程序的運行結果打印到標准輸出,而錯誤提示(如gcc報的警告和錯誤)打印到標准錯誤輸出,所以fopen的錯誤處理寫成這樣更符合慣例:
if ( (fp = fopen("/tmp/file1", "r")) == NULL) { fputs("Error open file /tmp/file1\n", stderr); exit(1); }
3、errno與perror函數
很多系統函數在錯誤返回時將錯誤原因記錄在libc定義的全局變量errno中。各種錯誤對應一個錯誤碼,errno在errno.h中聲明,是一個整型變量,所有錯誤碼都是正整數。
直接打印errno只會打印出一個整數值看不出什麼錯誤,可以用perror或strerror函數將errno解釋成字符串再打印。
#include <stdio.h> void perror(const char *s);
perror函數將錯誤信息打印到標准錯誤輸出,首先打印參數s所指的字符串然後打印":"號,然後根據當前errno的值打印錯誤原因。
strerror函數可以根據錯誤號返回錯誤原因字符串。
#include <string.h> char *strerror(int errnum); 返回值:錯誤碼errnum所對應的字符串
有些函數的錯誤碼並不保存在errno中而是通過返回值返回,這時strerror就可以用到了:
fputs(strerror(n), stderr);
4、以字節為單位的IO函數
fget函數從指定的文件中讀一個字節,getchar從標准輸入讀一個字節,調用getchar()相當於調用fgetc(stdin)。
#include <stdio.h> int fgetc(FILE *stream); int getchar(void); 返回值:成功返回讀到的字節,出錯或者讀到文件末尾時返回EOF
標准IO庫操作的文件有時叫做流(Stream)。
fputc函數向指定的文件寫一個字節,putchar向標准輸出寫一個字節,調用putchar(c)相當於調用fput(c, stdout)。
#include <stdio.h> int fputc(int c, FILE *stream); int putchar(int c); 返回值:成功返回寫入的字節,出錯返回EOF
從終端設備讀有些特殊,當調用getchar()或fgetc(stdin)時,如果用戶沒有輸入字符,getchar函數就阻塞等待,所謂阻塞是指這個函數調用不返回,也就不能執行後邊的代碼,這個進程阻塞了,操作系統可以調度別的進程執行。從終端設備讀還有一個特點,用戶輸入一般字符不會是getchar函數返回,仍然阻塞著,只有當用戶輸入回車或者到達文件末尾時getchar才返回。
從終端設備輸入時有兩種方法表示文件結束,一種方法是在一行的開頭輸入Ctrl-D(如果不在一行的開頭則需要連續輸入兩次Ctrl-D),另一種方法是利用Shell的Heredoc語法:
$ ./a.out <<END > hello > hey > END hello hey
<<END表示從下一行開始是標准輸入,直到某一行開頭出現END時結束。<<後面的結束符可以任意指定,不一定得是END。
5、操作讀寫位置的函數
fseek函數可以移動讀寫位置,ftell可以返回當前的讀寫位置。
#include <stdio.h> int feek(FILE *stream, long offset, int whence); 返回值:成功返回0,出錯返回-1並設置errno long ftell(FILE *stream); 返回值:成功返回當前讀寫位置,出錯返回-1並設置errno void rewind(FILE *stream);
fseek的whence參數的含義:
SEEK_SET:從文件開頭移動offset個字節
SEEK_CUR:從當前位置移動offset個字節
SEEK_END:從文件末尾移動offset個字節
offset可正可負。如果向前移動的字節數超過了文件開頭則出錯返回,如果向後移動的字節數超過了文件末尾,再次寫入時將增大文件尺寸,從原來的文件末尾到fseek移動之後的讀寫位置之間的字節都是0。
6、以字符串為單位的IO函數
fgets從指定的文件中讀一行字符到調用者提供的緩沖區中,gets從標准輸入讀一行字符到調用者提供的緩沖區中。
#include <stdio.h> char *fgets(char *s, int size, FILE *stream); char *gets(char *s); 返回值:成功時s指向哪返回的指針就指向哪,出錯或者讀到文件末尾時返回NULL
fgets函數,參數s是緩沖區的首地址,size是緩沖區的長度,該函數從stream所指的文件中讀取以'\n'結尾的一行(包括'\n'在內)存到緩沖區s中,並且在該行末尾添加一個'\0'組成完整的字符串。如果文件中的一行太長,fgets從文件中讀了size-1個字符還沒有讀到'\n',就把已經讀到的size-1個字符和一個'\0'字符存入緩沖區,文件中剩下的半行可以在下次調用fgets時繼續讀。
如果一次fgets調用在讀入若干個字符後到達文件末尾,則將已讀到的字符串加上'\0'存入緩沖區並返回,如果再次調用fgets則返回NULL,可以據此判斷是否讀到文件末尾。如果文件中存在'\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'到標准輸出。
7、以記錄為單位的IO函數
#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指定的值。
8、格式化IO函數
#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類型傳進來。
#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中讀字符。
9、C標准庫的IO緩沖區
用戶程序調用C標准IO庫函數讀寫文件或設備,而這些庫函數最終要通過系統調用把讀寫請求傳給內核,最終內核驅動磁盤或設備完成IO操作。C標准庫為每個打開的文件分配一個IO緩沖區以加速讀寫操作,通過文件的FILE結構體可以找到這個緩沖區,用戶調用讀寫函數大多時候都在IO緩沖區中讀寫,只有少數時候需要把讀寫請求傳給內核。以fgetc/fputc為例,當用戶程序第一次調用fgetc讀一個文件時,fgetc函數可能通過系統調用進入內核讀1K字節進入到IO緩沖區,然後返回一個IO緩沖區中的第一個字節給用戶,把讀寫位置指向緩沖區的第二個字符下一次就從緩沖區中取,當把緩沖區中1K字節讀完之後再進入內核讀1K字節進緩沖區。
用戶程序調用fputc通常只是寫到I/O緩沖區中,這樣fputc函數可以很快地返回,如果I/O緩沖區寫滿了,fputc就通過系統
調用把I/O緩沖區中的數據傳給內核,內核最終把數據寫回磁盤。有時候用戶程序希望把I/O緩沖區中的數據立刻傳給內核,讓內核寫回設備,這稱為Flush操作,對應的庫函數是fflush,fclose函數在關閉文件之前也會做Flush操作。