C生萬物 | C語言檔案操作指南匯總【內附檔案外排序原始碼】

語言: CN / TW / HK

theme: channing-cyan

開啟掘金成長之旅!這是我參與「掘金日新計劃 · 2 月更文挑戰」的第 9 天,點選檢視活動詳情

一、為什麼使用檔案?

我們前面學習結構體時,寫了通訊錄的程式,當通訊錄執行起來的時候,可以給通訊錄中增加、刪除資料,此時資料是存放在記憶體中,當程式退出的時候,通訊錄中的資料自然就不存在了,等下次執行通訊錄程式的時候,資料又得重新錄入,如果使用這樣的通訊錄就很難受

所以就想到了通訊錄就應該把資訊記錄下來,只有我們自己選擇刪除資料的時候,資料才不復存在。這就涉及到了資料持久化的問題,我們一般資料持久化的方法有,把資料存放在【磁碟檔案】、存放到【資料庫】等方式

二、什麼是檔案?

==磁碟上的檔案是檔案==

但是在程式設計中,我們一般談的檔案有兩種:程式檔案、資料檔案(從檔案功能的角度來分類的)

1、程式檔案

包括源程式檔案(字尾為.c),目標檔案(windows環境字尾為.obj),可執行程式(windows環境字尾為.exe)。 - 【程式檔案】一般指的是我們建立工程時所編寫的程式碼,也就想下面這個【test.c】一樣

在這裡插入圖片描述

2、資料檔案

檔案的內容不一定是程式,而是程式執行時讀寫的資料,比如程式執行需要從中讀取資料的檔案,或者輸出內容的檔案。 - 【資料檔案】一般指通過程式去操縱的那個檔案

在這裡插入圖片描述 - 就想上面的這個【test.txt】就是一個數據檔案,通過【test.exe】執行起來時,記憶體中有有了資料,此時我們可以將資料寫到這個【test.txt】中,自然也可以從這個檔案中讀取資料到記憶體中

3、檔名

一個檔案要有一個唯一的檔案標識,以便使用者識別和引用

  • 檔名包含3部分:檔案路徑+檔名主幹+檔案字尾
  • [x] 例如:c:\code\test.txt
  • 為了方便起見,檔案標識常被稱為檔名。

三、檔案的開啟和關閉

1、檔案指標

緩衝檔案系統中,關鍵的概念是“檔案型別指標”,簡稱“檔案指標”

  • 每個被使用的檔案都在記憶體中開闢了一個相應的檔案資訊區,用來存放檔案的相關資訊(如檔案的名字,檔案狀態及檔案當前的位置等)。這些資訊是儲存在一個結構體變數中的。該結構體型別是有系統宣告的,取名【FILE】

例如,VS2019編譯環境提供的 stdio.h 標頭檔案中有以下的檔案型別申明:

c struct _iobuf { char *_ptr; int _cnt; char *_base; int _flag; int _file; int _charbuf; int _bufsiz; char *_tmpfname; }; typedef struct _iobuf FILE; FILE* pf;//檔案指標變數 - 不同的C編譯器的FILE型別包含的內容不完全相同,但是==大同小異==。每當開啟一個檔案的時候,系統會根據檔案的情況自動建立一個FILE結構的變數,並填充其中的資訊,使用者不必關心細節 - 一般都是通過一個FILE的指標來維護這個FILE結構的變數,這樣使用起來更加方便。我們來看看如何建立一個FILE*的指標變數

c FILE* pf;//檔案指標變數 - 定義pf是一個指向FILE型別資料的指標變數。可以使pf指向某個檔案的檔案資訊區(是一個結構體變數)。通過該檔案資訊區中的資訊就能夠訪問該檔案。也就是說,通過檔案指標變數能夠找到與它關聯的檔案

在這裡插入圖片描述

2、檔案的開啟和關閉【⭐】

接下去來講講有關檔案的開啟和關閉,如果說上面都是理論基礎,那麼這一塊的話就要涉及到程式碼了,所以豎起耳朵:ear:哦

首先舉兩個栗子🌰C語言中檔案的開啟、操作、關閉的流程基本就是下面這樣,可做參考 在這裡插入圖片描述 注:檔案在讀寫之前應該先開啟檔案,在使用結束之後應該關閉檔案 - ANSIC 規定使用fopen函式來開啟檔案,fclose來關閉檔案。顯示如何開啟和關閉檔案的個格式

c //開啟檔案 FILE * fopen ( const char * filename, const char * mode ); //關閉檔案 int fclose ( FILE * stream );


  • 下面是檔案的一些開啟方式,有很多的操作,大家挑重點記就行

==注:a即append(追加);b即binary(二進位制)==

