1.C語言文件概述
我們對文件的概念已經非常熟悉了,比如常見的 Word 文檔、txt 文件、源文件等。文件是數據源的一種,最主要的作用是保存數據。
在操作系統中,為了統一對各種硬件的操作,簡化接口,不同的硬件設備也都被看成一個文件。對這些文件的操作,等同於對磁盤上普通文件的操作。例如,通常把顯示器稱為標准輸出文件,printf 就是向這個文件輸出,把鍵盤稱為標准輸入文件,scanf 就是從這個文件獲取數據。
文件 | 硬件設備 |
---|---|
stdin | 標准輸入文件,一般指鍵盤;scanf()、getchar() 等函數默認從 stdin 獲取輸入。 |
stdout | 標准輸出文件,一般指顯示器;printf()、putchar() 等函數默認向 stdout 輸出數據。 |
stderr | 標准錯誤文件,一般指顯示器;perror() 等函數默認向 stderr 輸出數據(后續會講到)。 |
stdprn | 標准打印文件,一般指打印機。 |
我們不去探討硬件設備是如何被映射成文件的,大家只需要記住,在C語言中硬件設備可以看成文件,有些輸入輸出函數不需要你指明到底讀寫哪個文件,系統已經為它們設置了默認的文件,當然你也可以更改,例如讓 printf 向磁盤上的文件輸出數據。操作文件的正確流程為:打開文件 --> 讀寫文件 --> 關閉文件。文件在進行讀寫操作之前要先打開,使用完畢要關閉。
所謂打開文件,就是獲取文件的有關信息,例如文件名、文件狀態、當前讀寫位置等,這些信息會被保存到一個 FILE 類型的結構體變量中。關閉文件就是斷開與文件之間的聯系,釋放結構體變量,同時禁止再對該文件進行操作。
在C語言中,文件有多種讀寫方式,可以一個字符一個字符地讀取,也可以讀取一整行,還可以讀取若干個字節。文件的讀寫位置也非常靈活,可以從文件開頭讀取,也可以從中間位置讀取。
文件流
在《 載入內存,讓程序運行起來 》一文中提到,所有的文件(保存在磁盤)都要載入內存才能處理,所有的數據必須寫入文件(磁盤)才不會丟失。數據在文件和內存之間傳遞的過程叫做 文件流 ,類似水從一個地方流動到另一個地方。數據從文件復制到內存的過程叫做 輸入流 ,從內存保存到文件的過程叫做 輸出流 。文件是數據源的一種,除了文件,還有數據庫、網絡、鍵盤等;數據傳遞到內存也就是保存到C語言的變量(例如整數、字符串、數組、緩沖區等)。我們把數據在數據源和程序(內存)之間傳遞的過程叫做 數據流(Data Stream) 。相應的,數據從數據源到程序(內存)的過程叫做 輸入流(Input Stream) ,從程序(內存)到數據源的過程叫做 輸出流(Output Stream) 。
輸入輸出(Input output,IO)是指程序(內存)與外部設備(鍵盤、顯示器、磁盤、其他計算機等)進行交互的操作。幾乎所有的程序都有輸入與輸出操作,如從鍵盤上讀取數據,從本地或網絡上的文件讀取數據或寫入數據等。通過輸入和輸出操作可以從外界接收信息,或者是把信息傳遞給外界。
我們可以說,打開文件就是打開了一個流。
2.C語言文件的打開與關閉
在C語言中,文件操作都是由庫函數來完成的,這節介紹文件的打開和關閉。
文件的打開(fopen函數)
fopen() 函數用來打開一個文件,它的原型為:FILE *fopen(char *filename, char *mode);
filename
為文件名(包括文件路徑),
mode
為打開方式,它們都是字符串。fopen() 會獲取文件信息,包括文件名、文件狀態、當前讀寫位置等,並將這些信息保存到一個FILE類型的結構體變量中,然后將該變量的地址返回。
FILE是在stdio.h頭文件中定義的一個結構體,用來保存文件信息。如果希望接收 fopen() 的返回值,就需要定義一個 FILE 類型的指針。例如:
FILE *fp = ("demo.txt", "r");表示以“只讀”方式打開當前目錄下的 demo.txt 文件,並使 fp 指向該文件,這樣就可以通過 fp 來操作 demo.txt 了。fp 通常被稱為 文件指針 。又如:
FILE *fp = fopen("D:\\demo.txt","rb");表示以二進制方式打開 D 盤下的 demo.txt 文件,允許讀和寫。
打開方式(mode)有多種,見下表:
打開方式 | 說明 |
---|---|
r | 以只讀方式打開文件,只允許讀取,不允許寫入。該文件必須存在。 |
r+ | 以讀/寫方式打開文件,允許讀取和寫入。該文件必須存在。 |
rb+ | 以讀/寫方式打開一個二進制文件,允許讀/寫數據。 |
rt+ | 以讀/寫方式打開一個文本文件,允許讀和寫。 |
w | 以只寫方式打開文件,若文件存在則長度清為0,即該文件內容消失,若不存在則創建該文件。 |
w+ | 以讀/寫方式打開文件,若文件存在則文件長度清為零,即該文件內容會消失。若文件不存在則建立該文件。 |
a | 以追加的方式打開只寫文件。若文件不存在,則會建立該文件,如果文件存在,寫入的數據會被加到文件尾,即文件原先的內容會被保留(EOF符保留)。 |
a+ | 以追加方式打開可讀/寫的文件。若文件不存在,則會建立該文件,如果文件存在,則寫入的數據會被加到文件尾后,即文件原先的內容會被保留(原來的EOF符 不保留)。 |
wb | 以只寫方式打開或新建一個二進制文件,只允許寫數據。 |
wb+ | 以讀/寫方式打開或建立一個二進制文件,允許讀和寫。 |
wt+ | 以讀/寫方式打開或建立一個文本文件,允許讀寫。 |
at+ | 以讀/寫方式打開一個文本文件,允許讀或在文本末追加數據。 |
ab+ | 以讀/寫方式打開一個二進制文件,允許讀或在文件末追加數據。 |
文本文件和二進制文件的區別請查看: C語言fopen()打開文本文件與二進制文件的區別
幾點說明
1) 文件打開方式由r、w、a、t、b、+ 六個字符拼成,各字符的含義是:- r(read):讀
- w(write):寫
- a(append):追加
- t(text):文本文件,可省略不寫
- b(banary):二進制文件
- +:讀和寫
2) 如果沒有“b”字符,文件以文本方式打開。
3) 凡用“r”打開一個文件時,該文件必須已經存在。
4) 在打開一個文件時,如果出錯,fopen將返回一個空指針值NULL。在程序中可以用這一信息來判別是否完成打開文件的工作,並作相應的處理。因此常用以下程序段打開文件:
- if( (fp=fopen("D:\\demo.txt","rb") == NULL ){
- printf("Error on open D:\\demo.txt file!");
- getch();
- exit(1);
- }
5) 把一個文本文件讀入內存時,要將ASCII碼轉換成二進制碼,而把文件以文本方式寫入磁盤時,也要把二進制碼轉換成ASCII碼,因此文本文件的讀寫要花費較多的轉換時間。對二進制文件的讀寫不存在這種轉換。
6) 標准輸入文件 stdin(鍵盤)、標准輸出文件 stdout(顯示器)、標准錯誤文件 stderr(顯示器)是由系統打開的,可直接使用。
文件關閉(fclose函數)
文件一旦使用完畢,應該用fclose()
函數把文件關閉,以釋放相關資源,避免數據丟失。fclose() 的原型為:
int fclose(FILE *fp);fp 為文件指針。例如:
fclose(fp);文件正常關閉時,fclose() 的返回值為0,如果返回非零值則表示有錯誤發生。
3.C語言中文本文件與二進制文件的區別
在學習C語言fopen()函數后,知道它的第二個參數是標志字符串。如果字符串中出現'b',則表明是以打開二進制(binary)文件,否則是打開文本文件。
文本文件和二進制文件的本質區別
文件可以分為兩類: 二進制文件 和 字符(文本)文件 。從物理上講二進制文件和字符文件沒有區別,都是以二進制的形式保存在磁盤上。但是它們在文件的組織形式上不一樣,二進制文件有 文件頭(File Header) ,用以表明文件的大小、類型等信息,程序在處理二進制文件時一般會先分析文件頭,判斷文件是否合法,也就是說,文件頭后面的數據才是程序真正要處理的;字符文件沒有文件頭,第一個字節就是要顯示的內容。拿 BMP 文件舉例,其頭部的長度較為固定,前2字節用來記錄文件為BMP格式,接下來的8個字節用來記錄文件長度,再接下來的4字節用來記錄 BMP 文件頭的長度。
文本文件是基於字符編碼的,常見的編碼方式有 ASCII、UNICODE、UTF-8 等;指定編碼方式后,每個字節(也可以是每兩個、三個字節)所表示的字符是一樣的,任何程序都可以正確讀取。
二進制文件是自定義編碼的,也就是說,你可以根據具體程序指定每個字節(或者每兩個、三個字節)代表什么意思。例如,A 程序是圖像編輯器,指定 01001111 代表紅色,B 程序是視頻播放器,它把 01001111 理解為快進,顯然是不對的。
所以,字符文件是通用的,任何程序只要按照對應的編碼方式打開都可以正確顯示,二進制文件只有特定的程序才能處理。
文本文件和二進制文件都可以在屏幕上顯示,但是二進制文件的內容無法讀懂,大部分是亂碼。
fopen中的文本文件和二進制文件
在C語言中,二進制方式很簡單,讀文件時,會原封不動的讀出文件的全部內容,寫的時候,也是把內存緩沖區的內容原封不動的寫到文件中。而對文本文件的處理就不一樣了。Windows 和 DOS 下的文本文件以
CRLF(0X0D 0X0A)
作為換行符,而C語言本身以
LF(0X0A)
作為換行符,所以以文本方式寫入數據時,會將
LF(0X0A)
替換為
CRLF(0X0D 0X0A)
,而讀取數據時又會替換回來。
CR(0X0D)表示回車符,也就是 '\r';CL(0X0A)表示換行符,也就是 '\n'。在Linux和其他一些系統中,文本文件的換行符就是LF(0X0A),與C語言的換行符一樣。所以也就沒有了文本方式和二進制方式的區分,使不使用'b'標志都是一樣的。
這是由於不同操作系統對文本文件換行符的定義,和C語言中換行符的定義有所不同而造成的。在Windows下,C語言的輸入輸出函數會自動進行 CRLF 和 LF 的轉換,而Linux等就不必了。
另外,以文本方式打開時,遇到結束符
CTRLZ(0x1A)
就認為文件已經結束。所以,若使用文本方式打開二進制文件,就很容易出現文件讀不完整,或內容不對的錯誤。即使是用文本方式打開文本文件,也要謹慎使用,比如復制文件,就不應該使用文本方式。
綜上所述:二進制和文本模式的區別就在於對換行符和一些非可見字符的轉化上,如非必要,是使用二進制讀取會比較安全一些。
4.C語言以字符形式讀寫文件
在C語言中,讀寫文件比較靈活,既可以每次讀寫一個字符,也可以讀寫一個字符串,甚至是任意字節的數據(數據塊)。本節介紹以字符形式讀寫文件。
以字符形式讀寫文件時,每次可以從文件中讀取一個字符,或者向文件中寫入一個字符。主要使用兩個函數:fgetc()
和fputc()
。
字符讀取函數 fgetc
fgetc 是 file get char 的縮寫,意思是從指定的文件中讀取一個字符。它的原型為:int fgetc (FILE *fp);fp 為文件指針。fgetc() 讀取成功時返回讀取到的字符,讀取到文件末尾或讀取失敗時返回
EOF
。
EOF 是 end of file 的縮寫,表示文件末尾,是在 stdio.h 中定義的宏,它的值是一個負數,往往是 -1。返回值類型之所以為 int,就是為了容納這個負數(char不能是負數)。
EOF 不絕對是 -1,也可以是其他負數,這要看編譯器的實現。fgetc() 使用舉例:
- char ch;
- FILE*fp = fopen("D:\\demo.txt", "r+");
- ch = fgetc(fp);
D:\\demo.txt
文件中讀取一個字符,並保存到變量ch中。
在文件內部有一個位置指針,用來指向當前讀寫到的位置,也就是讀寫到第幾個字節。在文件打開時,該指針總是指向文件的第一個字節。使用fgetc 函數后,該指針會向后移動一個字節,所以可以連續多次使用fgetc讀取多個字符。
注意:這個文件內部的位置指針與C語言中的指針不是一回事。位置指針僅僅是一個標志,表示文件讀寫到的位置,也就是讀寫到第幾個字節,它不表示地址。文件每讀寫一次,位置指針就會移動一次,它不需要你在程序中定義和賦值,而是由系統自動設置,對用戶是透明的。
【示例】在屏幕上顯示 D:\\demo.txt 文件的內容。
- #include<stdio.h>
- int main(){
- FILE*fp;
- char ch;
- //如果文件不存在,給出提示並退出
- if( (fp=fopen("D:\\demo.txt","rt")) == NULL ){
- printf("Cannot open file, press any key to exit!");
- getch();
- exit(1);
- }
- //每次讀取一個字節,直到讀取完畢
- while( (ch=fgetc(fp)) != EOF ){
- putchar(ch);
- }
- putchar('\n'); //輸出換行符
- fclose(fp);
- return 0;
- }
該程序的功能是從文件中逐個讀取字符,在屏幕上顯示,直到讀取完畢。
程序第14行是關鍵,while 循環的條件為
(ch=fgetc(fp)) != EOF
。fget() 每次從位置指針所在的位置讀取一個字符,並保存到變量 ch,位置指針向后移動一個字節。當文件指針移動到文件末尾時,fget() 就無法讀取字符了,於是返回 EOF,表示文件讀取結束了。
對EOF的說明
EOF 本來表示文件末尾,意味着讀取結束,但是很多函數在讀取出錯時也返回 EOF,那么當返回EOF時,到底是文件讀取完畢了還是讀取出錯了?我們可以借助 stdio.h 中的兩個函數來判斷,分別是 feof() 和 ferror()。feof() 函數用來判斷文件內部指針是否指向了文件末尾,它的原型是:
int feof ( FILE * fp );當指向文件末尾時返回非零值,否則返回零值。
ferror() 函數用來判斷文件操作是否出錯,它的原型是:
int ferror ( FILE *fp );出錯時返回非零值,否則返回零值。
需要說明的是,文件出錯是非常少見的情況,上面的示例基本能夠保證將文件內的數據讀取完畢。如果追求完美,也可以加上判斷並給出提示:
- #include<stdio.h>
- int main(){
- FILE*fp;
- char ch;
- //如果文件不存在,給出提示並退出
- if( (fp=fopen("D:\\demo.txt","rt")) == NULL ){
- printf("Cannot open file, press any key to exit!");
- getch();
- exit(1);
- }
- //每次讀取一個字節,直到讀取完畢
- while( (ch=fgetc(fp)) != EOF ){
- putchar(ch);
- }
- putchar('\n'); //輸出換行符
- if(ferror(fp)){
- puts("讀取出錯");
- }else{
- puts("讀取成功");
- }
- fclose(fp);
- return 0;
- }
字符寫入函數fputc
fputc 是 file output char 的所以,意思是向指定的文件中寫入一個字符。調用的形式為:int fputc ( int ch, FILE *fp );ch 為要寫入的字符,fp 為文件指針。fputc() 寫入成功時返回寫入的字符,失敗時返回EOF,返回值類型為 int 也是為了容納這個負數。例如:
fputc('a', fp);或者:
char ch = 'a'; fputc(ch, fp);表示把字符 'a' 寫入fp所指向的文件中。
兩點說明
1) 被寫入的文件可以用寫、讀寫、追加方式打開,用寫或讀寫方式打開一個已存在的文件時將清除原有的文件內容,並將寫入的字符放在文件開頭。如需保留原有文件內容,並把寫入的字符放在文件末尾,就必須以追加方式打開文件。不管以何種方式打開,被寫入的文件若不存在時則創建該文件。2) 每寫入一個字符,文件內部位置指針向后移動一個字節。
【示例】從鍵盤輸入一行字符,寫入文件。
- #include<stdio.h>
- int main(){
- FILE*fp;
- char ch;
- //判斷文件是否成功打開
- if( (fp=fopen("D:\\demo.txt","wt+")) == NULL ){
- printf("Cannot open file, press any key to exit!\n");
- getch();
- exit(1);
- }
- printf("Input a string:\n");
- //每次從鍵盤讀取一個字符並寫入文件
- while ( (ch=getchar()) != '\n' ){
- fputc(ch,fp);
- }
- fclose(fp);
- return 0;
- }
讀字符串函數fgets
fgets() 函數用來從指定的文件中讀取一個字符串,並保存到字符數組中,它的原型為:char *fgets ( char *str, int n, FILE *fp );str 為字符數組,n 為要讀取的字符數目,fp 為文件指針。
返回值:讀取成功時返回字符數組首地址,也即 str;讀取失敗時返回 NULL;如果開始讀取時文件內部指針已經指向了文件末尾,那么將讀取不到任何字符,也返回 NULL。
注意,讀取到的字符串會在末尾自動添加 '\0',n 個字符也包括 '\0'。也就是說,實際只讀取到了 n-1 個字符,如果希望讀取 100 個字符,n 的值應該為 101。例如:
- #define N 101
- char str[N];
- FILE*fp = fopen("D:\\demo.txt", "r");
- fgets(str, N, fp);
需要重點說明的是,在讀取到 n-1 個字符之前如果出現了換行,或者讀到了文件末尾,則讀取結束。這就意味着,不管n的值多大,fgets() 最多只能讀取一行數據,不能跨行。在C語言中,沒有按行讀取文件的函數,我們可以借助 fgets(),將n的值設置地足夠大,每次就可以讀取到一行數據。
【示例】一行一行地讀取文件。
- #include <stdio.h>
- #include <stdlib.h>
- #define N 100
- int main(){
- FILE*fp;
- char str[N+1];
- if( (fp=fopen("d:\\demo.txt","rt")) == NULL ){
- printf("Cannot open file, press any key to exit!\n");
- getch();
- exit(1);
- }
- while(fgets(str, N, fp) != NULL){
- printf("%s", str);
- }
- fclose(fp);
- system("pause");
- return 0;
- }
C語言中文網
http://c.biancheng.net
一個學習編程的好網站!

