????????文件操作是C語言編程中不可或缺的一部分,它使得程序能夠持久化存儲數據,并在需要時高效讀寫。本文將從基礎概念到實戰技巧,系統講解C語言文件操作的核心知識點,并結合代碼示例幫助讀者深入理解。
一. 為什么需要文件操作?
? ? ? ? 程序運行時,數據存儲在內存中,一旦程序結束,內存數據就會被釋放。文件操作解決了數據的持久化問題,例如:
? ? ? ? 保存用戶配置:如游戲的存檔和設置。
? ? ? ? 處理大規模數據:如日志文件或數據庫的讀寫。
? ? ? ? 跨進程通信:通過文件共享數據。
? ? ? ? 二. 文件類型
? ? ? ? 先補充文件和文件名的概念:
? ? ? ? 文件:磁盤(硬盤)上的文件是文件。
? ? ? ? 文件名:也稱文件標識,例如C:\code\test.txt。由文件路徑、文件名主干、文件后綴三部分組成,以便用戶識別和引用。?
? ? ? ? 在程序設計中,我們一般講兩種文件:程序文件和數據文件(從文件功能的角度分類)。
1. 程序文件
? ? ? ? 程序文件包含三種:
? ? ? ? 源文件(.c):開發者編寫的代碼文件。
? ? ? ? 目標文件(.obj):編譯后的中間文件。
? ? ? ? 可執行文件(.exe):鏈接后直接運行的程序。?
? ? ? ? 2. 數據文件?
? ? ? ? 文件的內容不一定是程序,而是程序運行時讀寫的數據,比如程序運行需要從中讀取數據的文件,或者輸出內容的文件。這就是數據文件,也是本章討論的重點。
? ? ? ? 數據文件包含兩種:
? ? ? ? 文本文件:如.txt、.csv,內容為ASCII字符。
? ? ? ? 二進制文件:如圖片、音頻,內容為二進制數據。
? ? ? ? 不同數據存儲的數據文件類型不同,字符型數據一律以ASCII形式存儲,數值型數據可以用ASCII形式存儲,也可以使用二進制形式存儲。
? ? ? ? 例如,十進制整型10000,二進制為00000000 00000000 00100111 00010000。如果以ASCII碼的形式輸出到磁盤(文本文件),為“1” “0” “0” “0” “0”,磁盤中占用5個字節(每個字符1個字節);如果以二進制形式輸出(二進制文件),為0x10,0x27,0x00,0x00,則在磁盤中只占4個字節。
????????測試代碼:
#include <stdio.h>int main()
{int a = 10000;FILE* pf = fopen("test.txt", "wb");//wb - write binaryfwrite(&a, 4, 1, pf);//二進制的形式寫到文件中fclose(pf);pf = NULL;return 0;
}
? ? ? ? 這段代碼創建了一個新文件,并將整數10000以二進制形式寫入文件中。運行后調試控制臺沒有任何結果,但是編譯器已經創建了一個二進制文件(.txt),可以通過添加現有項找到該二進制文件。
?????????我們通過選擇打開方式為二進制編譯器就能打開該二進制文件。
? ? ? ? 文件內容:
? ? ? ? 三. 流和標準流?
? ? ? ? 程序的數據需要輸出到各種外部設備,也需要從外部設備獲取數據,不同的外部設備的輸入輸出操作各不相同。為了方便程序員對各種設備進行方便的操作,我們抽象出了流的概念。?
? ? ? ? 1. 什么是流?
? ? ? ? 在C語言中,流(stream)是一個抽象概念,表示程序與外部設備(如鍵盤、顯示器、文件、網絡等)之間數據傳輸的通道。
? ? ? ? 可以將流想象成一條“數據河流”,數據在這條河中單向流動,因此有輸入流也有輸出流:
? ? ? ? 輸入流:數據從外部設備(如鍵盤、文件)流向程序。
? ? ? ? 輸出流:數據從程序流向外部設備(如顯示器、文件)。
? ? ? ? 2. 流的抽象意義?
? ? ? ? 流的抽象意義有兩點:?
? ? ? ? 1. 統一接口:不同設備的操作方式差異巨大(例如鍵盤輸入和文件讀取),但流通過統一接口(如fgetc、fprintf)屏蔽了底層細節,程序員無需關心設備的具體實現。
? ? ? ? 2. 緩沖機制:流通常與緩沖區(Buffer)結合使用。例如,數據從內存寫入磁盤時,先暫存到緩沖區,緩沖區滿后一次性寫入,提升IO效率(計算機系統在進行輸入/輸出操作時的性能表現)。?
? ? ? ? 3. 標準流?
? ? ? ? 但是我們從鍵盤輸入數據,向屏幕上輸出,并沒有打開流。是因為C語言程序啟動時,默認打開三個預定義的流,稱為標準流:
? ? ? ? 1. stdin - 標準輸入流,通常關聯鍵盤輸入,scanf函數就是從標準輸入流中讀取數據。
? ? ? ? 2. stdout - 標準輸出流,通常關聯顯示器輸出,printf函數就是將信息輸出到標準輸出流中。
? ? ? ? 3. stderr - 標準錯誤流,專用于輸出錯誤信息,默認也關聯顯示器。
?????????標準流的特點:?
? ? ? ? 1. 無需手動打開和關閉:程序啟動時自動創建,結束時自動釋放。
? ? ? ? 2. 數據類型為FILE*(稱為文件指針):C語言中,就是通過FILE*的文件指針來維護流的各種操作。?
?????????四. 文件指針
?????????每個被使用的文件,都在內存中開辟了一個相應的文件信息區,用來存放文件的相關信息,如文件的名字,文件狀態以及文件當前的位置等。
? ? ? ? 這些信息被保存在一個名為FILE的結構體變量中。該結構體是由系統聲明的,如VS2013編譯環境下提供的stdio.h頭文件中有以下的文件類型聲明:
struct _iobuf
{char *_ptr;int _cnt;char *_base;int _flag;int _file;int _charbuf;int _bufsize;char *_tmpfname;
};typedef struct _iobuf FILE;
? ? ? ? 不同的編譯器的FILE類型包含的內容不完全相同,但是大同小異。每當打開?個文件的時候,系統會根據文件的情況自動創建?個FILE結構的變量,并填充其中的信息,使用者不必關心細節。
?????????般都是通過?個FILE的指針來維護這個FILE結構的變量,這樣使用起來更加方便。下面我們可以創建?個FILE*的指針變量:?
FILE* pf;//文件指針變量
????????定義pf是?個指向FILE類型數據的指針變量。可以使pf指向某個文件的文件信息區(是?個結構體變量)。通過該文件信息區中的信息就能夠訪問該文件。也就是說,通過文件指針變量能夠間接找到與它關聯的文件。
? ? ? ? 示意圖:
? ? ? ? 五. 文件操作核心函數?
? ? ? ? 1. 打開與關閉文件?
? ? ? ? 文件在讀寫之前應該先打開文件,在使用結束后應該關閉文件。ANSI C規定使用fopen函數來打開文件,fclose函數來關閉文件。函數原型如下:
? ? ? ? fopen打開文件,需指定文件路徑(文件名,filename)和模式(mode)。打開文件失敗時,會返回NULL。
? ? ? ? 比如:
#include <stdio.h>int main()
{FILE* pf = fopen("test.txt", "r");//用只讀的方式打開文件if (pf == NULL){perror("fopen");return 1;}fclose(pf);pf = NULL;return 0;
}
? ? ? ? 這里并不存在文件名為test.txt的文件,所以pf為NULL。運行結果:
? ? ? ? 文件的打開模式有以下幾種:
模式 | 含義 | 文件不存在時的行為 |
"r"(只讀) | 為了讀取數據,打開一個已經存在的文本文件 | 出錯 |
"w"(只寫) | 為了寫入數據,打開一個文本文件(寫入會覆蓋原有內容) | 建立一個新文件 |
"a"(追加) | 向文本文件尾添加數據 | 建立一個新文件 |
"rb"(只讀) | 為了讀取數據,打開一個已經存在的二進制文件 | 出錯 |
"wb"(只寫) | 為了寫入數據,打開一個二進制文件 | 建立一個新文件 |
"ab"(追加) | 向二進制文件尾添加數據 | 建立一個新文件 |
"r+"(讀寫) | 為了讀和寫,打開一個文本文件 | 出錯 |
"w+"(讀寫) | 為了讀和寫,建立一個新的文本文件 | 建立一個新文件 |
"a+"(讀寫) | 打開一個文本文件,在文件尾進行讀寫? | 建立一個新文件 |
"rb+"(讀寫) | 為了讀和寫,打開一個二進制文件 | 出錯 |
"wb+"(讀寫) | 為了讀和寫,建立一個新的二進制文件 | 建立一個新文件 |
"ab+"(讀寫) | 打開一個二進制文件,在文件尾進行讀寫? | 建立一個新文件 |
? ? ? ? 比如:
#include <stdio.h>int main()
{int a = 10000;FILE* pf = fopen("test.txt", "w");//用只寫的方式打開文件if (pf == NULL){perror("fopen");return 1;}fclose(pf);pf = NULL;return 0;
}
? ? ? ? 運行后就會發現,程序文件相同目錄下生成了一個新文件,名為test.txt。
? ? ? ? 默認在當前目錄下讀或寫,也可以通過絕對路徑和相對路徑讓代碼按照指定路徑讀/寫文件:
? ? ? ? 絕對路徑:從根目錄開始,完整描述文件或目錄位置的路徑,以“ / ”或“ \ ”?分隔。例如,C:\User\Username\Documents\file.txt。
? ? ? ? 相對路徑:相對于當前工作目錄或文件位置的路徑。例如,當前路徑是C:\User\Username\Documents,那么file.txt的相對路徑可以是..\Picture\file.jpg,表示文件file.jpg位于當前目錄的上一級目錄Picture中( . 表示當前目錄,.. 表示上一級目錄,以此類推)。
? ? ? ? 注意:代碼中要連用兩個反斜杠,表示一個反斜杠,防止它被解釋為一個轉義序列符。
? ? ? ? 2. 順序讀寫函數?
? ? ? ? 當我們掌握了打開和關閉文件,就要來學習如何讀寫文件。
????????順序讀寫函數有以下幾種:
函數名 | 功能 | 適用于 |
fgetc | 字符輸入函數(讀取單個字符) | 所有輸入流 |
fputc | 字符輸出函數(寫入單個字符) | 所有輸出流 |
fgets | 文本行輸入函數(讀取一行文本) | 所有輸入流 |
fputs | 文本行輸出函數(寫入一行文本) | 所有輸出流 |
fscanf | 格式化輸入函數 | 所有輸入流 |
fprintf | 格式化輸出函數 | 所有輸出流 |
fread | 二進制輸入 | 文件輸入流 |
fwrite | 二進制輸出 | 文件輸出流 |
? ? ? ? 上面說的適用于所有輸出/入流,一般指適用于標準輸出/入流和其他輸出/入流(如文件輸出/入流)?。前六個函數是針對文本數據進行文件的輸出和輸出,最后兩個是針對二進制數據進行文件的輸出和出入。
? ? ? ? 2.1 fputc和fgetc
? ? ? ?fputc函數的原型:
? ? ? ? ?作用:fputc函數將字符character(傳遞參數是字符的ACSII碼值),寫入stream流(指向的對應文件信息區的指針),并前進位置指示器(即光標)。
? ? ? ? 返回值:如果寫入成功,會返回該字符的ACSII碼值;如果寫入失敗,會返回EOF。
? ? ? ? 例如:
#include <stdio.h>int main()
{FILE* pf = fopen("data.txt", "w");if (pf == NULL){perror("fopen");return 1;}//寫文件fputc('a', pf); fputc('b', pf);fputc('c', pf);fputc('d', pf);//關閉文件fclose(pf);pf = NULL;return 0;
}
? ? ? ? 這段代碼將字符a、b、c、d寫入data.txt中。運行之后就會發現,程序文件相同路徑下生成了一個data.txt文本文件,打開后會有如下內容:
? ? ? ? 當寫入第一個字符a時,光標就會移動到a的后面,隨后寫入字符b,再次移動光標,以此類推。?
? ? ? ?fgetc函數的原型:
? ? ? ??作用:fgetc函數從流中獲取字符,并前進光標。
? ? ? ? 返回值:如果獲取成功,會返回該字符的ACSII碼值(int類型);如果獲取字符失敗,會返回EOF。?
? ? ? ? 例如:
#include <stdio.h>int main()
{FILE* pf = fopen("data.txt", "r");if (pf == NULL){perror("fopen");return 1;}//讀文件(文件原有字符串“abcd”)int ch = fgetc(pf);printf("%c\n", ch);ch = fgetc(pf);printf("%c\n", ch);ch = fgetc(pf);printf("%c\n", ch);ch = fgetc(pf);printf("%c\n", ch);//也可以通過fgetc的返回值讀取文件中所有字符://while ((int ch = fgetc(pf)) != EOF)//{// printf("%c", ch);//}//關閉文件fclose(pf);pf = NULL;return 0;
}
? ? ? ? 這段代碼從data.txt中讀取了前四個字符,并依次打印。運行結果:
?????????當讀取第一個字符a時,光標就會移動到a的后面,隨后寫讀取字符b,再次移動光標,以此類推。?
? ? ? ? 再來看一個例子:
#include <stdio.h>int main()
{int ch = fgetc(stdin);putchar(ch);//打印一個字符,相當于printf("%c", )//getchar -- 讀取一個字符,相當于scanf("%c", )return 0;
}
? ? ? ? fgetc函數從標準輸入流stdin中獲取字符,putchar函數再將字符輸出。運行后會發現,控制臺窗口沒有輸出任何數據,光標停在首位。因為此時標準輸入流中沒有數據,我們可以通過鍵盤輸入字符,這個字符就會進入標準輸入流,并被獲取和打印。運行結果:
? ? ? ? 這說明,fgetc函數適用于所有輸入流,同樣的也可以證明fputc函數適用于所有輸出流,代碼如下:
#include <stdio.h>int main()
{int ch = fgetc(stdin);fputc(ch, stdout);return 0;
}
? ? ? ? 我們通過鍵盤輸入一個字符,對應的就會打印這個字符。運行結果:
?????????2.2?fputs和fgets
? ? ? ? fputs函數的原型:
? ? ? ? 作用:fputs函數將指針str指向的字符串寫入流中。
? ? ? ? 返回值:如果寫入成功,會返回一個非負值(non-negative value);如果寫入失敗,會返回EOF。?fputs函數將字符串寫入流中時,遇到“ \0 ”結束寫入。
? ? ? ? 例如:
#include <stdio.h>int main()
{FILE* pf = fopen("data.txt", "w");if (pf == NULL){perror("fopen");return 1;}//寫文件//寫入一行字符fputs("How are you?\n", pf);fputs("abcdefg\n", pf);//關閉文件fclose(pf);pf = NULL;return 0;
}
? ? ? ? 這里就會發現,data.txt多出了很多內容:
? ? ? ? 注意:
????????1. 寫入字符串含“\n”,寫入文件時就會換行。
? ? ? ? 2. 我們會發現,原本的“abcd”已經不見了,這就是“w”只寫模式的特點:寫入會覆蓋原有內容。?
? ? ? ? fgets函數的原型:
? ? ? ? 作用:從流中讀取字符,并將其作為字符串儲存到str中,直到讀取(num-1)個字符或者到達換行符或文件結束符(end-of-file)為止(以先發生的為準),并移動光標至讀取字符的后面?。當換行符使fgets停止讀取時,換行符仍被函數認為是一個有效字符,并包含在復制到str的字符串中。
? ? ? ? 返回值:如果成功,該函數返回str;如果發生讀取失敗,則返回的指針是空指針。
? ? ? ? 例如:
#include <stdio.h>int main()
{FILE* pf = fopen("data.txt", "r");if (pf == NULL){perror("fopen");return 1;}//讀文件,data.txt中有字符串“abcdefghijk”//讀取多個字符char ch[10];fgets(ch, 10, pf);//關閉文件fclose(pf);pf = NULL;return 0;
}
? ? ? ? 調試并打開監視:
? ? ? ? 這里說明,函數參數num為10時,實際上是只讀取了9個有效字符,和一個“\0”。把數組大小和num改為20,再調試并打開監視:?
? ? ? ? fgets讀取完所有字符后就不會再讀取。
? ? ? ? 再舉一個例子:
#include <stdio.h>int main()
{FILE* pf = fopen("data.txt", "r");if (pf == NULL){perror("fopen");return 1;}//讀文件//data.txt中有://hello world//hahahachar ch[20];fgets(ch, 20, pf);printf("%s", ch);//關閉文件fclose(pf);pf = NULL;return 0;
}
? ? ? ? 運行結果:
? ? ? ? 并沒有讀取到hahaha。但是調試并打開監視:
? ? ? ? 這里ch儲存了轉行符,所以只要再讀取一次就能打印hahaha:
? ? ? ? 2.3?fprintf和fscanf
? ? ? ? fprintf函數的原型:
?
? ? ? ? fprintf函數將具有一定格式的數據寫入流中。函數參數后面有省略號,這被稱為可變參數。printf函數的參數中也存在可變參數,例如:
printf("%d",10);
printf("hello");
printf("%d %s",10,:"hello");
? ? ? ? 參數的類型和數量都不同,所以函數參數用可變參數代替。fprintf的使用和printf非常相似,只是fprintf的函數參數比printf多了一個文件指針類型的流,因此使用fprintf函數并不困難,例如:
#include <stdio.h>int main()
{int age = 18;char name[20] = "zhangsan";double grades = 95.5;FILE* pf = fopen("data.txt", "w");if (pf == NULL){perror("fopen");return 1;}//寫文件fprintf(pf, "%d %s %.2lf", age, name, grades);//關閉文件fclose(pf);pf = NULL;return 0;
}
? ? ? ? 運行結果:
? ? ? ? 能夠按一定格式輸出,也就能按一定格式輸入,fscanf函數就能從文件中獲取數據。
? ? ? ? fscanf函數的原型如下:
? ? ? ? fscanf函數和scanf函數的參數相似(fscanf函數的參數多了“FILE* stream”),使用方法也很相似,例如:
#include <stdio.h>int main()
{int age;char name[20];double grades;FILE* pf = fopen("data.txt", "r");if (pf == NULL){perror("fopen");return 1;}//讀文件//文件原有://18 zhangsan 99.50//scanf("%d %s %lf", &age, name, &grades);--再加上流就是fscanf函數:fscanf(pf, "%d %s %lf", &age, name, &grades);//關閉文件fclose(pf);pf = NULL;return 0;
}
? ? ? ? 運行結果:
?????????2.4?fwrite和fread
? ? ? ? fwrite函數的原型:
???
?
? ? ? ? fwrite函數將數據以二進制形式寫入流中。該函數有以下參數:
? ? ? ? 1. const void* ptr:?ptr指向被寫的數據。
? ? ? ? 2. size_t size:被寫的數據中一個元素的長度(單位是字節)。
? ? ? ? 3. size_t count:元素的個數。
? ? ? ? 4. FILE* stream:寫入數據到stream流。
? ? ? ? 例如,將一個整型數組數據以二進制形式寫入data.txt文件中,代碼如下:
#include <stdio.h>int main()
{FILE* pf = fopen("data.txt", "wb");//以二進制寫的方式打開文件if (pf == NULL){perror("fopen");return 1;}//寫文件int arr[] = { 1,2,3,4,5,6,7,8,9,10 };fwrite(arr, sizeof(int), 10, pf);//關閉文件fclose(pf);pf = NULL;return 0;
}
? ? ? ? 運行后打開data.txt,會發現寫入了數據,但是并不能看出來是10個數字:
? ? ? ? 用文本文件的方式打開二進制文件,數據就會變為亂碼,但實際上數組中的十個整型數據已經寫入了data.txt。?
? ? ? ? 我們可以用fread函數讀取數據驗證想法,fread函數的原型如下:
? ? ? ? fread函數從流中讀取二進制數據。該函數的四個參數和fwrite函數一樣。例如:
#include <stdio.h>int main()
{FILE* pf = fopen("data.txt", "rb");//以二進制只讀的方式打開文件if (pf == NULL){perror("fopen");return 1;}//寫文件int arr[10] = {0};fread(arr, sizeof(int), 10, pf);int i = 0;for (i = 0; i < 10; i++){printf("%d ", arr[i]);}//關閉文件fclose(pf);pf = NULL;return 0;
}
? ? ? ? 運行結果:
? ? ? ? 說明fread函數成功從data.txt中讀取這十個數字,原本文件中確實儲存著這十個數字。?
? ? ? ? 如果讓fread函數讀取文本文件,就不會讀取到正確數據。例如,我們在data.txt中寫入十進制整型1到10,再用相同代碼讀取數據,運行結果:
? ? ? ? 讀取數據明顯不正確。所以要注意文本文件和二進制文件的區別,以及對應的數據之間的區別。
? ? ? ? 3. sscanf和sprintf
? ? ? ? 這里補充sscanf和sprintf函數。
????????我們介紹過printf、fprintf、scanf和fscanf函數:
? ? ? ? scanf?-- 針對stdin的格式化的輸入函數
? ? ? ? printf?-- 針對stdout的格式化的輸出函數
? ? ? ? fscanf -- 針對所有輸入流 格式化的輸入函數
? ? ? ? fprintf -- 針對所有輸出流的格式化的輸出函數
? ? ? ? sscanf和sprintf函數能夠實現有格式的數據與字符串之間的轉換:
? ? ? ? sscanf -- 從字符串中,按照格式提取格式化的數據
? ? ? ? sprintf -- 將帶有格式的數據,按照格式轉化成字符串
? ? ? ? sprintf函數的原型:
? ? ? ? sprintf函數寫格式化的數據到str指向的字符串中,也就是將格式化的數據轉換成字符串。例如:
#include <stdio.h>int main()
{int age = 18;char name[20] = "zhangsan";double grades = 95.5;char buf[120] = { 0 };//printf("%d %s %.1lf", age, name, grades)sprintf(buf, "%d %s %.1lf", age, name, grades);printf("%s\n", buf);return 0;
}
? ? ? ? 運行結果:
? ? ? ? sprintf函數將age、name和grades這三個不同類型的變量轉換成了字符串并寫入buf指向的字符串中。?
? ? ? ? sscanf函數的原型:
? ? ? ? sscanf函數和sprintf函數的作用相反,sscanf函數從字符串中讀取有格式的數據。例如:
#include <stdio.h>int main()
{int age = 18;char name[20] = "zhangsan";double grades = 95.5;char buf[120] = { 0 };sprintf(buf, "%d %s %.1f", age, name, grades);int age2 = 0;char name2[20] = "";double grades2 = 0;//scanf("%d %s %lf", &age2, &name2, &grades2);sscanf(buf, "%d %s %lf", &age2, &name2, &grades2);printf("%d\n", age2);printf("%s\n", name2);printf("%.1lf\n", grades2);return 0;
????????運行結果:
? ? ? ? sscanf函數從buf指向的字符串中,按%d %s %lf的格式順序讀取數據,并分別存儲在變量中。?
4. 文件的隨機讀寫
? ? ? ? 文件的讀寫,既支持順序讀寫,也支持隨機讀寫。比如文件中有“abcdef”,假設此時光標默認在f的右邊,我們可以通過一些函數讓光標移動到e的左側,以此為起始位置進行讀寫,這就是文件的隨機讀寫。?
? ? ? ? 4.1 fseek?
? ? ? ? fseek函數的原型:
? ? ? ? fseek函數可以根據文件指針的位置和偏移量來重新定位文件指針(即文件內容的光標)。該函數有三個參數:
? ? ? ? 1. FILE* stream:指向文件的流。
? ? ? ? 2. long int offset:相對于起始位置origin的偏移量(單位字節),向右偏移是正數,向左偏移是負數。
? ? ? ? 3. int origin:origin有三種情況:SEEK_SET(文件的起始處),SEEK_CUR(光標的當前位置),SEEK_END(文件的末尾處)。
? ? ? ? 例如,文件中有“abcdefghi”?:
int ch = fgetc(pf);
printf("%c\n",ch);
ch = fgetc(pf);
printf("%c\n",ch);
? ? ? ? 這樣就會打印a和b兩個字符。如果想要讀取a后立即讀取e,就可以用fseek函數重新定位光標(光標應該在e的左側):
fseek(pf,4,SEEK_SET);//從文件起始處,偏移量為4的地方
fseek(pf,3,SEEK_CUR);//從光標的當前位置(即a的右側),偏移量為3的地方
fseek(pf,-5,SEEK_END);//從文件末尾處,偏移量為-5(向左偏移,為負數)的地方
? ? ? ? 這三種寫法都是定位光標在e的左側。完整代碼:
#include <stdio.h>int main()
{int age;char name[20];double grades;FILE* pf = fopen("data.txt", "r");if (pf == NULL){perror("fopen");return 1;}//讀文件int ch = fgetc(pf);printf("%c\n", ch);fseek(pf, 4, SEEK_SET);//fseek(pf, 3, SEEK_CUR);//fseek(pf, -5, SEEK_END);ch = fgetc(pf);printf("%c\n", ch);//關閉文件fclose(pf);pf = NULL;return 0;
}
? ? ? ? 運行結果:
? ? ? ? 4.2 ftell?
? ? ? ? ftell函數的原型:
? ? ? ? ftell函數會返回文件指針相對于起始位置的偏移量(返回類型為long int)。?
? ? ? ? 例如:
#include <stdio.h>int main()
{int age;char name[20];double grades;FILE* pf = fopen("data.txt", "r");if (pf == NULL){perror("fopen");return 1;}//讀文件//文件原有內容:abcdefghiint ch = fgetc(pf);printf("%c\n", ch);fseek(pf, 4, SEEK_SET);ch = fgetc(pf);printf("%c\n", ch);printf("%ld\n", ftell(pf));//關閉文件fclose(pf);pf = NULL;return 0;
}
? ? ? ? 當第二次fgetc函數運行后,光標指在e和f之間,與起始位置的偏移量為5。?
? ? ? ? 運行結果:
4.3 rewind?
? ? ? ? rewind函數的原型:
? ? ? ? rewind函數可以讓文件指針的位置回到文件的起始位置。
? ? ? ? 例如:
#include <stdio.h>int main()
{int age;char name[20];double grades;FILE* pf = fopen("data.txt", "r");if (pf == NULL){perror("fopen");return 1;}//讀文件int ch = fgetc(pf);printf("%c\n", ch);fseek(pf, 4, SEEK_SET);ch = fgetc(pf);printf("%c\n", ch);rewind(pf);//光標再次回到起始位置ch = fgetc(pf);//此時讀取的就是字符aprintf("%c\n", ch);//關閉文件fclose(pf);pf = NULL;return 0;
}
? ? ? ? 第二次fgetc后,rewind函數讓光標再次指向了初始位置,則第三次fgetc函數讀取到的字符仍然是字符a。
運行結果:
? ? ? ? 以上就是文件的隨機讀寫中最重要的三個函數。?
六. 文件讀取結束的判定
? ? ? ? 在讀取文件時,我們可以利用“文件是否讀取結束”這一信息判斷是否繼續讀取文件。
? ? ? ? 例如:
#include <stdio.h>int main()
{int age;char name[20];double grades;FILE* pf = fopen("data.txt", "r");if (pf == NULL){perror("fopen");return 1;}//讀文件//文件有:"abcdefghi"int ch = 0;while ((ch = fgetc(pf)) != EOF){printf("%c\n", ch);}//關閉文件fclose(pf);pf = NULL;return 0;
}
? ? ? ? 運行結果:
? ? ? ? 這里就是循環判斷fgetc函數的返回值是否為EOF(文件結束標志),如果返回值不是EOF,說明文件還可以正常讀取,代碼就會繼續讀取并打印數據;如果返回值是EOF,說明文件已經讀取結束,跳出循環。
? ? ? ? 判定文件讀取結束,一般都是判斷函數的返回值:
? ? ? ? 1.文本文件(EOF/NULL):
?????????????????fgetc(函數返回數據的ASCII碼值):判斷返回值是否為EOF。
? ? ? ? ?????????fgets(函數返回str):判斷返回值是否為NULL。
? ? ? ? 2. 二進制文件(返回值是否小于實際要讀的個數):
? ? ? ? ? ? ? ? ?fread:判斷返回值是否小于實際要讀的個數。
? ? ? ? 文件讀取結束有兩種情況:
? ? ? ? 1. 正常讀取:遇到文件末尾而結束。
? ? ? ? 2.?異常讀取:發生讀取錯誤而結束。
? ? ? ? 想要知道是哪種情況導致文件讀取結束,就需要用到feof函數和ferror函數:
????????int feof ( FILE * stream ):如果文件讀取時遇到文件末尾,則返回非0的整型。
????????int ferror ( FILE * stream ):如果文件讀取時發生錯誤,則返回非0的整型。
? ? ? ? 例如(文本文件):
int main(void)
{FILE* pf = fopen("data.txt", "r");if (pf == NULL){perror("fopen");return 1;}//fgetc 當讀取失敗的時候或者遇到文件結束的時候,都會返回EOFint c; // 注意:int,非char,要求處理EOFwhile ((c = fgetc(pf)) != EOF) //I/O讀取文件循環{putchar(c);}//判斷是什么原因結束的if (ferror(pf))puts("I/O error when reading");else if (feof(pf))puts("End of file reached successfully");fclose(pf);pf = NULL;
}
? ? ? ? 運行結果:
? ? ? ? 根據返回結果,文件讀取結束是因為到達文件末尾。
? ? ? ? 例如(二進制文件):?
#include <stdio.h>enum { SIZE = 5 }; int main(void)
{double a[SIZE] = { 1.,2.,3.,4.,5. }; FILE * fp = fopen("test.bin", "wb"); // 必須??進制模式fwrite(a, sizeof *a, SIZE, fp); // 寫 double 的數組fclose(fp);double b[SIZE];fp = fopen("test.bin","rb");size_t ret_code = fread(b, sizeof *b, SIZE, fp); // 讀 double 的數組if(ret_code == SIZE) {//判斷返回值是否小于實際要讀的個數puts("Array read successfully, contents: ");for(int n = 0; n < SIZE; ++n)printf("%f ", b[n]);putchar('\n');} else { // error handlingif (feof(fp))printf("Error reading test.bin: unexpected end of file\n");else if (ferror(fp)) {perror("Error reading test.bin");}}fclose(fp);
}
? ? ? ? 這里就是判斷fread函數的返回值(ret_code)是否小于實際要讀的個數,如果不小于,代碼接著打印數據,?如果小于,代碼就不會再打印數據,而是用feof函數和ferror函數檢測錯誤情況。
? ? ? ? 運行結果:
? ? ? ? 由此可知,在文件讀取過程中,不能用feof函數和ferror函數的返回值直接來判斷文件是否結束,正確方法應該是判斷各種文件順序寫函數的返回值。
七. 文件緩沖區
????????ANSI C 標準采用“緩沖文件系統”?處理數據文件。
????????緩沖文件系統是指系統自動地在內存中為程序中每?個正在使用的文件開辟一塊“文件緩沖區”。從內存向磁盤輸出數據會先送到內存中的緩沖區,裝滿緩沖區后才?起送到磁盤上。如果從磁盤向計算機讀入數據,則從磁盤文件中讀取數據輸入到內存緩沖區(充滿緩沖區),然后再從緩沖區逐個地將數據送到程序數據區(程序變量等)。
????????緩沖區的大小根據C編譯系統決定的。?
? ? ? ? 示意圖:
? ? ? ? 例如:
#include <stdio.h>
#include <windows.h>
//VS2022 WIN11環境測試
int main()
{FILE*pf = fopen("test.txt", "w");fputs("abcdef", pf);//先將代碼放在輸出緩沖區printf("睡眠10秒-已經寫數據了,打開test.txt 文件,發現文件沒有內容\n");Sleep(10000);printf("刷新緩沖區\n");fflush(pf);//刷新緩沖區時,才將輸出緩沖區的數據寫到?件(磁盤)//注:fflush 在?版本的VS上不能使?了printf("再睡眠10秒-此時,再次打開test.txt 文件,文件有內容了\n");Sleep(10000);fclose(pf);//注:fclose在關閉?件的時候,也會刷新緩沖區pf = NULL;return 0;
}
? ? ? ? 根據代碼指示在不同時間段打開text.txt文件,就會發現文件內容的變化。
? ? ? ? 這里得出結論:因為有緩沖區的存在,C語言在操作文件的時候,需要做刷新緩沖區或者在文件操作結束的時候關閉文件。如果不做,可能導致讀寫文件的問題。
八. 總結
????????文件操作是C語言中實現數據持久化的核心技能。通過本文的學習,我們了解到文件的打開、讀寫、隨機訪問及錯誤處理等關鍵操作。?
? ? ? ? 掌握文件操作,讓你的程序真正“記住”數據!