| 檔案使用方式 | 含義 | 如果指定檔案不存在 | |--|--|--| | 【重點】“r”(只讀) | 為了輸入資料,開啟一個已經存在的文字檔案 | 出錯 | | 【重點】“w”(只寫)| 為了輸出資料,開啟一個文字檔案 | 建立一個新的檔案 | | 【重點】“a”(追加) | 向文字檔案尾新增資料 | 建立一個新的檔案 | | rb”(只讀) | 為了輸入資料,開啟一個二進位制檔案 | 出錯 | | “wb”(只寫) | 為了輸出資料,開啟一個二進位制檔案 | 建立一個新的檔案 | |“ab”(追加) | 向一個二進位制檔案尾新增資料 | 出錯 | |“r+”(讀寫) | 為了讀和寫,開啟一個文字檔案 | 出錯 | | “w+”(讀寫) | 為了讀和寫,建議一個新的檔案 | 建立一個新的檔案 | | “a+”(讀寫) | 開啟一個檔案,在檔案尾進行讀寫 | 建立一個新的檔案 | |“rb+”(讀寫) | 為了讀和寫開啟一個二進位制檔案 | 出錯 | | “wb+”(讀寫) | 為了讀和寫,新建一個新的二進位制檔案 | 建立一個新的檔案 | | “a+”(讀寫) | 開啟一個二進位制檔案,在檔案尾進行讀寫 | 建立一個新的檔案 |


例:

```c int main(void) { //開啟檔案 FILE* pf = fopen("test.txt", "w"); if (NULL == pf) { perror("fail fopen"); return 1; }

//寫檔案

//關閉檔案
fclose(pf);
pf = NULL;      //防止野指標
return 0;

} ```

四、檔案的順序讀寫【重點掌握】

接下去我們來聊聊有關檔案的順序讀寫操作,首先要說的就是一些重要的庫函式

1、8個重要的庫函式

下面的8個庫函式都很重要,大家最好都要記住,而且對於它們的用法也要熟知 | 功能 | 函式名 | 適用於 | |--|--|--| | 字元輸入函式【讀】 | fgetc | 所有輸入流 | | 字元輸出函式【寫】 | fputc | 所有輸出流 | | 文字行輸入函式【讀】 | fgets | 所有輸入流 | | 文字行輸出函式【寫】 | fgets | 所有輸入流 | | 格式化輸入函式【讀】 | fscanf| 所有輸入流 | | 格式化輸出函式【寫】 | fprintf| 所有輸入流 | | 二進位制輸入【讀】 | fread| 檔案 | | 二進位制輸入【寫】 | fwrite | 檔案 |

對於上面的這些函式的使用最關鍵的一點就是:【讀】對應的輸入流,【寫】對應的輸出流 - 在初識C語言時,我們學習了【scanf】和【printf】,只要瞭如何從鍵盤讀取資料,然後將資料顯示在螢幕上

在這裡插入圖片描述 - 現在我們可以從鍵盤、螢幕過渡到檔案,也可以從檔案讀、寫資料


接下去就讓我們來一一認識一下他們吧

1.1 單字元輸入輸出【fputc和fgetc】

這連個比較簡單,我一說你就能懂

  • 首先我們去cplusplus裡面找到這兩個函式的描述

在這裡插入圖片描述

好,有了一個基本的瞭解後,我們就到VS2019中去實操一下

  • 首先是寫檔案,我們往【test.txt】中寫一個字元a進去 c //寫檔案 fputc('a', pf); 在這裡插入圖片描述
  • 既然能寫一個,那我們多寫幾個試試

在這裡插入圖片描述 - 那我們能不能將26個字母都寫進去呢?當然是可以的,不過不是這麼一句一句寫,要用迴圈來寫

c for (int i = 0; i < 26; ++i) { fputc('a' + i, pf); } 在這裡插入圖片描述


  • 可以寫資料了,那能不能將我們寫進去的內容再讀出來呢,這就要用到 fgetc() 了,而且在開啟檔案的時候要以【讀】也就是【r】的形式開啟
  • 既然是讀取資料,那我們就要去接收讀到的這個資料,剛才看到這個庫函式的返回值是【int】,所以我們就這麼去接收

c int ch = fgetc(pf); printf("讀出來的字元為:%c\n", ch); 在這裡插入圖片描述 - 能讀一個,那也能讀多個,我們多讀幾個試試

c int ch = fgetc(pf); printf("讀出來的字元為:%c\n", ch); ch = fgetc(pf); printf("讀出來的字元為:%c\n", ch); ch = fgetc(pf); printf("讀出來的字元為:%c\n", ch);

在這裡插入圖片描述 - 然後我們再把這26個字母都讀出來試試

