本節重點
- 理解文件的形式與基本概念
- 二進制文件與文本文件
- 文件的打開與關閉
- 文件讀寫函數、
- 文件緩沖區
正文開始---------------------------------------------------------------------------------------------------------------------
一、為什么使用文件
程序運行時數據存儲在內存中(如變量、數組),但程序結束后內存會被操作系統回收,數據隨之丟失,用戶配置、日志記錄、程序狀態等數據需跨會話保存,文件是操作系統提供的標準持久化方案,通過文件存儲,C 程序能夠突破內存限制,實現數據的長期保存和跨會話共享,是構建健壯應用程序的基礎能力。
也就是說數據保存在內存之中容易丟失,我們可以通過文件操作將數據保存在文件也就是磁盤或硬盤之中。
二、什么是文件
在計算機領域,文件是存儲數據的核心單位,用于將信息以特定格式保存在物理存儲設備(如硬盤、SSD)中。
在Windows中通過可視化操作界面,文件的存在形式是這樣的:
而在Linux操作系統中,通過命令行操作文件的存在形式是這樣的:
2.1 程序文件
程序文件是包含機器語言指令(二進制代碼)或解釋型語言代碼的文件,可被操作系統識別并執行從而實現特定的功能。
程序文件包括源程序文件(后綴為.c),目標文件(windows環境后綴為.obj),可執行文件(Windows環境后綴為.exe)。
2.2 數據文件
文件的內容不一定是程序,而是程序運行時讀寫的數據,比如程序運行需要從中讀取數據的文件,或者輸出內容的文件。
在這里,我們主要討論的是數據文件。
在之前我們所處理的數據的輸入輸出都是以終端為對象,即從終端的鍵盤輸入數據(主要通過scanf 函數),運行結果顯示到顯示器上(主要通過 printf 函數)。
其實有時候我們可以把信息輸出到磁盤上,當需要的時候再從磁盤上把數據讀取到內存中使用,這里處理的就是磁盤上的文件。
2.3、文件名
文件名要有一個唯一的文件標識,以便用戶識別和引用。
文件名包含3部分:文件路徑+文件主干+文件后綴,例如:
D:\code\new_code\code.exe
為了方便起見,文件標識符常被稱為文件名。
四、二進制文件與文本文件
根據數據的組織形式,數據文件被稱為文本文件或者二進制文件。
數據在內存中以二進制的形式存儲,如果不加轉換就輸出到外存的文件中,就是二進制文件。如果要求在外存上以ASCLL碼的形式存儲,則需要在存儲前轉換,以ASCLL碼的形式存儲的文件就是文本文件。
一個數據在文件中是怎么存儲的呢?
字符一律按ASCLL碼的形式存儲,數值型數據既可以用ASCLL碼形式存儲也可以以二進制形式存儲。
例如,一個數值型數據10000以ASCLL碼存儲時一共有5個字符分別為1、0、0、0、0共占用5個字節,而以二進制形式存儲時占4個字節。
四、文件的打開與關閉
4.1 流和標準流
4.1.1 流
在編程中,“流”(Stream)是一種抽象的數據處理模型,用于逐步傳輸或處理數據,而無需一次性將整個數據加載到內存中。它類似于現實世界中的“水流”,數據像水流一樣按順序流動,程序可以按需讀取或寫入數據片段。
C程序針對文件、畫面、鍵盤等的數據的輸入輸出操作都是通過流實現的。
4.1.2 標準流
在編程中,“標準流”(Standard Streams)是操作系統或運行時環境預定義的三個默認數據流,用于程序與外界(如終端、文件或其他程序)進行交互。它們是:
- stdin? -標準輸入流,在大多數情況下從鍵盤輸入,scanf函數就是從標準輸入流中讀取數據
- stdout-標準輸出流,大多數環境中輸出到顯示器界面,printf函數就是將信息輸出到標準輸出流中
- stderror-標準錯誤流,大多數環境中輸出到顯示器界面。
當我們的程序啟動,或者說進程創建時,操作系統會為我們的程序自動分配三個標準流并默認連接到終端設備(顯示器、鍵盤)。
stdin、stdout、stderror三個流的類型都是FILE*,通常稱為文件指針,在C語言中就是通過文件指針來維護流的各種操作的。
4.2 文件指針
當我們打開一個文件時,為了方便我們對被打開文件的進一步管理和維護,內存中就會開辟一個相應的“文件信息區”用來存放被打開文件的相關信息(如文件的名字,狀態以及文件當前的位置等等文件屬性)。這些信息是保存在一個結構體變量中的,也就是說這個“文件信息區”就是一個結構體,該結構體的類型是由系統聲明的,取名為FILE。
例如,vs2013編譯環境提供的stdio.h頭文件中有以下的文件類型聲明:
struct _iobuf {char *_ptr;int _cnt;char *_base;int _flag;int file;int _charbuf;int bufsiz;char *_tmpfname;};
typedef struct _iobuf FILE;
每當打開一個文件時,系統就會開辟內存空間創建一個FILE結構體來管理該文件的相關屬性和信息,使用者不必關心其細節。
為了將每個文件與所對應的“文件休信息區”(FILE)關聯起來,就引出了文件指針(FILE*)的概念。通過使文件指針指向某個文件的“文件信息區”(本質是一個結構體變量),通過該文件信息區中的信息就能找到并管理該文件。
也就是說,通過文件指針變量就能間接找到與它關聯的文件。
4.3 文件的打開與關閉
文件在讀寫之前首先應該打開文件,在使用結束后關閉文件。本質上就是保證打開文件后系統就會創建對應的“文件信息區”來方便用戶對文件進行管理與維護,關閉文件目的是釋放相應的內存空間,不然過多無效的“文件信息區”會占用內存,嚴重時會導致內存泄漏。
在編寫程序的時候,在打開文件的同時,都會返回一個FILE* 類型的指針變量指向該文件,也相當于建立了指針與文件的關系。
ANSIC規定使用fopen函數來打開文件,fclose來關閉文件:
//打開文件
FILE* fopen(const char* filename,const char* mode);//關閉文件
int fclose(FILE* stream);
mode參數表示文件的打開方式,下面都是文件的打開模式:
模式 | 描述 | 讀權限 | 寫權限 | 追加模式 | 文件不存在時 | 文件存在時 | 初始位置 |
---|---|---|---|---|---|---|---|
r | 只讀模式(文本文件) | ?? | ? | ? | 失敗(返回?NULL ) | 保留內容,從文件開頭讀取 | 文件開頭 |
w | 只寫模式(文本文件,覆蓋原有內容) | ? | ?? | ? | 創建新文件 | 清空文件內容 | 文件開頭 |
a | 追加模式(文本文件,寫入內容到文件末尾) | ? | ?? | ?? | 創建新文件 | 保留內容,寫入位置在文件末尾 | 文件末尾 |
r+ | 讀寫模式(文本文件,不截斷文件) | ?? | ?? | ? | 失敗 | 保留內容,從文件開頭讀寫 | 文件開頭 |
w+ | 讀寫模式(文本文件,覆蓋原有內容) | ?? | ?? | ? | 創建新文件 | 清空文件內容 | 文件開頭 |
a+ | 讀寫追加模式(文本文件,寫入到文件末尾,可讀取整個文件) | ?? | ?? | ?? | 創建新文件 | 保留內容,寫入位置在文件末尾 | 文件末尾(讀從開頭) |
rb | 只讀模式(二進制文件) | ?? | ? | ? | 失敗 | 保留內容,從文件開頭讀取 | 文件開頭 |
wb | 只寫模式(二進制文件,覆蓋原有內容) | ? | ?? | ? | 創建新文件 | 清空文件內容 | 文件開頭 |
ab | 追加模式(二進制文件,寫入內容到文件末尾) | ? | ?? | ?? | 創建新文件 | 保留內容,寫入位置在文件末尾 | 文件末尾 |
r+b | 讀寫模式(二進制文件,不截斷文件) | ?? | ?? | ? | 失敗 | 保留內容,從文件開頭讀寫 | 文件開頭 |
w+b | 讀寫模式(二進制文件,覆蓋原有內容) | ?? | ?? | ? | 創建新文件 | 清空文件內容 | 文件開頭 |
a+b | 讀寫追加模式(二進制文件,寫入到文件末尾,可讀取整個文件) | ?? | ?? | ?? | 創建新文件 | 保留內容,寫入位置在文件末尾 | 文件末尾(讀從開頭) |
x | 獨占創建模式(僅當文件不存在時創建,失敗返回?NULL ) | ??/? | ?? | ? | 創建新文件 | 失敗(文件已存在) | 文件開頭 |
實例代碼:
#include<stdio.h>int main()
{FILE* pf=fopen("text.txt","w");if(pf==NULL){//文件打卡失敗perror("fopen");return 0;}const char* txt="hello world!!";fputs(txt,pf);fclose(pf);return 0;
}
五、文件的順序讀寫
5.1? 字符讀寫
fgetc?
- 功能:從文件中讀取單個字符
- 參數:FILE* stream(文件指針)
- 返回值:成功返回字符的int值,失敗或文件結束返回EOF
fputc
- 功能:向文件中寫入單個字符
- 參數:int c(字符),FILE* stream(文件指針)
- 返回值:?成功返回寫入的字符,失敗返回EOF
5.2? 字符串讀寫
fgets
- 功能:從文件中讀取一行字符串(直到遇到換行符或指定長度)
- 參數:char* s(自定義緩沖區),int size(最大長度),FILE* stream(文件指針)
- 返回值:成功返回緩沖區的指針,失敗或文件結束返回NULL
fputs
- 功能:向指定文件中寫入一行字符串
- 參數:const char* s(字符串),FILE* stream
- 返回值:成功返回非負值,失敗返回EOF
5.3? 格式化讀寫
fscanf
- 功能:按指定格式從文件中讀取數據
- 參數:FILE* stream,格式化字符串,變量地址列表
- 返回值:成功匹配并賦值的參數個數,失敗返回EOF
fprintf
- 功能:按指定格式向文件中寫入數據
- 參數:FILE* stream,格式化字符串,變量列表
- 返回值:成功返回寫入的字符數,失敗返回負數
5.4? 塊讀寫(二進制模式)
fread
- 功能:從文件中讀取二進制數據塊
-
參數:void* ptr(緩沖區),size_t size(每個元素大小),size_t nmemb(要讀取的元素數量),FILE* stream。
- 返回值:成功讀取的元素數量。
fwrite
- 功能:向文件寫入二進制數據塊。
-
參數:void* ptr(要寫入的數據的指針),
size_t size(每個元素大小)
,size_t nmemb(要寫入的元素的數量)
,FILE *stream
。 - 返回值:成功寫入的元素數量
六、文件的隨機讀寫?
文件的隨機讀寫允許程序直接跳轉到文件的任意位置進行讀寫操作,無需按順序訪問。
fseek
- 功能:移動文件指針到指定位置
- 參數:FILE* stream(文件指針),long offset(偏移量字節) ,int origin(基準位置)
- 返回值:成功返回0,失敗返回非0值
ftell
- 功能:返回文件指針相對于起始位置的偏移量
- 參數:FILE* stream
- 返回值: 成功返回long類型的偏移量
rewind
- 功能:將文件指針重置到文件開頭
- 參數:FILE* stream
- 返回值: 無
七、文件緩沖區
在之后的學習中我們會了解到,其實C語言中文件的各類讀寫函數其實底層都封裝了系統調用接口read,write來向操作系統發出各類操作指令,而系統調用是一個復雜的概念它涉及到操作系統對內存空間的各類調度與管理,如果C語言中的文件函數直接訪問文件或磁盤中的數據,頻繁地進行讀取和寫入操作,就意味著系統調用接口的頻繁調用加重CPU的負擔,減低I/O的效率。
為了避免這個問題,我們引入了文件緩沖區的概念,它通過先將程序數據先放入緩沖區中,通過刷新機制將多次小數據量的讀寫合并為少量大塊操作,減少直接訪問磁盤的次數,顯著提升I/O效率,避免頻繁的系統調用。
?寫個代碼驗證一下:
#include<stdio.h>
#include<windows.h>
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
}