fgets() 遇到換行時,會將換行符一並讀取到當前字符串。該示例的輸出結果之所以和 demo.txt 保持一致,該換行的地方換行,就是因為 fgets() 能夠讀取到換行符。而 gets() 不一樣,它會忽略換行符。
寫字符串函數fputs
fputs() 函數用來向指定的文件寫入一個字符串,它的原型為:int fputs( char *str, FILE *fp );str 為要寫入的字符串,fp 為文件指針。寫入成功返回非負數,失敗返回EOF。例如:
- char *str = "http://c.biancheng.net";
- FILE*fp = fopen("D:\\demo.txt", "at+");
- fputs(str, fp);
【示例】向上例中建立的 d:\\demo.txt 文件中追加一個字符串。
- #include<stdio.h>
- int main(){
- FILE*fp;
- char str[102] = {0}, strTemp[100];
- if( (fp=fopen("D:\\demo.txt", "at+")) == NULL ){
- printf("Cannot open file, press any key to exit!\n");
- getch();
- exit(1);
- }
- printf("Input a string:");
- gets(strTemp);
- strcat(str, "\n");
- strcat(str, strTemp);
- fputs(str, fp);
- fclose(fp);
- return 0;
- }
C C++ Java Linux Shell
,打開 D:\\demo.txt,文件內容為:
C語言中文網
http://c.biancheng.net
一個學習編程的好網站!
C C++ Java Linux Shell
fread() 函數用來從指定文件中讀取塊數據。所謂塊數據,也就是若干個字節的數據,可以是一個字符,可以是一個字符串,可以是多行數據,並沒有什么限制。fread() 的原型為:
size_t fread ( void *ptr, size_t size, size_t count, FILE *fp );fwrite() 函數用來向文件中寫入塊數據,它的原型為:
size_t fwrite ( void * ptr, size_t size, size_t count, FILE *fp );對參數的說明:
- ptr 為內存區塊的指針,它可以是數組、變量、結構體等。fread() 中的 ptr 用來存放讀取到的數據,fwrite() 中的 ptr 用來存放要寫入的數據。
- size:表示每個數據塊的字節數。
- count:表示要讀寫的數據塊的塊數。
- fp:表示文件指針。
- 理論上,每次讀寫 size*count 個字節的數據。
size_t 是在 stddef.h 頭文件中使用 typedef 定義的數據類型,表示無符號整數,也即非負數,常用來表示數量。
返回值:返回成功讀寫的塊數,也即 count。如果返回值小於 count:
- 對於 fwrite() 來說,肯定發生了寫入錯誤,可以用 ferror() 函數檢測。
- 對於 fread() 來說,可能讀到了文件末尾,可能發生了錯誤,可以用 ferror() 或 feof() 檢測。
【示例】從鍵盤輸入一個數組,將數組寫入文件再讀取出來。
- #include<stdio.h>
- #define N 5
- int main(){
- //從鍵盤輸入的數據放入a,從文件讀取的數據放入b
- int a[N], b[N];
- int i, size = sizeof(int);
- FILE*fp;
- if( (fp=fopen("D:\\demo.txt", "rb+")) == NULL ){
- printf("Cannot open file, press any key to exit!\n");
- getch();
- exit(1);
- }
- //從鍵盤輸入數據 並保存到數組a
- for(i=0; i<N; i++){
- scanf("%d", &a[i]);
- }
- //將數組a的內容寫入到文件
- fwrite(a, size, N, fp);
- //將文件中的位置指針重新定位到文件開頭
- rewind(fp);
- //從文件讀取內容並保存到數組b
- fread(b, size, N, fp);
- //在屏幕上顯示數組b的內容
- for(i=0; i<N; i++){
- printf("%d ", b[i]);
- }
- printf("\n");
- fclose(fp);
- return 0;
- }
23 409 500 100 222↙
23 409 500 100 222
fwrite()/fread() 函數直接操作字節,建議使用二進制方式打開文件。請閱讀《 C語言中文本文件與二進制文件的區別 》了解更多。
打開 D:\\demo.txt,發現文件內容根本無法閱讀。這是因為我們使用
"rb+"
方式打開文件,數據以二進制形式寫入文件,一般無法閱讀。
數據寫入完畢后,位置指針在文件的末尾,要想讀取數據,必須將文件指針移動到文件開頭,這就是
rewind(fp);
的作用。更多關於rewind函數的內容請點擊:
C語言rewind函數
。
文件的后綴不一定是 .txt,它可以是任意的,你可以自己命名,例如 demo.ddd、demo.doc、demo.diy 等。【示例】從鍵盤輸入兩個學生數據,寫入一個文件中,再讀出這兩個學生的數據顯示在屏幕上。
- #include<stdio.h>
- #define N 2
- structstu{
- char name[10]; //姓名
- int num; //學號
- int age; //年齡
- float score; //成績
- }boya[N], boyb[N], *pa, *pb;
- int main(){
- FILE*fp;
- int i;
- pa = boya;
- pb = boyb;
- if( (fp=fopen("d:\\demo.txt", "wb+")) == NULL ){
- printf("Cannot open file, press any key to exit!\n");
- getch();
- exit(1);
- }
- //從鍵盤輸入數據
- printf("Input data:\n");
- for(i=0; i<N; i++,pa++){
- scanf("%s %d %d %f",pa->name, &pa->num,&pa->age, &pa->score);
- }
- //將數組 boya 的數據寫入文件
- fwrite(boya, sizeof(structstu), N, fp);
- //將文件指針重置到文件開頭
- rewind(fp);
- //從文件讀取數據並保存到數據 boyb
- fread(boyb, sizeof(structstu), N, fp);
- //輸出數組 boyb 中的數據
- for(i=0; i<N; i++,pb++){
- printf("%s %d %d %f\n", pb->name, pb->num, pb->age, pb->score);
- }
- fclose(fp);
- return 0;
- }
Input data: Tom 2 15 90.5↙ Hua 1 14 99↙ Tom 2 15 90.500000 Hua 1 14 99.000000
這兩個函數的原型為:
int fscanf ( FILE *fp, char * format, ... ); int fprintf ( FILE *fp, char * format, ... );fp 為文件指針,format 為格式控制字符串,... 表示參數列表。與 scanf() 和 printf() 相比,它們僅僅多了一個 fp 參數。例如:
- FILE*fp;
- int i, j;
- char *str, ch;
- fscanf(fp, "%d %s", &i, str);
- fprintf(fp,"%d %c", j, ch);
【示例】用 fscanf 和 fprintf 函數來完成對學生信息的讀寫。
- #include<stdio.h>
- #define N 2
- structstu{
- char name[10];
- int num;
- int age;
- float score;
- } boya[N], boyb[N], *pa, *pb;
- int main(){
- FILE*fp;
- int i;
- pa=boya;
- pb=boyb;
- if( (fp=fopen("D:\\demo.txt","wt+")) == NULL ){
- printf("Cannot open file, press any key exit!");
- getch();
- exit(1);
- }
- //從鍵盤讀入數據,保存到boya
- printf("Input data:\n");
- for(i=0; i<N; i++,pa++){
- scanf("%s %d %d %f", pa->name, &pa->num, &pa->age, &pa->score);
- }
- pa = boya;
- //將boya中的數據寫入到文件
- for(i=0; i<N; i++,pa++){
- fprintf(fp,"%s %d %d %f\n", pa->name, pa->num, pa->age, pa->score);
- }
- //重置文件指針
- rewind(fp);
- //從文件中讀取數據,保存到boyb
- for(i=0; i<N; i++,pb++){
- fscanf(fp, "%s %d %d %f\n", pb->name, &pb->num, &pb->age, &pb->score);
- }
- pb=boyb;
- //將boyb中的數據輸出到顯示器
- for(i=0; i<N; i++,pb++){
- printf("%s %d %d %f\n", pb->name, pb->num, pb->age, pb->score);
- }
- fclose(fp);
- return 0;
- }
Input data: Tom 2 15 90.5↙ Hua 1 14 99↙ Tom 2 15 90.500000 Hua 1 14 99.000000打開 D:\\demo.txt,發現文件的內容是可以閱讀的,格式非常清晰。用 fprintf() 和 fscanf() 函數讀寫配置文件、日志文件會非常方便,不但程序能夠識別,用戶也可以看懂,可以手動修改。
如果將 fp 設置為 stdin,那么 fscanf() 函數將會從鍵盤讀取數據,與 scanf 的作用相同;設置為 stdout,那么 fprintf() 函數將會向顯示器輸出內容,與 printf 的作用相同。例如:
- #include<stdio.h>
- int main(){
- int a, b, sum;
- fprintf(stdout, "Input two numbers: ");
- fscanf(stdin, "%d %d", &a, &b);
- sum = a + b;
- fprintf(stdout, "sum=%d\n", sum);
- return 0;
- }
Input two numbers: 10 20↙
sum=30
實現隨機讀寫的關鍵是要按要求移動位置指針,這稱為文件的定位。
文件定位函數rewind和fseek
移動文件內部位置指針的函數主要有兩個,即 rewind() 和 fseek()。rewind() 用來將位置指針移動到文件開頭,前面已經多次使用過,它的原型為:
void rewind ( FILE *fp );fseek() 用來將位置指針移動到任意位置,它的原型為:
int fseek ( FILE *fp, long offset, int origin );參數說明:
1) fp 為文件指針,也就是被移動的文件。
2) offset 為偏移量,也就是要移動的字節數。之所以為 long 類型,是希望移動的范圍更大,能處理的文件更大。
3) origin 為起始位置,也就是從何處開始計算偏移量。C語言規定的起始位置有三種,分別為文件開頭、當前位置和文件末尾,每個位置都用對應的常量來表示:
起始點 | 常量名 | 常量值 |
---|---|---|
文件開頭 | SEEK_SET | 0 |
當前位置 | SEEK_CUR | 1 |
文件末尾 | SEEK_END | 2 |
fseek(fp, 100, 0);值得說明的是,fseek() 一般用於二進制文件,在文本文件中由於要進行轉換,計算的位置有時會出錯。
文件的隨機讀寫
在移動位置指針之后,就可以用前面介紹的任何一種讀寫函數進行讀寫了。 由於是二進制文件,因此常用 fread() 和 fwrite() 讀寫。【示例】從鍵盤輸入三組學生信息,保存到文件中,然后讀取第二個學生的信息。
- #include<stdio.h>
- #define N 3
- structstu{
- char name[10]; //姓名
- int num; //學號
- int age; //年齡
- float score; //成績
- }boys[N], boy, *pboys;
- int main(){
- FILE*fp;
- int i;
- pboys = boys;
- if( (fp=fopen("d:\\demo.txt", "wb+")) == NULL ){
- printf("Cannot open file, press any key to exit!\n");
- getch();
- exit(1);
- }
- printf("Input data:\n");
- for(i=0; i<N; i++,pboys++){
- scanf("%s %d %d %f", pboys->name, &pboys->num, &pboys->age, &pboys->score);
- }
- fwrite(boys, sizeof(structstu), N, fp); //寫入三條學生信息
- fseek(fp, sizeof(structstu), SEEK_SET); //移動位置指針
- fread(&boy, sizeof(structstu), 1, fp); //讀取一條學生信息
- printf("%s %d %d %f\n", boy.name, boy.num, boy.age, boy.score);
- fclose(fp);
- return 0;
- }
Input data:
Tom 2 15 90.5↙
Hua 1 14 99↙
Zhao 10 16 95.5↙
Hua 1 14 99.000000
實現文件復制的主要思路是:開辟一個緩沖區,不斷從原文件中讀取內容到緩沖區,每讀取完一次就將緩沖區中的內容寫入到新建的文件,直到把原文件的內容讀取完。
這里有兩個關鍵的問題需要解決:
1) 開辟多大的緩沖區合適?緩沖區過小會造成讀寫次數的增加,過大也不能明顯提高效率。目前大部分磁盤的扇區都是4K對齊的,如果讀寫的數據不是4K的整數倍,就會跨扇區讀取,降低效率,所以我們開辟4K的緩沖區。
2) 緩沖區中的數據是沒有結束標志的,如果緩沖區填充不滿,如何確定寫入的字節數?最好的辦法就是每次讀取都能返回讀取到的字節數。
fread() 的原型為:
size_t fread ( void *ptr, size_t size, size_t count, FILE *fp );它返回成功讀寫的塊數,該值小於等於 count。如果我們讓參數 size 等於1,那么返回的就是讀取的字節數。
注意:fopen()一定要以二進制的形式打開文件,不能以文本形式打開,否則系統會對文件進行一些處理,如果是文本文件,像.txt等,可能沒有問題,但如果是其他格式的文件,像.mp4, .rmvb, .jpg等,復制后就會出錯,無法讀取。
代碼實現:
- #include <stdio.h>
- #include <stdlib.h>
- int copyFile(char *fileRead, char *fileWrite);
- int main(){
- char fileRead[100]; // 要復制的文件名
- char fileWrite[100]; // 復制后的文件名
- // 獲取用戶輸入
- printf("要復制的文件:");
- scanf("%s", fileRead);
- printf("將文件復制到:");
- scanf("%s", fileWrite);
- // 進行復制操作
- if( copyFile(fileRead, fileWrite) ){
- printf("恭喜你,文件復制成功!\n");
- }else{
- printf("文件復制失敗!\n");
- }
- return 0;
- }
- /**
- * 文件復制函數
- * @param fileRead 要復制的文件
- * @param fileWrite 復制后文件的保存路徑
- * @return int 1: 復制成功;2: 復制失敗
- **/
- int copyFile(char *fileRead, char *fileWrite){
- FILE*fpRead; // 指向要復制的文件
- FILE*fpWrite; // 指向復制后的文件
- int bufferLen = 1024*4; // 緩沖區長度
- char *buffer = (char*)malloc(bufferLen); // 開辟緩存
- int readCount; // 實際讀取的字節數
- if( (fpRead=fopen(fileRead, "rb")) == NULL || (fpWrite=fopen(fileWrite, "wb")) == NULL ){
- printf("Cannot open file, press any key to exit!\n");
- getch();
- exit(1);
- }
- // 不斷從fileRead讀取內容,放在緩沖區,再將緩沖區的內容寫入fileWrite
- while( (readCount=fread(buffer, 1, bufferLen, fpRead)) > 0 ){
- fwrite(buffer, readCount, 1, fpWrite);
- }
- free(buffer);
- fclose(fpRead);
- fclose(fpWrite);
- return 1;
- }
要復制的文件:d://1.mp4 將文件復制到:d://2.mp4 恭喜你,文件復制成功!如果文件不存在,會給出提示,並終止程序:
要復制的文件:d://123.mp4 將文件復制到:d://333.mp4 d://cyuyan.txt: No such file or directory
第46行是文件復制的核心代碼。通過fread()函數,每次從 fileRead 文件中讀取 bufferLen 個字節,放到緩沖區,再通過fwrite()函數將緩沖區的內容寫入fileWrite文件。
正常情況下,每次會讀取bufferLen個字節,即readCount=bufferLen;如果文件大小不足bufferLen個字節,或者讀取到文件末尾,實際讀取到的字節就會小於bufferLen,即readCount<bufferLen。所以通過fwrite()寫入文件時,應該以readCount為准。
定義文件指針的一般形式為:
FILE *fp;這里的FILE,實際上是在stdio.h中定義的一個結構體,該結構體中含有文件名、文件狀態和文件當前位置等信息,fopen 返回的就是FILE類型的指針。
注意:FILE是文件緩沖區的結構,fp也是指向文件緩沖區的指針。
不同編譯器 stdio.h 頭文件中對 FILE 的定義略有差異,這里以標准C舉例說明:
- typedef struct_iobuf {
- int cnt; // 剩余的字符,如果是輸入緩沖區,那么就表示緩沖區中還有多少個字符未被讀取
- char *ptr; // 下一個要被讀取的字符的地址
- char *base; // 緩沖區基地址
- int flag; // 讀寫狀態標志位
- int fd; // 文件描述符
- // 其他成員
- } FILE;
我們知道,當我們從鍵盤輸入數據的時候,數據並不是直接被我們得到,而是放在了緩沖區中,然后我們從緩沖區中得到我們想要的數據 。如果我們通過 setbuf() 或 setvbuf() 函數將緩沖區設置10個字節的大小,而我們從鍵盤輸入了20個字節大小的數據,這樣我們輸入的前10個數據會放在緩沖區中,因為我們設置的緩沖區的大小只能夠裝下10個字節大小的數據,裝不下20個字節大小的數據。那么剩下的那10個字節大小的數據怎么辦呢?暫時放在了輸入流中。請看下圖:

上面的箭頭表示的區域就相當是一個輸入流,紅色的地方相當於一個開關,這個開關可以控制往深綠色區域(標注的是緩沖區)里放進去的數據,輸入20個字節的數據只往緩沖區中放進去了10個字節,剩下的10個字節的數據就被停留在了輸入流里!等待下去往緩沖區中放入!接下來系統是如何來控制這個緩沖區呢?
再說一下 FILE 結構體中幾個相關成員的含義:
cnt // 剩余的字符,如果是輸入緩沖區,那么就表示緩沖區中還有多少個字符未被讀取
ptr // 下一個要被讀取的字符的地址
base // 緩沖區基地址
在上面我們向緩沖區中放入了10個字節大小的數據,FILE結構體中的 cnt 變為了10 ,說明此時緩沖區中有10個字節大小的數據可以讀,同時我們假設緩沖區的基地址也就是 base 是0x00428e60 ,它是不變的 ,而此時 ptr 的值也為0x00428e60 ,表示從0x00428e60這個位置開始讀取數據,當我們從緩沖區中讀取5個數據的時候,cnt 變為了5 ,表示緩沖區還有5個數據可以讀,ptr 則變為了0x0042e865表示下次應該從這個位置開始讀取緩沖區中的數據 ,如果接下來我們再讀取5個數據的時候,cnt 則變為了0 ,表示緩沖區中已經沒有任何數據了,ptr 變為了0x0042869表示下次應該從這個位置開始從緩沖區中讀取數據,但是此時緩沖區中已經沒有任何數據了,所以要將輸入流中的剩下的那10個數據放進來,這樣緩沖區中又有了10個數據,此時 cnt 變為了10 ,注意了剛才我們講到 ptr 的值是0x00428e69 ,而當緩沖區中重新放進來數據的時候這個 ptr 的值變為了0x00428e60 ,這是因為當緩沖區中沒有任何數據的時候要將 ptr 這個值進行一下刷新,使其指向緩沖區的基地址也就是0x0042e860這個值!因為下次要從這個位置開始讀取數據!
在這里有點需要說明:當我們從鍵盤輸入字符串的時候需要敲一下回車鍵才能夠將這個字符串送入到緩沖區中,那么敲入的這個回車鍵(\r)會被轉換為一個換行符\n,這個換行符\n也會被存儲在緩沖區中並且被當成一個字符來計算!比如我們在鍵盤上敲下了123456這個字符串,然后敲一下回車鍵(\r)將這個字符串送入了緩沖區中,那么此時緩沖區中的字節個數是7 ,而不是6。
緩沖區的刷新就是將指針 ptr 變為緩沖區的基地址 ,同時 cnt 的值變為0 ,因為緩沖區刷新后里面是沒有數據的!
ftell()函數
ftell() 函數用來獲取文件內部指針(位置指針)距離文件開頭的字節數,它的原型為:long int ftell ( FILE * fp );注意:fp 要以二進制方式打開,如果以文本方式打開,函數的返回值可能沒有意義。
先使用 fseek() 將文件內部指針定位到文件末尾,再使用 ftell() 返回內部指針距離文件開頭的字節數,這個返回值就等於文件的大小。請看下面的代碼:
- long fsize(FILE*fp){
- fseek(fp, 0, SEEK_END);
- return ftell(fp);
- }
long size = fsize(fp); fread(buffer, 1, 1, fp);fread() 函數將永遠讀取不到內容。
所以,獲取到文件大小后還需要恢復文件內部指針,請看下面的代碼:
- long fsize(FILE*fp){
- long n;
- fpos_tfpos; //當前位置
- fgetpos(fp, &fpos); //獲取當前位置
- fseek(fp, 0, SEEK_END);
- n = ftell(fp);
- fsetpos(fp,&fpos); //恢復之前的位置
- return n;
- }
完整的示例:
- #include<stdio.h>
- #include<stdlib.h>
- #include<conio.h>
- long fsize(FILE*fp);
- int main(){
- long size = 0;
- FILE*fp = NULL;
- char filename[30] = "D:\\1.mp4";
- if( (fp = fopen(filename, "rb")) == NULL ){ //以二進制方式打開文件
- printf("Failed to open %s...", filename);
- getch();
- exit(EXIT_SUCCESS);
- }
- printf("%ld\n", fsize(fp));
- return 0;
- }
- long fsize(FILE*fp){
- long n;
- fpos_tfpos; //當前位置
- fgetpos(fp, &fpos); //獲取當前位置
- fseek(fp, 0, SEEK_END);
- n = ftell(fp);
- fsetpos(fp,&fpos); //恢復之前的位置
- return n;
- }
除了順序文件,還有索引文件、散列文件等,一般用於特殊領域,例如數據庫、高效文件系統等。
順序文件的存儲結構決定了它能夠高效讀取內容,但不能夠隨意插入、刪除和修改內容。例如在文件開頭插入100個字節的數據,那么原來文件的所有內容都要向后移動100個字節,這不僅是非常低效的操作,而且還可能覆蓋其他文件。因此C語言沒有提供插入、刪除、修改文件內容的函數,要想實現這些功能,只能自己編寫函數。
以插入數據為例,假設原來文件的大小為 1000 字節,現在要求在500字節處插入用戶輸入的字符串,那么可以這樣來實現:
1) 創建一個臨時文件,將后面500字節的內容復制到臨時文件;
2) 將原來文件的內部指針調整到500字節處,寫入字符串;
3) 再將臨時文件中的內容寫入到原來的文件(假設字符串的長度為100,那么此時文件內部指針在600字節處)。
刪除數據時,也是類似的思路。假設原來文件大小為1000字節,名稱為 demo.mp4,現在要求在500字節處往后刪除100字節的數據,那么可以這樣來實現:
1) 創建一個臨時文件,先將前500字節的數據復制到臨時文件,再將600字節之后的所有內容復制到臨時文件;
2) 刪除原來的文件,並創建一個新文件,命名為 demo.mp4;
3) 將臨時文件中的所有數據復制到 demo.mp4。
修改數據時,如果新數據和舊數據長度相同,那么設置好內部指針,直接寫入即可;如果新數據比舊數據長,相當於增加新內容,思路和插入數據類似;如果新數據比舊數據短,相當於減少內容,思路和刪除數據類似。實際開發中,我們往往會保持新舊數據長度一致,以減少編程的工作量,所以我們不再討論新舊數據長度不同的情況。
總起來說,本節重點討論數據的插入和刪除。
文件復制函數
在數據的插入刪除過程中,需要多次復制文件內容,我們有必要將該功能實現為一個函數,如下所示:- /**
- * 文件復制函數
- * @param fSource 要復制的原文件
- * @param offsetSource 原文件的位置偏移(相對文件開頭),也就是從哪里開始復制
- * @param len 要復制的內容長度,小於0表示復制offsetSource后邊的所有內容
- * @param fTarget 目標文件,也就是將文件復制到哪里
- * @param offsetTarget 目標文件的位置偏移,也就是復制到目標文件的什么位置
- * @return 成功復制的字節數
- **/
- long fcopy(FILE*fSource, long offsetSource, long len, FILE*fTarget, long offsetTarget){
- int bufferLen = 1024*4; // 緩沖區長度
- char *buffer = (char*)malloc(bufferLen); // 開辟緩存
- int readCount; // 每次調用fread()讀取的字節數
- long nBytes = 0; //總共復制了多少個字節
- int n = 0; //需要調用多少次fread()函數
- int i; //循環控制變量
- fseek(fSource, offsetSource, SEEK_SET);
- fseek(fTarget, offsetTarget, SEEK_SET);
- if(len<0){ //復制所有內容
- while( (readCount=fread(buffer, 1, bufferLen, fSource)) > 0 ){
- nBytes += readCount;
- fwrite(buffer, readCount, 1, fTarget);
- }
- }else{ //復制len個字節的內容
- n = (int)ceil((double)((double)len/bufferLen));
- for(i=1; i<=n; i++){
- if(len-nBytes < bufferLen){ bufferLen = len-nBytes; }
- readCount = fread(buffer, 1, bufferLen, fSource);
- fwrite(buffer, readCount, 1, fTarget);
- nBytes += readCount;
- }
- }
- fflush(fTarget);
- free(buffer);
- return nBytes;
- }
fcopy(fSource, 0, -1, fTarget, 0);
文件內容插入函數
請先看代碼:- /**
- * 向文件中插入內容
- * @param fp 要插入內容的文件
- * @param buffer 緩沖區,也就是要插入的內容
- * @param offset 偏移量(相對文件開頭),也就是從哪里開始插入
- * @param len 要插入的內容長度
- * @return 成功插入的字節數
- **/
- int finsert(FILE*fp, long offset, void *buffer, int len){
- long fileSize = fsize(fp);
- FILE*fpTemp; //臨時文件
- if(offset>fileSize || offset<0 || len<0){ //插入錯誤
- return -1;
- }
- if(offset == fileSize){ //在文件末尾插入
- fseek(fp, offset, SEEK_SET);
- if(!fwrite(buffer, len, 1, fp)){
- return -1;
- }
- }
- if(offset < fileSize){ //從開頭或者中間位置插入
- fpTemp = tmpfile();
- fcopy(fp, 0, offset, fpTemp, 0);
- fwrite(buffer, len, 1, fpTemp);
- fcopy(fp, offset, -1, fpTemp, offset+len);
- freopen(FILENAME, "wb+", fp );
- fcopy(fpTemp, 0, -1, fp, 0);
- fclose(fpTemp);
- }
- return 0;
- }
1) fsize() 是在《 C語言獲取文件大小(長度) 》自定義的函數,用來獲取文件大小(以字節計)。
2) 第17行判斷數據的插入位置,如果是在文件末尾,就非常簡單了,直接用 fwrite() 寫入即可。
3) 如果從文件開頭或中間插入,就得創建臨時文件。
tmpfile() 函數用來創建一個臨時的二進制文件,可以讀取和寫入數據,相當於 fopen() 函數以"wb+"方式打開文件。該臨時文件不會和當前已存在的任何文件重名,並且會在調用 fclose() 后或程序結束后自動刪除。
文件內容刪除函數
請看下面的代碼:- int fdelete(FILE*fp, long offset, int len){
- long fileSize = getFileSize(fp);
- FILE*fpTemp;
- if(offset>fileSize || offset<0 || len<0){ //錯誤
- return -1;
- }
- fpTemp = tmpfile();
- fcopy(fp, 0, offset, fpTemp, 0); //將前offset字節的數據復制到臨時文件
- fcopy(fp, offset+len, -1, fpTemp, offset); //將offset+len之后的所有內容都復制到臨時文件
- freopen(FILENAME, "wb+", fp ); //重新打開文件
- fcopy(fpTemp, 0, -1, fp, 0);
- fclose(fpTemp);
- return 0;
- }
(1)size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
其中,ptr:指向保存結果的指針;size:每個數據類型的大小;count:數據的個數;stream:文件指針
函數返回讀取數據的個數。
(2)size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
其中,ptr:指向保存數據的指針;size:每個數據類型的大小;count:數據的個數;stream:文件指針
函數返回寫入數據的個數。