c for (int i = 0; i < 26; ++i) { printf("%c ", fgetc(pf)); } printf("\n");

在這裡插入圖片描述 - 但是呢,我們平常在讀取檔案中內容的時候,並不知道里面有什麼東西,有多少東西,因此應該寫一個通過的程式,才能適應更多的情況 - 我們再仔細看看fgetc的簡述。可以看到當它讀到檔案末尾的時候便會返回EOF,即End Of File(檔案結束)

在這裡插入圖片描述 - 此時我們就可以將程式碼寫成這樣。將for迴圈改為while迴圈 c int ch = 0; while ((ch = fgetc(pf)) != EOF) { printf("%c ", ch); } printf("\n"); 可以看到,一樣是可以顯示出來的

在這裡插入圖片描述

1.2 文字行輸入輸出【fputs和fgets】

接下來說說有關文字行的輸入輸出 - 首先來了解一下這個兩個函式

在這裡插入圖片描述

  • 然後我們像向檔案中寫入一個字串試試

在這裡插入圖片描述 - 接下去多寫幾行試試

在這裡插入圖片描述


  • 可以寫東西進去了,接下去一樣,將我們寫的東西讀出來試試
  • 可以看到,我這裡初始化一個數組開始讀取,在讀取結束之後DeBug和顯示視窗都可以看到只有四個字元,並沒有5個,這是為什麼呢?似乎是讀到了一個換行符

在這裡插入圖片描述 - 我們再仔細地觀察一個這個函式

在這裡插入圖片描述 - 看了一些官方文件的描述,應該清楚為什麼會只有四個了吧, - [x] 若是最大字元數num < 本行的字元數,那麼就會顯示【num - 1】個,最後一個給到【\0】,也就是對於字串而言的結束符 - [x] 若是最大字元數num > 本行的字元數,那麼除了顯示本行的所有字元之外,還會讀入一個換行符,接著就不會往下讀了。若是需要讀取下一行資料,則需要再次使用這個函式進行讀取

在這裡插入圖片描述

1.3 格式化輸入輸出【fprintf和fscanf】

接下去來看看有關檔案格式化的輸入輸出

  • 看到這個【fprintf】和【fscanf】是不是又想起來我們之前學的【printf】和【scanf】呢,我們對其進行一個對比。如下圖所示 在這裡插入圖片描述
  • 既然是進行格式化的輸入輸出,那我們就來嘗試寫一些不同格式的內容到檔案裡去,這裡直接定義一個結構體 c typedef struct student { char name[20]; int height; float score; }st;

c st s = { "zhangsan", 175, 95.5 }; //寫檔案 fprintf(pf, "%s %d %f", s.name, s.height, s.score); - 可以看到,就寫進去了

在這裡插入圖片描述


  • 然後還是一樣,我們要將其讀出來

在這裡插入圖片描述

1.4 二進位制輸入輸出【fwrite和fread】

最後我們來看看有關二進位制的讀與寫操作

  • 首先來完整地瞭解一下它們該如何操作 在這裡插入圖片描述

在這裡插入圖片描述 - 看了這些解釋之後相信你對二進位制檔案的讀寫有了一個基本的概念,接下去我們通過程式碼來鞏固一下

溫馨提示:寫二進位制檔案用【wb】,讀二進位制檔案用【rb】

  • 可以看到檔案中是寫入了一些資料,但是呢寫進去的東西是亂碼的樣子,看不太懂

在這裡插入圖片描述 - 不要急,因為我們以二進位制的形式寫入,自然也要使用二進位制的形式讀出

在這裡插入圖片描述

2、拓展:預設開啟的三個流

這裡給大家拓展一個小知識,也就是對於任何一個C語言程式,只要執行起來,就會預設地開啟三個流 - [x] stdin - 標準輸入流 - 鍵盤 - [x] stdout - 標準輸出流 - 螢幕 - [x] stderr - 標準輸錯誤 - 螢幕

通過觀看原始碼可以知曉,他們都是以巨集定義的形式存放在記憶體中的,之前我們說過,對於巨集定義而言是在程式開始之前就定義好的,也就是當程式執行起來之後,那它們就會存在了

在這裡插入圖片描述

然後我們去程式中執行一下試試

c int ch = fgetc(stdin); fputc(ch, stdout); - 然後可以看到,我們確實可以使用【stdin】和【stdout】這兩個流來進行輸入和輸出

在這裡插入圖片描述

c int ch = 0; fscanf(stdin, "%c", &ch); fprintf(stdout, "%c", ch);

在這裡插入圖片描述

3、對比一組函式【💪】

  • 上面講到了8個有關檔案順序讀寫的庫函式,接下去給大家對比一下一組函式
  • [x] scanf / fscanf / sscanf
  • [x] printf / fprintf / sprintf

