C語言之文件操作
C語言之文件操作
在本節我們將會講解有關文件的讀寫操作;
綱要:
- 一些需要掌握的知識點
- 文件名
- 文件類型
- 數據流
- 文件緩衝區
- 文件指針
- 與文件操作相關的一些函數
- 文件的打開及關閉
- 文件的順序讀寫
- 文件的隨機讀寫
- 文件快取區的刷新
- 一個易被誤用的點
- feof()的使用
正文開始:
一.一些需要掌握的知識點
文件有千千萬萬,但是在我們的程式設計當中,我們談的文件一般有兩種:
1.程式文件
包括源程式文件(後綴為.c),目標文件(windows環境後綴為.obj),可執行程式(windows環境後綴為.exe)。
2.數據文件
文件的內容不一定是程式,而是程式運行時讀寫的數據,比如程式運行需要從中讀取數據的文件,或者輸出內容的文件。
而在本節中,我們主要提到的是數據文件。
1.文件名
我們知道,名字都是用來標識和區別事物的,那麼文件名也是這樣,是區別各個文件的標識。
一個文件名要包含 3 個部分:文件路徑+文件名主幹+文件後綴
如:C:\Windows\System32\drivers\etc.txt
其中 :C:\Windows\System32\drivers\ 是文件路徑,etc 是文件名主幹,txt 是文件名後綴。
當然了,各個平台的文件路徑並不相同,以及為了方便起見文件標識通常別稱為文件名
2.文件類型
根據數據的組織形式,數據文件被稱為文本文件或者二進位文件。
二進位文件:數據在記憶體中以二進位的形式存儲,並不加轉換的輸出到外存。
文本文件:要求在外存上以ASCII碼的形式存儲,需要在存儲前轉換,以ASCII字元的形式存儲的文件。
那麼一個數據在記憶體中是怎樣存儲的呢?
字元一律以ASCII形式存儲,數值型數據既可以用ASCII形式存儲,也可以使用二進位形式存儲。
如有整數10000,如果以ASCII碼的形式輸出到磁碟,則磁碟中佔用5個位元組(每個字元一個位元組)
而二進位形式輸出,則在磁碟上只佔4個位元組(VS2019測試)。
如:
我們可以測試一番:
#include <stdio.h>
int main()
{
int a = 10000;
FILE* pf = fopen("test.txt", "wb");
fwrite(&a, 4, 1, pf);//二進位的形式寫到文件中
fclose(pf);
pf = NULL;
return 0;
}
我們打開的時候要注意以二進位編輯器來打開,你會發現出現了如下圖顯示的一串數字,其中它們是以十六進位現實的,轉換一下,剛好是上圖顯示的那串二進位數字(注意VS採用的是小端儲存模式)
3.數據流
數據流:
指程式於數據的交互是以流的形式進行的,包括輸入流與輸出流;
輸入流:
程式從輸入流讀取數據源。數據源包括鍵盤,文件,網路等,即:將數據源讀入到程式的外界通道。
輸出流:
程式向輸出流寫入數據。將程式中的數據輸出到外界(顯示器,印表機,文件,網路,等)的通訊通道。
採用數據流的目的:使得輸入輸出獨立於設備,不關心數據源來自何方,也不管輸出的目的地是何種設備。
4.文件緩衝區
緩衝區:
指在程式運行時,所提供的額外記憶體,可用來暫時存放做準備執行的數據。它可在創建、訪問、刪除靜態數據上,大大提高運行速度(速度的提高程度有時甚至可高達幾十倍),
為我們提供了極大的便捷,節省了大量的時間與精力
文件緩衝區:
ANSIC 標準採用「緩衝文件系統」處理的數據文件的,所謂緩衝文件系統是指系統自動地在記憶體中為程式中每一個正在使用的文件開闢一塊「文件緩衝區」。
從記憶體向磁碟輸出數據會先送到記憶體中的緩衝區,裝滿緩衝區後才一起送到磁碟上。如果從磁碟向電腦讀入數據,則從磁碟文件中讀取數據輸入到記憶體緩衝區(充滿緩衝區),
然後再從緩衝區逐個地將數據送到程式數據區(程式變數等)。緩衝區的大小根據C編譯系統決定的。
如:
無論是輸入輸出,都先在緩衝區里存著,然後在進行輸入輸出。
5.文件指針
緩衝文件系統中,關鍵的概念是「文件類型指針」,簡稱「文件指針」。
每個被使用的文件都在記憶體中開闢了一個相應的文件資訊區,用來存放文件的相關資訊(如文件的名字,文件狀態及文件當前的位置等)。
這些資訊是保存在一個結構體變數中的。該結構體類型是有系統聲明的,取名FILE。
我們可以來看在VS 2019中FILE的聲明:
//-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
//
// Stream I/O Declarations Required by this Header
//
//-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
#ifndef _FILE_DEFINED
#define _FILE_DEFINED
typedef struct _iobuf
{
void* _Placeholder;
} FILE;
#endif
不同的C編譯器的FILE類型包含的內容不完全相同,但是大同小異。
每當打開一個文件的時候,系統會根據文件的情況自動創建一個FILE結構的變數,並填充其中的資訊,使用者不必關心細節。
一般都是通過一個FILE的指針來維護這個FILE結構的變數,這樣使用起來更加方便。
如下:我們便創建了一個文件指針
FILE* pf;//文件指針變數
定義pf是一個指向FILE類型數據的指針變數。可以使pf指向某個文件的文件資訊區(是一個結構體變數)。
通過該文件資訊區中的資訊就能夠訪問該文件。也就是說,通過文件指針變數能夠找到與它關聯的文件。
比如:
到此,我們的基本概念就結束了,下面進入到函數部分:
二.與文件操作相關的一些函數
1.文件的打開及關閉
文件在使用前,我們肯定要打開文件;在使用結束後,我們需要關閉文件。
1.fopen() — 文件打開函數
聲明:文檔
FILE* fopen(const char* filename, const char* mode);
參數:
const char* filename —- 文件名
const char* mode —- 文件打開方式
文件打開方式:
那,接下來看一個實例:
/* fopen example */
#include <stdio.h>
int main()
{
FILE* pFile;
pFile = fopen("myfile.txt", "w");//打開一個文件(沒有就創建),以寫的方式打開
if (pFile != NULL)//如果打開成功
{
fputs("fopen example", pFile);//就往文件里寫入 fopen example
fclose(pFile);//關閉文件
}
return 0;
}
2.fclose() — 文件關閉函數
聲明:文檔
int fclose(FILE* stream);
參數為要關閉文件的文件指針
示例:
int main()
{
//相對路徑
//.. 表示上一級目錄
//. 當前目錄
//FILE* pf = fopen("../data.txt", "r");//在上一級文件中打開data.txt,如果沒有就報錯
//絕對路徑 C:\Windows\System32\drivers\etc.txt
//./hehe/test.txt
//../../
FILE* pf = fopen("../../data.txt", "r");
if (pf == NULL)
{
printf("打開文件失敗\n");
printf("%s\n", strerror(errno));//注意頭文件的包含 stding.h errno.h
return 1;//失敗返回
}
//打開文件成功
printf("打開文件成功\n");
//讀寫文件
//...
//關閉文件
fclose(pf);
pf = NULL;//及時置NULL
return 0;
}
註:
其他的文件打開模式,將在函數講解的時候一併講解:
2.文件的順序讀寫
1. 字元輸入輸出函數
fput — 向指定輸出流輸出一個字元 聲明
int fputc ( int character, FILE * stream );
參數:
int character — 所輸入的字元
FILE * stream — 指定輸出流
fgetc — 向指定輸入流輸入一個字元 聲明
int fgetc ( FILE * stream );
參數:
FILE * stream — 指定輸入流
示例 1:
//在文件里寫入a-z26個字母
int main()
{
//fopen函數如果是以寫的形式打開
//如果文件不錯在,會創建這個文件
//如果文件存在,會清空文件的內容
//fopen函數如果是以讀的形式打開
//文件不存在打開失敗
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 1;//失敗返回
}
//寫文件
int i = 0;
for (i = 'a'; i <= 'z'; i++)
{
fputc(i, pf);//在文件里寫 --- pf 我們自己定義的文件指針
fputc(i, stdout);//顯示在螢幕上 --- stdout --- 標準輸出流
} // 從鍵盤輸入 --- stdin --- 標準輸入流
//關閉文件
fclose(pf);
pf = NULL;
return 0;
}
這裡需要注意的就是所指定的輸出流:
在C語言所寫的程式運行起來時,會默認打開三個流:
1.stdin – 標準輸入流 (鍵盤)
2.stdout – 標準輸出流 (螢幕)
3.stderr – 標準錯誤流(螢幕)
示例 2:
//從剛才寫的文件,再把內容讀出來
int main()
{
FILE* pf = fopen("data.txt", "r");// r 是以讀的形式打開文件,如果沒有該文件就報錯
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
//打開文件成功,讀文件
//int ch = fgetc(pf);
//printf("%c\n", ch);//a
//ch = fgetc(pf);
//printf("%c\n", ch);//b
//ch = fgetc(pf);
//printf("%c\n", ch);//c
//ch = fgetc(pf);
//printf("%c\n", ch);//d
int ch = 0;
while ((ch = fgetc(pf)) != EOF)// pf --- 所指定的輸入流
{
printf("%c ", ch);
}
//關閉文件
fclose(pf);
pf = NULL;
return 0;
}
這裡需要注意一點:
fgetc() 的返回值:我們在這就只說出現錯誤的資訊:
If the position indicator was at the end-of-file, the function returns EOF and sets the eof indicator (feof) of stream.
If some other reading error happens, the function also returns EOF, but sets its error indicator (ferror) instead.
翻譯過來就是:
如果文件指針處於文件末端,則函數返回EOF,並設置流的eof指示器(feof)
如果發生其他讀數錯誤,函數也會返回EOF,但會設置錯誤指示器(ferror)
這裡的feof和ferror我們專門會放到最後講
2.文本行輸入輸出函數
fputs — 文本行輸出行數 聲明
int fputs ( const char * str, FILE * stream );
參數:
const char * str — 將被寫入輸出流的字元指針
FILE * stream — 輸出流
fgets — 文本行輸入函數 聲明
char * fgets ( char * str, int num, FILE * stream );
參數:
char * str — 所輸入資訊的存放位置
int num — 要讀內容的大小
FILE * stream — 輸入流
示例 1:
int main()
{
FILE* pf = fopen("data.txt", "a");// a --- 如果沒有就該文件就創建,有就在該文件後方繼續追加內容
if (pf == NULL) // 而 w 如果存在該文件會覆蓋重寫
{
printf("%s\n", strerror(errno));
return 1;
}
//寫一行數據
fputs("hello\n", pf);//輸出到文件中
fputs("hello\n", stdout);//在螢幕上顯示
fputs("hello world\n", pf);//輸出到文件中
fputs("hello world\n",stdout);//在螢幕上顯示
fclose(pf);
pf = NULL;
return 0;
}
示例 2:
int main()
{
char arr[100] = {0};//存放寫入的資訊
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
//讀一行數據
//fgets(arr, 100, pf);
//printf("%s\n", arr);
while (fgets(arr, 10, pf) != NULL)//輸出---一次讀十個
{
printf("%s", arr);
}
//fgets從標準輸入流中讀取
fgets(arr, 100, stdin);
printf("%s\n", arr);
fclose(pf);
pf = NULL;
return 0;
}
注意:
1.在fgets中num的大小數包含 \0 ,在內的,如果所輸入的內容大小大於指定的大小,fgets會強行截斷加 \0.
2.返回值的處理:
If the end-of-file is encountered while attempting to read a character, the eof indicator is set (feof). If this happens before any characters could be read, the pointer returned is a null pointer
(and the contents of str remain unchanged).
If a read error occurs, the error indicator (ferror) is set and a null pointer is also returned (but the contents pointed by str may have changed).
簡單點說:遇到錯誤和文件結尾都會返回NULL,但會設置不同的指示器(feof,ferror)(後面會說)
3.格式化輸入輸出函數
fprintf — 格式化輸出函數 聲明
int fprintf ( FILE * stream, const char * format, ... );
fscanf — 格式化輸入函數 聲明
int fscanf ( FILE * stream, const char * format, ... );
別看他們倆長得花里胡哨的,但是使用卻和我們的printf,scanf大致相同,只是多出了一個 流 的填寫
我們來看看 printf 和 scanf的聲明:
int scanf ( const char * format, ... );
int printf ( const char * format, ... );
所以我們來看一個示例:
示例 1:
struct Stu
{
char name[20];
int age;
float score;
};
int main()
{
struct Stu s = {"zhangsan", 20, 66.5f};
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
//格式化的寫入
fprintf(pf,"%s %d %f", s.name, s.age, s.score);
//printf("%s %d %f", s.name, s.age, s.score);//對比一下只是差了一個 流 的指定
//關閉文件
fclose(pf);
pf = NULL;
return 0;
}
int main()
{
struct Stu s = {0};
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
//格式化的讀取
fscanf(pf, "%s %d %f", s.name, &(s.age), &(s.score));//從文件中讀取
fprintf(stdout, "%s %d %f\n", s.name, s.age, s.score);//輸出到螢幕上
//scanf("%s %d %f", s.name, &(s.age), &(s.score));
//關閉文件
fclose(pf);
pf = NULL;
return 0;
}
注意:
讀取錯誤或結束時,fscanf 的返回值也是EOF,判斷同 fgetc
這裡其實還有一組與它倆相似的函數 — sprintf 以及 sscanf (點擊函數名看文檔)
它倆是幹什麼的呢,一個是把結構化的數據轉換為字元串,一個是把字元串轉化為結構化的數據
同樣,我們來看看聲明:
int sprintf ( char * str, const char * format, ... );
char * str --- 我們要把格式化生成的字元串所存放的地址
int sscanf ( const char * s, const char * format, ...);
const char * s --- 我們所要讀取的字元串
示例:
struct Stu
{
char name[20];
int age;
float score;
};
int main()
{
struct Stu s = {"zhangsan", 20, 66.5f};
char buf[200] = { 0 };
//sprintf可以把結構化的數據轉換為一個字元串
sprintf(buf, "%s %d %f", s.name, s.age, s.score);
printf("按照字元串的形式:%s\n", buf);
struct Stu tmp = { 0 };
//sscanf可以把一個字元串轉換為一個結構化的數據
sscanf(buf, "%s %d %f", tmp.name, &(tmp.age), &(tmp.score));
printf("按照格式化的形式:%s %d %f\n", tmp.name, tmp.age, tmp.score);
return 0;
}
4.二進位輸入輸出函數
fwrite — 二進位寫 文檔
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
這個函數解釋一下就是: 將來自 ptr 指向的數據,一次寫size個位元組,共寫 count 次,輸出到 stream 指定的流中
fread — 二進位讀 文檔
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
這個函數解釋一下就是: 將 stream 指定的流中 讀 count 次,一次讀 size 個位元組,存到 ptr 所指向的內容中
示例:
// data.txt 內容 zhangsan 20 66.500000
int main()
{
struct Stu s = {0};
FILE* pf = fopen("data.txt", "rb");//binary --- 二進位
if (pf == NULL) // rb --- 二進位讀
{
printf("%s\n", strerror(errno));
return 1;
}
//讀文件-二進位
fread(&s, sizeof(struct Stu), 1, pf);
printf("%s %d %f\n", s.name, s.age, s.score);
fclose(pf);
pf = NULL;
return 0;
}
註:
若讀取時發現讀取的內容的個數比指定的最大個數小時,就結束,然後判斷是讀到文件末尾,還是讀取失敗
示例 2:
int main()
{
int a = 10000;
FILE*pf = fopen("bin.dat", "wb");//二進位寫
if (pf == NULL)
{
return 1;
}
fwrite(&a, sizeof(int), 1, pf);
fclose(pf);
pf = NULL;
return 0;
}
3.文件的隨機讀寫
1.seek — 根據文件指針的位置和偏移量來定位文件指針。
int fseek ( FILE * stream, long int offset, int origin );
FILE * stream --- 流
long int offset --- 偏移量
int origin --- 從哪裡開始,有三個選擇
1.SEEK_SET --- 從文件頭開始
2.SEEK_CUR --- 從當前位置開始
3.SEEk_END --- 從文件末尾開始
示例:
//此時文件內容為:123456789
int main()
{
FILE* fp = fopen("data.txt", "r");
if (fp == NULL)
{
perror("\n");
exit(1);
}
char a = fgetc(fp);
printf("%c ", a);//此時結果為 1,現在指針指向 2
a = fgetc(fp);//此時讀取2,指針指向 3
printf("%c ", a);
fseek(fp, -1, SEEK_END);//將文件指針置於文章末尾
a = fgetc(fp);//此時讀取9,指針再次指向末尾
printf("%c ", a);
fseek(fp, 1, SEEK_SET);//將文件指針置於文章頭
a = fgetc(fp);//此時讀取1,指針再次指向2
printf("%c ", a);
fclose(fp);
fp = NULL;
return 0;
}
2.ftell — 返迴文件指針相對於起始位置的偏移量
long int ftell ( FILE * stream );
3.rewind — 讓文件指針的位置回到文件的起始位置
void rewind ( FILE * stream );
示例:
int main()
{
FILE*pf = fopen("data.txt", "r");
if (pf == NULL)
{
return 1;
}
//讀取
int ch = fgetc(pf);
printf("%c\n", ch);
ch = fgetc(pf);
printf("%c\n", ch);
//定位文件指針到文件的起始位置
//fseek(pf, -2,SEEK_CUR);
//fseek(pf, 0, SEEK_SET);
//printf("%d\n", ftell(pf));
rewind(pf);
ch = fgetc(pf);//要在這裡讀取'a'
printf("%c\n", ch);
fclose(pf);
pf = NULL;
return 0;
}
4.文件快取區的刷新
我們曾在上文提到文件緩衝區的概念:
緩衝文件系統是指系統自動地在記憶體中為程式中每一個正在使用的文件開闢一塊「文件緩衝區」。
從記憶體向磁碟輸出數據會先送到記憶體中的緩衝區,裝滿緩衝區後才一起送到磁碟上。
如果從磁碟向電腦讀入數據,則從磁碟文件中讀取數據輸入到記憶體緩衝區(充滿緩衝區),然後再從緩衝區逐個地將數據送到程式數據區(程式變數等)。
緩衝區的大小根據C編譯系統決定的。
所以,當我們在程式中命令電腦往文件中寫一些東西的時候,如果我們想在輸出語句已結束,文件就有內容(即需要寫的內容從文件緩衝區寫到了文件中),
此時,我們不妨 fflush 一下
fflush — 刷新文件快取區
int fflush ( FILE * stream );
參數就是我們所定義的文件指針。
示例:
#include <stdio.h>
#include <windows.h>
//VS2019 WIN10環境測試
int main()
{
FILE* pf = fopen("test.txt", "w");
fputs("abcdef", pf);//先將程式碼放在輸出緩衝區
printf("睡眠10秒-已經寫數據了,打開test.txt文件,發現文件沒有內容\n");
Sleep(10000);
printf("刷新緩衝區\n");
fflush(pf);//刷新緩衝區時,才將輸出緩衝區的數據寫到文件(磁碟)
//註:fflush 在一些編譯器上並不能使用
printf("再睡眠10秒-此時,再次打開test.txt文件,文件有內容了\n");
Sleep(10000);
fclose(pf);
//註:fclose在關閉文件的時候,也會刷新緩衝區
pf = NULL;
return 0;
}
三.一個易被誤用的點
feof的錯誤使用
在剛才的講解中,我們已經都說過了各輸入函數遇到錯誤時的返回值,所以我們在寫文件循環讀取的循環條件時,一定要注意各函數的返回值的區別 !
在不滿足循環條件後 判斷是發生錯誤跳出,還是讀到文本末尾結束(這就是前面所說到的指示器 feof — 文本結束, ferror — 遇到錯誤)
如圖:
示例:
int main()
{
FILE*pf = fopen("data.txt", "r");
if (pf == NULL)
{
return 1;
}
//讀取
int ch = 0;
while ((ch = fgetc(pf)) != EOF)
{
printf("%c ", ch);
}
//找結束的原因
if (ferror(pf))
{
printf("讀取是發生錯誤,失敗,而結束\n");
}
else if (feof(pf))
{
printf("遇到文件末尾,而結束的\n");
}
fclose(pf);
pf = NULL;
return 0;
}
所以,我們在日常使用的時候要注意這個點!
|——————————————————————
到此,對於文件管理的講解便結束了!
因筆者水平有限,若有錯誤,還望指正!