C語言文件讀寫——文本文件讀操作
最近和幾個初學C語言的朋友討論文件讀寫,發現他們在使用C語言文件讀寫功能的時候遇到了不少問題,不是文件打開方式有問題,就是文件讀寫有問題,總是得不到自己想要的結果。
C語言文件讀寫操作,既簡單又複雜,要熟練使用每一項功能,也確實不易,既有文本文件操作,也有二進制文件的讀寫。本文先比較詳細地介紹C語言的文本文件讀操作,下一篇會詳細介紹文本文件的寫使用。
什麼是文本文件
文本文件是由一行一行的字符的有序序列組成的。一行是由0個或者若干個字符加上一個行結束符'\n'(換行符)組成的,但是最後一行的最後一個字符是否是'\n'可能在不同的平臺的實現不太一樣,但是由於是最後一行的最後一個字符,所以並不會影響正常讀寫,但是我們在寫文本文件的時候,最好也保證最有一個字符是行結束符'\n'。
在Windows上,爲了方便人閱讀文本文檔,在寫入'\n'的時候,系統會自動將'\n'轉換爲'\r\n'(回車換行),在讀取的時候會自動將'\r\n'轉換爲'\n'。在Linux上,並不會有這種轉換,所以Windows上的文本文件在Linux上比較低的版本上進行讀寫的時候有時候需要進行一下轉換,但是現在的Linux系統基本上已經能夠正常處理'\r\n'了。
爲了能夠保證文本文件的正常讀寫,要求:
- 寫入的字符是可打印字符,或者一些特殊的控制符,比如'\t'(TAB鍵),'\n'。
- 不要在空格字符後緊跟'\n'字符,否則可能在讀取的時候空格會丟失。
- 最後一個字符是'\n'。
讀文本文件示例
說了這麼多,我們先直接來一個讀取文本文件內容的例子,然後再做一下具體分析,這個文本文件的內容就是本文檔的部分內容,我們把它讀取出來,然後在控制檯顯示,我們以Windows系統爲例,Linux下面的操作方式是類似的,讀取文本文件的示例代碼如下:
#include <stdio.h>
void read_text(const char* file_name)
{
char line[1024]={0};
FILE *file = fopen(file_name,"rt");
if(!file)
return;
while(1)
{
//文件讀取結束
if(EOF == fscanf(file,"%s",line))
break;
printf("%s\n",line);
}
fclose(file);
}
int main(int argc, char* argv[])
{
read_text("test.txt");
return 0;
}
然後運行程序,看效果是否是我們期望的那樣,一行一行地顯示在控制檯,如下圖所示,上邊是程序的輸出,下邊是test.txt文件的內容。
讀文本文件詳解
C語言中打開文件,主要有兩個函數,fopen和fopen_s,fopen_s是C11標準中新引入的,它們的函數申明是這樣的:
(1)
FILE *fopen( const char *filename, const char *mode ); (until C99)
FILE *fopen( const char *restrict filename, const char *restrict mode );(since C99)
(2)
errno_t fopen_s(FILE *restrict *restrict streamptr,
const char *restrict filename,
const char *restrict mode);(since C11)
fopen_s會做一些額外的檢查,更“安全”一些,C11標準後,有很多這樣的_s的函數,都是爲了更加安全,會做一些安全性的檢查,比如函數傳入的指針是否爲空,傳入的緩衝區是否會溢出等等。
返回值FILE*就代表了一個文件流對象,如果爲NULL,則表示打開文件失敗。
重點看一下mode,即打開文件的方式,常用的mode主要有:
mode | 含義 | 說明 | 如果文件存在 | 如果文件不存在 |
“r” | 讀 | 以讀的方式打開文件,打開以後只能讀 | 成功打開,並從文件開始讀 | 打開失敗 |
"w" | 寫 | 創建一個文件進行寫 | 文件內容會被清空 | 創建一個新文件 |
"a" | 追加 | 追加內容到文件末尾 | 將文件內容追加到文件末尾 | 創建一個新文件 |
"r+" | 擴展讀 | 打開文件進行讀寫,可讀可寫 | 從文件開始讀 | 打開失敗 |
"w+" | 擴展寫 | 創建一個文件進行讀寫 | 文件內容會被清空 | 創建一個新文件 |
"a+" | 擴展追加 | 打開文件進行讀寫 | 追加內容到文件末尾 | 創建一個新文件 |
11啊
除了這些基本的打開模式之外,還可以附加一些別的模式,比如“t”,“b”等,例如“rb”,表示以二進制方式打開文件,"wt"以文本文件的方式創建文件,默認是文本文件方式,所以“t”一般可以省略。
從上面的表格我們可以簡單總結出來,只要是有“r”標誌,文件就要求存在,否則就會打開失敗,其他的打開模式都不需要文件一定存在,如果文件不存在,則會創建一個新文件。所以,在進行文件讀寫之前,一定要明確自己的目的,是讀文件,還是寫文件,是從文件開始寫,還是追加內容到文件末尾。
本節的主題是讀文本文件,所以我們在使用fopen的時候,就使用"r"或者"rt"標誌,不要使用別的模式。
說到讀文本文件內容,不是一件簡單事情,因爲讀文本文件內容也涉及到好些函數,如果選擇不當,得到的結果也會不是我們期望的。
其實文本文件的內容雖然在前面介紹過,要作爲文本文件是有要求的,比如前面提到的每行要求以'\n'結束等。但是文本文件內容本身又可以分爲有格式的和沒有格式的,比如我們前面的test.txt文件,就是沒有格式的,就是純文本文件,因爲除了換行之外,沒有任何別的格式,信息是雜亂的。如果我們查看一下一個普通的Excel文件,裏面的數據就很有格式,一行一行,一列一列,非常整齊,這就是有格式的文本文件樣子,我們看一個有格式的文本文件,這是一個簡易的學生名單文件student.txt,每一行包含的內容爲學號,姓名,所屬學院,成績,如下圖所示。
這個student.txt文件內容就是格式化的,即每一行的數據格式都是一致的,包含的信息都是類似的,即都包含學號,姓名,學院和分數,而且它們之間是用空格隔開的,這就是格式化的文件數據。
在讀取非格式化和格式化數據的時候,需要用到的函數是不一樣的,否則,用非格式化數據讀取函數去讀取格式化的數據,讀取到的內容我們也不好利用,同樣,用格式化數據讀取函數去讀取非格式化的數據,有時候也得不到正確結果。
非格式化文本文件讀取
非格式文本文件內容讀取主要有兩個函數,fgetc和fgets,fgetc的函數原型爲:
int fgetc( FILE *stream ); |
每次從文件中讀取一個字符,直到文件結尾。如果讀到文件結尾,返回EOF。
fgetc不會對文件中的內容做任何處理,一次就讀一個字符,'\n'也會想普通字符一樣讀取,我們來看一下使用fgetc讀取我們前面提到的text.txt文件的效果,代碼如下:
void read_text_by_getc(const char* file_name)
{
int ch = 0;
FILE *file = fopen(file_name,"rt");
if(!file)
return;
while(EOF != (ch = fgetc(file)))
{
//在屏幕上輸出讀到的每一個字符
putchar(ch);
}
fclose(file);
}
則執行效果下圖所示。
fgetc對於讀取非格式化的文本內容,就是最真實地反應了文本文件中的內容。
但是fgetc讀取容易,處理起來卻很麻煩,如果只是在屏幕上顯示文件內容的話,沒有問題,如果還要想對讀取的內容進行處理的話,就麻煩一些。
我們再來看一下fgets函數。
fgets的函數原型爲:
char *fgets( char *restrict str, int count, FILE *restrict stream ); |
(since C99) |
每次從文件中讀取指定長度的內容,讀取的長度最多爲count-1個字符。讀到'\n'的時候,這一次讀取就結束了,即使還沒有讀到count-1個字符。如果讀取失敗,返回NULL。
我們先看一下使用fgets讀取test.txt文本文件的效果,代碼如下:
void read_text_by_gets(const char* file_name)
{
char buffer[20]={0};
FILE *file = fopen(file_name,"rt");
if(!file)
return;
while(NULL != (fgets(buffer,sizeof(buffer),file)))
{
//顯示每一次讀取到的內容
printf("%s",buffer);
}
fclose(file);
}
然後運行,結果如下所示。
從圖中可以看到,顯示結果一塌糊塗,爲什麼呢?因爲我們的緩衝區buffer大小是20,所以每次只讀19個字符,有時候剛好得到半個漢字,就顯示很凌亂了。
因此,如果我們想完整的一次讀取一行的數據,這個緩衝區buffer必須足夠大,至少要超過文件中最長的一行字符的長度,我們修改一下程序,我們知道我們的test.txt文件中每行都不會太長,所以我們把buffer的大小設置成128,看看效果如何,如下圖所示。
因此,我們在對於非格式化文本進行讀取的時候,要選擇合適的函數來讀取,如果想一行一行的讀取文本文件中的內容,最好使用fgets,並且提供足夠大的緩衝區。
格式化文本文件讀取
格式化文本文件的讀取主要用到函數fscanf和fscanf_s。
先看一下fscanf函數的原型。
int fscanf( FILE *restrict stream, const char *restrict format, ... ); |
(since C99) |
fscanf和scanf一樣,有一個格式化字符串format參數,因此在讀取文本內容的時候會嚴格按照這個format格式去讀,因此,對於我們的讀取結構化的數據就非常方便,以我們的student.txt爲例,我們再來看一下student.txt文件的內容。
1001 張三 計算機學院 90.0
1002 李四 計算機學院 98.7
1003 王五 軟件學院 88
1004 黃渤海 人文學院 97
每一行代表一個學生,包含學號,姓名,所屬學院以及成績。
當使用fscanf讀取的時候,同樣,遇到'\n',一次讀取就結束了,假設我們有四個變量ID,Name,College,Score,我們可以用scanf一次把一行的數據都讀取到這四個變量中,我們看一下代碼:
void read_formated_text(const char* file_name)
{
int ID;
char Name[32]={0};
char College[128]={0};
float Score = 0.0;
FILE *file = fopen(file_name,"rt");
if(!file)
return;
printf("學生名單爲:\n");
while(1)
{
//文件讀取結束
if(EOF == fscanf(file,"%d %s %s %f",&ID,Name,College,&Score))
break;
printf("學號:%d 姓名:%s 學院:%s 分數:%.2f\n",ID,Name,College,Score);
}
fclose(file);
}
運行結果下圖所示。
如果我們有一個結構體Student的話,也可以通過這種方式把一行的數據直接讀取到一個結構體中,代碼如下:
void read_formated_text_by_struct(const char* file_name)
{
struct Student stu;
FILE *file = fopen(file_name,"rt");
if(!file)
return;
printf("學生名單爲:\n");
while(1)
{
//文件讀取結束
if(EOF == fscanf(file,"%d %s %s %f",&stu.ID,stu.Name,stu.College,&stu.Score))
break;
printf("學號:%d 姓名:%s 學院:%s 分數:%.2f\n",stu.ID,stu.Name,stu.College,stu.Score);
}
fclose(file);
}
結果是一樣的。
現在簡單介紹一下fscanf_s,其函數原型爲:
int fscanf_s(FILE *restrict stream, const char *restrict format, ...); |
(since C11) |
對於這個函數的使用,牢記住一點就好了,如果讀取的是字符串類型,需要在字符串變量後面在跟一個字符串長度的值來指明字符串變量或者數組能夠容納多少個字符,比如我們前面的讀取student.txt文件的代碼就可以改爲:
void read_formated_text_by_struct(const char* file_name)
{
struct Student stu;
FILE *file = fopen(file_name,"rt");
if(!file)
return;
printf("學生名單爲:\n");
while(1)
{
//文件讀取結束
if(EOF == fscanf_s(file,"%d %s %s %f",&stu.ID,stu.Name,sizeof(stu.Name)-1,stu.College,sizeof(stu.College)-1,&stu.Score))
break;
printf("學號:%d 姓名:%s 學院:%s 分數:%.2f\n",stu.ID,stu.Name,stu.College,stu.Score);
}
fclose(file);
}
因爲Name和College都是字符串類型,所以在它們後面都跟了一個長度。執行結果仍然一樣。
到了這裏,大家以爲文本文件的讀取內容應該已經結束了,其實不是,如果你是C語言的初級選手或者剛學計算機不太久,後面的內容可以暫時跳過。
窄字符與寬字符
窄字符和寬字符都是指對字符的編碼,窄字符就是一個字符佔有的空間只有一個字節,寬字符通常一個字符佔用兩個字節的空間,我們也常常說寬字符爲UNICODE編碼字符。
我們前面所講的讀取文件,這些函數其實都是針對窄字符的,因爲這些文件內容的編碼都是窄字符編碼,一個英文是一個字符,一個漢字是兩個字符,它們都是一個字符佔用一個字節。
如果是寬字符的話,一個字符佔用兩個字節,每一個英文字符還是一個字符,一箇中文漢字通常也是一個字符,但是它們都佔用兩個字節。我們把test.txt文件另存一下,存爲UNICODE編碼,用記事本的另存功能,選擇UNICODE,如圖所示。
如果我們此時還是用前面的代碼來讀取test_unicode.txt文件,效果會是什麼樣呢?我們以read_text_by_getc來驗證一下,結果如圖所示。
可以再次用一塌糊塗來形容,什麼也不是了。
這就涉及到另外的讀取函數了,是專門針對寬字符的,主要也有這幾個函數,與窄字符對應的函數分別爲:
wint_t fgetwc( FILE *stream ); |
(since C95) |
wchar_t *fgetws( wchar_t * restrict str, int count, FILE * restrict stream ); |
(since C99) |
int fwscanf( FILE *restrict stream, |
(since C99) |
int fwscanf_s( FILE *restrict stream, |
(5) | (since C11) |
它們的用法和窄字符的用法完全一樣,唯一的不同是它們都要求被讀取的文件的內容是UNICODE即寬字符編碼的。
這裏不做試驗了,有的朋友如果有興趣的話,自己可以試一試,看看效果。
《完》