在這裡插入圖片描述 - 主要還是來看看【sprintf】和【sscanf】這兩個新面貌

在這裡插入圖片描述 在這裡插入圖片描述

  • 接下去我們通過程式碼來看看是不是真的可以實現

```c char buf[100] = { 0 }; st s = { "zhangsan", 170, 95.5f }; st tmp = { 0 };

//能否將這個結構體的成員轉化為字串 sprintf(buf, "%s %d %f", s.name, s.height, s.score); printf("%s\n", buf);

//能否將這個字串中內容還原為一個結構體資料呢 sscanf(buf, "%s %d %f", tmp.name, &(tmp.height), &(tmp.score));

printf("%s %d %f", tmp.name, tmp.height, tmp.score); ``` - 可以看到,我將一個結構體資料以格式化的形式寫到了一個字串中,然後又從這個字串中以格式化的形式讀取資料到一個結構體變數中,這麼轉換來轉換去,完全沒有問題。你也可以自己去試試

在這裡插入圖片描述


  • 那可能這麼講還是有點抽象,我們通過一個現實中開發的場景再來描述一下。比如說前端給到使用者一個收集資訊的表單,使用者輸入資料之後呢,==前端==就將這些資訊用“+”號做了一個拼接給到==後端==,後端呢為了要識別這些資訊,一定會建立一個結構體,裡面包含這些資訊的,這個時候就可以使用到我們上面所說的【sscanf】以格式化的方式去讀取這個字串了,然後就可以解析出使用者的這些資料,然後去進行一個處理了
  • 當然在現實的軟體開發中,是不會這麼去做的,因為有現成封裝的API可以呼叫,庫裡面會提供一個【序列化/反序列化】的API可以呼叫,開發者無需考慮其底層的實現 在這裡插入圖片描述

五、檔案的隨機讀寫

說完了檔案的順序讀寫,接下去我們來講講有關檔案的隨機讀寫,這裡我會【象徵性】地介紹三個有代表性的函式,其他不常用到的我就不介紹了

1、fseek

根據檔案指標的位置和偏移量來定位檔案指標 - 首先來看看它的相關介紹 - 可以看到,最重要的還是最後的那個引數,因為有三個選項可以使用。我會一一介紹 在這裡插入圖片描述 - 然後通過程式碼我們再來實現一下這個功能。首先看到是使用到了【SEEK_SET】從檔案的起始位置開始偏移,因為檔案的起始是從第一個字元開始,向後偏移三位就到了【d】的位置

在這裡插入圖片描述 - 接下來我們再來看一種。剛才是從前往後偏移,現在則是從後往前偏移,那就要使用到【SEEK_END】

在這裡插入圖片描述

  • 最後一個是【SEEK_CUR】,也就是從當前位置向後偏移

在這裡插入圖片描述


==補充一個實際案例== c FILE* pFile; pFile = fopen("example.txt", "wb"); fputs("This is an apple.", pFile); fseek(pFile, 9, SEEK_SET); fputs(" sam", pFile); fclose(pFile); - 可以看到這裡使用了【fseek】,將檔案指標偏移到了一個位置,然後從這個位置開始寫了一個字串,執行之後開啟檔案可以發現裡面的英文句子就發現了變化

在這裡插入圖片描述

2、ftell

返回檔案指標相對於起始位置的偏移量

在這裡插入圖片描述 - 這個很簡單,就是返回當前檔案指標所在流中的位置

在這裡插入圖片描述


