前些時候,我們學習的C語言程序都是由輸入輸出和算法組成的控制臺程序。我們在終端上來輸入我們提供的數據,然後程序也會通過終端來告訴我們最終運行的結果。
但是,可能有的同學已經觀察到了,我們日常使用的別人開發的程序,大多數都是通過文件來提供數據的。比如一個Excel的報表,程序可以直接來分析裡面的數據。再比如,一個TXT格式的電子書,程序可以直接分析有多少字、多少個章節,甚至還可以生成出一個目錄來。
擁有這樣能力的程序,是不是感覺功能強大了許多?這就要用到我們今天要講到的內容——「文件操作」。
關於文件在我們比較熟悉的Windows系統下,文件類型的區分是用「擴展名」來進行的。但其實擴展名並不是指「文件格式」,它只是一個「門牌號」而已。至於它到底對不對,那系統就不知道了。可能有很多的新手,在遇到格式的問題的時候,會認為直接更改擴展名,就能實現格式轉換。不瞞你們說,我小時候也有過這種想法。但是後來發現,不行。舉個例子,現在有一個 MP3 的文件,要轉成 AAC。這兩個文件從編碼上來講,就是不一樣的。MP3 只能用 MP3 的方式去讀取,AAC 只能用 AAC 的方式去讀取。如果你把擴展名直接改成 AAC,那麼系統就被你騙了,就會用 AAC 的方式去讀取實際還是 MP3 的文件,當然是不行了。
不同的擴展名,就對應了不同的讀取方式。「EXE」 就代表 Windows 系統下的可執行二進位文件,「TXT」是純文本文件,等等。
在 Linux 和 Unix 作業系統下,文件的定義就寬泛多了。不光軟體,硬體也可以叫文件。也就是說,硬體實際上也是當做文件的方式來處理的。
在C語言中,文件一般分為兩種,一種是二進位文件,就是我們編譯出來的那個東西,我們是看不懂的;另一種是文本文件,也就是我們常說的原始碼。
打開和關閉文件我們要對一個文件進行操作,首先我們需要把文件打開,然後才能讀或者寫。對文件操作完成後,我們還要將文件關閉。
C語言中的打開文件使用fopen函數,通式如下:
fopen("文件路徑", "模式")
如果打開文件成功,則會返回一個FILE結構的指針,通過這個指針,我們就可以對這個文件進行操作;如果打開文件失敗,則會返回NULL。
下面是所有的模式:
模式功能"r"以只讀的形式打開文件,並從頭開始讀取前面幾個都好理解,只是最後一個,為啥要區分一個二進位出來呢?
不加「b」的情況下,就是以文本的形式來打開。因為在不同的作業系統中,換行符是不同的。Unix系統用\n,MacOS用\r,而Windows用的是\r\n,那麼在文本模式下打開,C語言會根據系統環境的不同,來轉化換行符。而在二進位的模式下,就不會進行任何的轉換。
當你對文件操作完畢後,一定要記得把文件用fclose()函數來關閉。其實我們在打開文件後的所有操作,實際上都被記錄到了緩存裡,只有執行了關閉後,我們的更改才會生效。如果關閉成功,則函數會返回0;失敗的話,就會返回EOF。關閉成功後,我們創建的文件指針就會失效。
//Example 01
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
FILE* f;
int chr;
if ((f = fopen("file1.txt", "r")) == NULL)
{
printf("打開失敗!\n");
exit(EXIT_FAILURE);
}
while ((chr = getc(f)) != EOF)
{
putchar(chr);
}
fclose(f);
return 0;
}//file1.txt中的內容
C programming makes me happy!//Consequence 01
C programming makes me happy!
順序讀寫文件打開了文件之後,就可以進行我們的操作了。
讀寫單個字符讀取單個字符,我們可以用fgetc和getc這兩個來實現。它們的作用,就是讀取一個字符,然後將光標移動到下一個位置。
#include <stdio.h>
...
int fgetc(FILE* stream);
int getc(FILE* stream);函數的參數,是一個FILE結構體的指針,也就是一個準備讀取的文件流。讀取成功就會將讀取到的unsigned char內容轉化為int並返回;文件結束或者讀取失敗就返回EOF。
這倆函數不同的地方就在於,fgetc是函數實現,而getc是用宏實現。宏會產生大量的代碼量,但是沒有函數調用堆棧的步驟,所以速度會快很多。但是宏的展開可能會多次調用參數,因此如果參數中含有自增、自減這種副作用的的方法,就只能用函數實現的fgetc了。
寫入單個字符,我們可以用fputc和putc,帶有f的,就是函數,另一個就是宏的實現的了。
#include <stdio.h>
...
int fputc(int c, FILE* stream);
int putc(int c, FILE* stream);第一個參數是你要寫入的字符,第二個是你要寫入的文件流。
讀寫整個字符串這裡就要用到fgets和fputs兩個函數了。
#include <stdio.h>
...
char* fgets(char* s, int size, FILE* stream);
int fputs(const chat* s, FILE* stream);其中,fgets有三個參數,第一個是一個字符型指針,用來存放讀取的數據;第二個用來指定讀取的長度(包含'\0');第三個是用於指定讀取的文件流。
函數調用成功後,會返回第一個參數所指向的地址。如果讀取到EOF則eof指示器被設置。若一開始就讀取到EOF,第一個參數的內容不變,返回NULL。若讀取發生錯誤,則error指示器被設置,函數返回NULL,第一個參數內容可能會被改變。
fputs第一個參數用於存放待寫入的數據,第二個是指定待寫入的文件流。函數調用成功,返回一個非 0 值,失敗則返回EOF。
格式化讀寫文件在文件裡,我們就不能用我們熟悉的scanf和printf了。但是C語言也提供一組類似的函數:fscanf和fprintf。
用法上,第一個參數用於指定文件流,後面的就是照搬的scanf和printf中的參數。
//Example 02
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main(void)
{
FILE* fp;
struct tm* p;
time_t t;
time(&t);
p = localtime(&t);
//寫入日期到文件
if ((fp = fopen("date.txt", "w")) == NULL)
{
printf("打開文件失敗!\n");
exit(EXIT_FAILURE);
}
fprintf(fp, "%d-%d-%d", 1900 + p -> tm_year, 1 + p -> tm_mon, p -> tm_mday);
fclose(fp);
//讀取文件日期,輸出到終端
int year, month, day;
if ((fp = fopen("date.txt", "r")) == NULL)
{
printf("打開文件失敗!\n");
exit(EXIT_FAILURE);
}
fscanf(fp, "%d-%d-%d", &year, &month, &day);
printf("%d-%d-%d\n", year, month, day);
fclose(fp);
return 0;
}//date.txt中的內容
2020-6-15//Consequence 02
2020-6-15二進位讀寫我們用fopen函數可以用二進位的方式來打開一個文件,但實際上我們要用二進位的方式來讀寫,還得用相應的函數才行。
C語言提供了fread和fwrite兩個函數來實現二進位的讀取和寫入。
#include <stdio.h>
...
size_t fread(void* ptr, size_t size, size_t nmemb, FILE* stream);
size_t fwrite(const void* ptr, size_t size, size_t nmemb, FILE* stream);首先來看fread。這個函數有四個參數。第一個指向存放數據的地址,第二個指定讀取的每個元素的尺寸,第三個指定準備讀取的元素個數,最後一個指向待讀取的文件流。
函數調用成功,會返回讀取到的元素個數,如果實際讀取的比第三個參數小,那麼可能會一直讀取到文件末尾或者發生錯誤,這種情況就要通過foef和ferror來進一步判斷。
然後是fwrite,也是有四個參數。第一個是指向存放數據的地址,第二個是指定待寫入的每個元素的尺寸,第三個是指定待寫入的元素的個數,最後一個是指向待寫入的文件流。
隨機讀寫文件剛剛我們介紹的,都是從文件頭開始讀寫。但是我們實際生產生活中,很多時候我們是需要任意修改的。比如改一個文檔,很有可能是中間的什麼地方錯了,或者是表達有不妥。那麼這個時候如果你還要從頭開始去檢索,那樣效率就太低了。
於是,C語言也為我們提供了這個功能,就是隨機讀寫。
首先,我們要了解光標的位置,才能夠更好地運用這個功能。C語言為我們提供了ftell函數,它可以告訴我們現在的光標位置。
#include <stdio.h>
...
long ftell(FILE* stream);如果將一個文件看成一個數組,那麼這個函數返回的就是這個數組的下標。
//Example 01
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
FILE* fp;
if ((fp = fopen("data.txt", "w")) == NULL)
{
printf("文件打開失敗!\n");
exit(EXIT_FAILURE);
}
printf("%ld\n", ftell(fp));
fputc('T', fp);
printf("%ld\n", ftell(fp));
fputs("echZone\n", fp);
printf("%ld\n", ftell(fp));
fclose(fp);
return 0;
}//data.txt中的內容
TechZone//Consequence 01
0
1
10如果你想將光標快速移動到文件頭,可以用rewind函數來實現。
...
rewind(fp);
fputs("Hello", fp);
fclose(fp);
...//data.txt中的內容
Helloone可以看到,它會覆蓋我們前面的數據。
有的同學可能會說了,你這不還是沒解決問題嗎?
好的,那就來解決下問題吧。C語言給我們提供了一個函數fseek,這個函數可以直接把光標跳轉到我們想要的位置。
#include <stdio.h>
...
int fseek(FILE* stream, long int offset, int whence);第一個參數是指的我們要讀取的文件流,第二個是偏移量(往後走是正數,往前走是負數),第三個是指的開始偏移的位置。
值描述SEEK_SET文件開頭SEEK_CUR當前位置SEEK_END文件末尾如果我要定位到第一百個字符的位置,那麼:
fseek(fp, 100, SEEK_SET)倒數第 10 個就要這樣:
fseek(fp, -10, SEEK_END)
標準流標準輸入,標準輸出和標準錯誤輸出一般C語言程序在執行的時候,都會有 3 個面向終端的文件流,分別是「標準輸入」,「標準輸出」和「標準錯誤輸出」。我們之前用printf的時候,其實就是在往標準輸出流中寫入字符串;用scanf的時候,其實就是函數在從標準輸入流中讀取字符串。當然,我們寫的程序也不可能一直都是正確的,警告和報錯的情況時有發生,這個時候其實就是對標準錯誤輸出中寫入數據。
這三個流,我們就將它們稱為:「標準流」
C語言分別為這三個標準流提供了對應的文件指針:stdin,stdout,stderr
比如打開文件失敗的時候,就可以這樣顯示:
...
fputs("打開文件失敗!\n", stderr);
exit(EXIT_FAILURE);
...這樣就不用printf這種「不專業」的錯誤指示方法了。
打開文件失敗!
錯誤處理每個流的內部都有兩個指示器。一個是「文件結束指示器feof」,當遇到文件末尾時被設置;另一個是「錯誤指示器ferror」,當讀寫文件出錯時被設置。
...
if (ferror(fp))
{
fputs("出錯了!\n", stderr);
}
...而使用clearerr可以人為地清除兩個指示器的狀態:
...
clearerr(fp);
...錯誤指示器只能判斷是否出了錯誤,但具體是什麼錯誤,那就要看errno和perror了。
首先看errno。這個函數包含在errno.h這個頭文件中。它會返回一個錯誤碼。
#include <errno.h>
...
printf("打開文件失敗:%d\n", errno);
...舉個例子:
打開文件失敗:2但是這個錯誤代碼不是所有人都知道它的含義。所以C語言又提供了一個函數perror,它可以直接用文字來提示我們錯誤的地方。
#include <stdio.h>
...
perror("打開文件失敗,原因是");
...結果是這樣的:
打開文件失敗,原因是:No such file or directory中間的冒號是自動加上的。
C語言基礎內容大體到這裡就結束了。我們也終於算是入門了C語言。或許以後在你的開發生涯中,用的最多的不是C語言,但這門語言對你帶來的提升,那是不可忽視的。C語言的文章自此就告一段落,以後還會寫一些進階的內容,但不會連續發布了。如果你有什麼好的題材或者是問題,都可以私信提供給我,我會考慮把它們寫進文章的。最後,祝各位學有所成!
●編號767,輸入編號直達本文
●輸入m獲取文章目錄
分享C/C++技術文章