==補充一個實際案例== c FILE* pFile; long size; pFile = fopen("myfile.txt", "rb"); if (pFile == NULL) perror("Error opening file"); else { fseek(pFile, 0, SEEK_END); //non-portable size = ftell(pFile); fclose(pFile); printf("Size of myfile.txt: %ld bytes.\n", size); } - 這個案例很巧妙地結合了我們上面所學過的【fseek】和【ftell】,求出了這個檔案的位元組大小

在這裡插入圖片描述

3、rewind

讓檔案指標的位置回到檔案的起始位置

在這裡插入圖片描述 - 可以看到,我們又讀到了a,表明檔案指標pf確實回到到了起始位置

在這裡插入圖片描述


==補充一個實際案例== ```c int n; FILE* pFile; char buffer[27]; pFile = fopen("myfile.txt", "w+"); for (n = 'A'; n <= 'Z'; n++) fputc(n, pFile);

rewind(pFile); //當檔案指標pFile重新回到起始位置 fread(buffer, 1, 26, pFile); //通過檔案指標讀入26個字母到buffer字元陣列中 fclose(pFile); buffer[26] = '\0'; //'\0'表示字串的結束位置 puts(buffer); ``` - 這個案例就是將1~26個大寫英文字母寫入檔案,然後在讓檔案指標回到起始位置,在使用二進位制的讀取方式將檔案中的內容讀取到字元陣列中,最後為字串設定結束標誌,打印出來便是檔案中寫入的內容

在這裡插入圖片描述

六、文字檔案和二進位制檔案

接下去我們來談談文字檔案和二進位制檔案

  • 【二進位制檔案】:資料在記憶體中以二進位制的形式儲存。不加轉換的輸出到外存
  • 【文字檔案】:以ASCII字元的形式儲存的檔案。在外存上以ASCII碼的形式儲存,則需要在儲存前轉換

字元一律以ASCII形式儲存,數值型資料既可以用ASCII形式儲存,也可以使用二進位制形式儲存 在這裡插入圖片描述 上面就是一個十進位制的數值10相關的兩種儲存形式,我測試了一下,以二進位制的形式存放到檔案裡只佔4個位元組,但是以ASCLL碼的形式存放到檔案裡就需要佔5個位元組

  • 接下去我們通過下面這段程式碼來看看二進位制的儲存形式 c int main() { int a = 10000; FILE* pf = fopen("test.txt", "wb"); fwrite(&a, 4, 1, pf);//二進位制的形式寫到檔案中 fclose(pf); pf = NULL; return 0; }

在這裡插入圖片描述

在這裡插入圖片描述

七、檔案讀取結束的判定

牢記:在檔案讀取過程中,不能用feof函式的返回值直接用來判斷檔案的是否結束

1、被錯誤使用的eof

去網上看很多的程式碼可以發現,大家幾乎都錯誤地使用了【feof】這個函式,認為它和==EOF==一樣就是用來判斷檔案是否結束,但是並不是這樣,我們一起來探究一下這個函式

  • 從中我們可以知曉【feof】應用於當檔案讀取結束的時候,判斷是讀取失敗結束,還是遇到檔案尾結束 在這裡插入圖片描述

2、fgetc、fgets、fscanf、fread結束判斷解讀

  • 對於上面的四個讀取檔案函式,要怎麼去判斷它們是否結束呢?我們通過觀察這些函式的返回值來看看

fgetc - [x] 如果讀取正常,返回讀取到的字元的ASCLL碼值 - [x] 如果讀取失敗,返回EOF

在這裡插入圖片描述


fgets - [x] 如果讀取正常,返回讀取到的資料的地址 - [x] 如果讀取失敗,返回NULL

在這裡插入圖片描述


fscanf - [x] 如果讀取正常,返回的是格式串中指定的資料個數 - [x] 如果讀取失敗,返回的是小於格式串中指定的資料個數

在這裡插入圖片描述


fread - [x] 如果讀取正常,返回的是等於要讀取的資料個數 - [x] 如果讀取失敗,返回的是小於要讀取的資料個數

在這裡插入圖片描述

3、例項程式碼走讀

接下去大家走讀兩段程式碼

==文字檔案操作== - 首先通過檔案指標以讀的形式打開了這個檔案,然後去判斷一下是否開啟成功,這裡是換了一個形式去判斷,和我們在【二叉樹】章節判斷根結點是否為空是一個道理。(fp == NULL) (root == NULL) - 因為條件表示式為真也就是1的時候會進入if判斷,那!pf == 1可以推出pf == 0等價於pf == NULL - 接下去的話就是去這個檔案中一個讀取內容然後輸出,若是檔案到達了EOF,也就是【fgetc】的結束判斷條件,此時才可以使用【feof】去進行判斷,所以可以看出【feof】是在檔案結束之後去判斷檔案是因為什麼而結束的。【ferror】若是成立的話表示這個檔案是因為I/O的讀取的問題中斷的;若不是【feof】判斷滿足就表示其是正常結束c int c; // 注意:int,非char,要求處理EOF FILE* fp = fopen("test.txt", "r"); if (!fp) { perror("File opening failed"); return EXIT_FAILURE; } //fgetc 當讀取失敗的時候或者遇到檔案結束的時候,都會返回EOF while ((c = fgetc(fp)) != EOF) // 標準C I/O讀取檔案迴圈 { putchar(c); } //判斷是什麼原因結束的 if (ferror(fp)) puts("I/O error when reading"); else if (feof(fp)) puts("End of file reached successfully"); fclose(fp); ==二進位制檔案操作== - 這是一個二進位制的檔案操作,所以以二進位制的形式開啟,然後使用二進位制的寫法【fwrite】從a這個陣列的首元素地址開始拿取SIZE個大小為double的資料通過fp這個檔案指標寫出到檔案中 講一下這裡的(sizeof a)是什麼意思,a是陣列的首元素地址,然後通過【 * 】解引用可以獲取到每個陣列元素的大小了 - 陣列a中的資料寫入檔案後,就要再開啟這個檔案然後將檔案中的內容個讀出來,我們將其儲存在一個變數中然後對這個變數進行一個判斷 - 因為這個是一個二進位制檔案,因此我們要去判斷它返回的個數是否小於需要讀取的個數*,若是成立則表示沒有讀完就結束了,若是和SIZE的個數相同的話表示都讀完了,然後我們將讀取到陣列b中的內容輸出一下即可 - 若是沒有讀完但是檔案又結束了,那麼此時使用【feof】判斷成立了,將不對的資訊打印出來即可,若是沒有到達檔案末尾但是又讀取結束了,進入了【ferror】的判斷,表示檔案的I/O流出現問題了 ```c enum { SIZE = 5 }; int main(void) { double a[SIZE] = { 1.,2.,3.,4.,5. }; double b[SIZE];

FILE* fp = fopen("test.bin", "wb"); // 必須用二進位制模式
fwrite(a, sizeof * a, SIZE, fp); // 寫 double 的陣列
fclose(fp);

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 handling
    if (feof(fp))
        printf("Error reading test.bin: unexpected end of file\n");
    else if (ferror(fp)) {
        perror("Error reading test.bin");
    }
}

fclose(fp);

} ```

好,走讀完上面的這個兩個例項,相信你對檔案操作應該有了一個更進一步的理解,接下去我們來講講有關檔案緩衝區的知識

八、檔案緩衝區

ANSIC 標準採用【緩衝檔案系統】處理的資料檔案的,所謂緩衝檔案系統是指系統自動地在記憶體中為程式中每一個正在使用的檔案開闢一塊“檔案緩衝區”。

  • [x] 從記憶體向磁碟輸出資料【寫】會先送到記憶體中的緩衝區,裝滿緩衝區後才一起送到磁碟上。
  • [x] 如果從磁碟向計算機【讀】入資料,則從磁碟檔案中讀取資料輸入到記憶體緩衝區(充滿緩衝區),然後再從緩衝區逐個地將資料送到程式資料區(程式變數等)。
  • 緩衝區的大小根據C編譯系統決定的

==下面是有關檔案緩衝區的示意圖== 在這裡插入圖片描述 ==下面是例項程式碼==

```c 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;

} ``` 在這裡插入圖片描述 在這裡插入圖片描述

📚拓展:檔案外排序【更上一層樓】

說了這麼多有檔案的操作,但是不實際去使用還是不行的,紙上得來終覺淺,絕知此事要躬行。接下去就讓我們來看看如何對一個檔案中的資料進行排序

1、前言

  • 對於檔案中的資料,一般都是很大的,不像我們上面所講的十二十個數,可能會有成千上百的資料需要我們去排序,此時效率最高的就是【歸併排序】了,因為面對海量的資料而言,像效率較高的【快速排序】需要克服三數取中的困難,還有像【堆排序】【希爾排序】這些,都無法支援隨機訪問,所以很難去對大量的檔案進行一個排序,速度會非常之慢。即使是有檔案函式【fseek()】這樣的函式可以使檔案指標偏移,還是很難做到高效。因為磁碟的速度比起記憶體差了太多太多了,具體的我不太清楚大概有差個幾千倍這樣,
  • 所以我們就想到了【歸併排序】,它既是內排序,也是外排序,而且效能也不差,算是速度較快的幾個排序之一了。但是要如何進行歸併呢?

2、思路解析

在這裡插入圖片描述 - 回憶一下歸併排序的原理,就是兩個有序區有序,然後兩兩一歸才使得整體可以有序,如果左右都無需,那麼繼續對其進行左右分割歸併 - 但是本次,我要教給你的你是另外一種思路:

將一個大檔案平均分割成N份,保證每份的大小可以載入到記憶體中,然後使用快排將其排成有序再寫回一個個小檔案,此時就擁有了檔案中歸併的先決條件 - 具體示意圖如下

在這裡插入圖片描述 ==這裡我設定一個這樣的規則,令檔案1為【1】,檔案2位【2】,它們歸併之後即為【12】,然後再讓【12】和檔案3即【3】歸併變成【123】,以此類推,所以最後歸出的檔名應該是【12345678910】==

3、程式碼詳解

下面是大檔案分割成10個小檔案的邏輯,首先來講解一下這塊,程式碼中很多內容涉及到檔案操作,如果有檔案操作還不是很懂的小夥伴記得再去溫習一下

  • 整體的邏輯就在於從檔案中讀取100個數據,但是分批進行讀取,每次首先去讀9個數,然後當讀到第十個數的時候,先將其加入陣列中,然後再對陣列中的這10個數進行排序。排完序後就將這個10個數通過檔案指標再寫到一個小檔案中
  • 接著當第二次迴圈上來的時候,就開始讀第11~20個數;以此往復,直到讀完這個100個數為止,那此時我們的工程目錄下就會出現10個小檔案,就是對這100個數的分隔排序後的結果 ```cpp void MergeSortFile(const char file) { FILE fout = fopen(file, "r"); if (!fout) { perror("fopen fail"); exit(-1); }

    int num = 0; int n = 10; int i = 0; int b[10]; char subfile[20]; int filei = 1; //1.讀取大檔案,然後將其平均分成N份,載入到記憶體中後對每份進行排序,然後再寫回小檔案 memset(b, 0, sizeof(int) * n); while (fscanf(fout, "%d\n", &num) != EOF) { if (i < n - 1) { b[i++] = num; //首先讀9個數據到陣列中 } else { b[i] = num; //再將第十個輸入放入陣列 QuickSort(b, 0, n - 1); //對其進行排序

        sprintf(subfile, "%d", filei++);
    
        FILE* fin = fopen(subfile, "w");
        if (!fin)
        {
            perror("fopen fail");
            exit(-1);
        }
        //再進本輪排好序的10個數以單個小檔案的形式寫到工程檔案下
        for (int j = 0; j < n; ++j)
        {
            fprintf(fin, "%d\n", b[j]);
        }
        fclose(fin);
    
        i = 0;      //i重新置0,方便下一次的讀取
        memset(b, 0, sizeof(int) * n);
    }
    

    } ``` - 我們來看一下排序的結果

在這裡插入圖片描述 - 將大檔案分成10個小檔案後,接下去就是要對這個10個小檔案進行歸併,具體規則我上面已經說了 - 下面就是單趟歸併的邏輯的,就和我們上面說到的歸併排序的程式碼是很類似的,只不過這裡是檔案的操作而已。要注意的是對於檔案來說是有一個檔案指標的,若是你讀取了一個之後那麼檔案指標這個結構體中的資料標記就會發生變化,標記為當然所讀內容的下一個了 - 所以我們不能將讀取讀取小檔案中的資料的操作放在while迴圈中,應該單獨將其抽離出來進行判斷才才對。若是哪個檔案中的數小,那麼就將這個數寫到新的【mfile】檔案中去,然後繼續讀取當前檔案的後一個內容 ```cpp //檔案歸併邏輯 void _MergeSortFile(const char file1, const char file2, const char mfile) { FILE fout1 = fopen(file1, "r"); if (!fout1) { perror("fopen fail"); exit(-1); }

FILE* fout2 = fopen(file2, "r");
if (!fout2)
{
    perror("fopen fail");
    exit(-1);
}

FILE* fin = fopen(mfile, "w");
if (!fin)
{
    perror("fopen fail");
    exit(-1);
}

int num1, num2;
//返回值拿到迴圈外來接受
int ret1 = fscanf(fout1, "%d\n", &num1);
int ret2 = fscanf(fout2, "%d\n", &num2);
while (ret1 != EOF && ret2 != EOF)
{
    if (num1 < num2)
    {
        fprintf(fin, "%d\n", num1);
        ret1 = fscanf(fout1, "%d\n", &num1);
    }
    else
    {
        fprintf(fin, "%d\n", num2);
        ret2 = fscanf(fout2, "%d\n", &num2);
    }
}

while (ret1 != EOF)
{
    fprintf(fin, "%d\n", num1);
    ret1 = fscanf(fout1, "%d\n", &num1);
}
while (ret2 != EOF)
{
    fprintf(fin, "%d\n", num2);
    ret2 = fscanf(fout2, "%d\n", &num2);
}

fclose(fout1);
fclose(fout2);
fclose(fin);

} ``` 最後在開啟檔案後不要忘了將檔案關閉哦,不然就白操作了

  • 當然上面是一個單趟的邏輯,我們還要對【file1】【file2】【mfile】進行一個迭代

```cpp //利用互相歸併到檔案,實現整體有序 char file1[100] = "1"; char file2[100] = "2"; char mfile[100] = "12"; for (int i = 2; i <= n; ++i) { _MergeSortFile(file1, file2, mfile);

//迭代
strcpy(file1, mfile);
sprintf(file2, "%d", i + 1);
sprintf(mfile, "%s%d", mfile, i + 1);

} ``` - 大概就是這麼一個迭代的過程

在這裡插入圖片描述 在這裡插入圖片描述

==整體程式碼展示==

```cpp //檔案歸併邏輯 void _MergeSortFile(const char file1, const char file2, const char mfile) { FILE fout1 = fopen(file1, "r"); if (!fout1) { perror("fopen fail"); exit(-1); }

FILE* fout2 = fopen(file2, "r");
if (!fout2)
{
    perror("fopen fail");
    exit(-1);
}

FILE* fin = fopen(mfile, "w");
if (!fin)
{
    perror("fopen fail");
    exit(-1);
}

int num1, num2;
//返回值拿到迴圈外來接受
int ret1 = fscanf(fout1, "%d\n", &num1);
int ret2 = fscanf(fout2, "%d\n", &num2);
while (ret1 != EOF && ret2 != EOF)
{
    if (num1 < num2)
    {
        fprintf(fin, "%d\n", num1);
        ret1 = fscanf(fout1, "%d\n", &num1);
    }
    else
    {
        fprintf(fin, "%d\n", num2);
        ret2 = fscanf(fout2, "%d\n", &num2);
    }
}

while (ret1 != EOF)
{
    fprintf(fin, "%d\n", num1);
    ret1 = fscanf(fout1, "%d\n", &num1);
}
while (ret2 != EOF)
{
    fprintf(fin, "%d\n", num2);
    ret2 = fscanf(fout2, "%d\n", &num2);
}

fclose(fout1);
fclose(fout2);
fclose(fin);

}

/檔案外排序/ void MergeSortFile(const char file) { srand((unsigned int)time(NULL)); FILE fout = fopen(file, "r"); if (!fout) { perror("fopen fail"); exit(-1); }

//先寫100個隨機數進檔案
//for (int i = 0; i < 100; ++i)
//{
//  int num = rand() % 100;
//  fprintf(fout, "%d\n", num);
//}

int num = 0;
int n = 10;
int i = 0;
int b[10];
char subfile[20];
int filei = 1;

//1.讀取大檔案,然後將其平均分成N份,載入到記憶體中後對每份進行排序,然後再寫回小檔案
memset(b, 0, sizeof(int) * n);
while (fscanf(fout, "%d\n", &num) != EOF)
{
    if (i < n - 1)
    {
        b[i++] = num;   //首先讀9個數據到陣列中
    }
    else
    {
        b[i] = num;     //再將第十個輸入放入陣列
        QuickSort(b, 0, n - 1);     //對其進行排序

        sprintf(subfile, "%d", filei++);

        FILE* fin = fopen(subfile, "w");
        if (!fin)
        {
            perror("fopen fail");
            exit(-1);
        }
        //再進本輪排好序的10個數以單個小檔案的形式寫到工程檔案下
        for (int j = 0; j < n; ++j)
        {
            fprintf(fin, "%d\n", b[j]);
        }
        fclose(fin);

        i = 0;      //i重新置0,方便下一次的讀取
        memset(b, 0, sizeof(int) * n);
    }
}

//利用互相歸併到檔案,實現整體有序
char file1[100] = "1";
char file2[100] = "2";
char mfile[100] = "12";
for (int i = 2; i <= n; ++i)
{
    _MergeSortFile(file1, file2, mfile);

    //迭代
    strcpy(file1, mfile);
    sprintf(file2, "%d", i + 1);
    sprintf(mfile, "%s%d", mfile, i + 1);

}

} ``` ==執行結果展示==

在這裡插入圖片描述

九、總結與提煉

好,我們來總結回顧一下本文所學習的知識

在本文中我們瞭解了什麼是檔案,知道了有【資料檔案】和【文字檔案】兩種,接下去主要是對資料檔案展開了一系列的操作: - 首先是說到如何去開啟和關閉一個檔案,以及開啟檔案的一些方式。之後講解了有關檔案的順序讀寫,裡面說到了對檔案進行讀寫的八個常用函式 【fgetc】【fputc】【fgets】【fputs】【fscanf】【fprintf】【fread】【fwrite】,說完順序讀寫後我們又講了隨機讀寫,也給大家講了三個函式 【fseek】【ftell】【rewind】。其他函式記不住沒關係,這是一個函式希望大家可以牢記於心,這樣你的檔案操作就能更加熟練 - 其次我們又說到了有關【feof】的錯誤使用,講解了其該如何去正確使用,這個函式也希望你可以記住,在進行檔案判斷是否讀取完畢的時候可以起到很關鍵的作用 - 最後,我們又說到了如何對一個檔案中的資料去進行排序,充分地將學習的知識運用到了實際的應用中,希望通過這個你對檔案的操作可以更上一層樓

最後很感謝您對本文的觀看,如有疑問,請於評論區留言或者私信我可以:cherry_blossom:

在這裡插入圖片描述