https://m.toutiaocdn.com/i6847739122979701260/?app=news_article×tamp=1594400967&use_new_style=1&req_id=20200711010927010129040078043AEF9D&group_id=6847739122979701260
前些時(shí)候,我們學(xué)習(xí)的C語言程序都是由輸入輸出和算法組成的控制臺程序。我們在終端上來輸入我們提供的數(shù)據(jù),然后程序也會通過終端來告訴我們最終運(yùn)行的結(jié)果。
但是,可能有的同學(xué)已經(jīng)觀察到了,我們?nèi)粘J褂玫膭e人開發(fā)的程序,大多數(shù)都是通過文件來提供數(shù)據(jù)的。比如一個(gè)Excel的報(bào)表,程序可以直接來分析里面的數(shù)據(jù)。再比如,一個(gè)TXT格式的電子書,程序可以直接分析有多少字、多少個(gè)章節(jié),甚至還可以生成出一個(gè)目錄來。
擁有這樣能力的程序,是不是感覺功能強(qiáng)大了許多?這就要用到我們今天要講到的內(nèi)容——「文件操作」。
在我們比較熟悉的Windows系統(tǒng)下,文件類型的區(qū)分是用「擴(kuò)展名」來進(jìn)行的。但其實(shí)擴(kuò)展名并不是指「文件格式」,它只是一個(gè)「門牌號」而已。至于它到底對不對,那系統(tǒng)就不知道了??赡苡泻芏嗟男率?,在遇到格式的問題的時(shí)候,會認(rèn)為直接更改擴(kuò)展名,就能實(shí)現(xiàn)格式轉(zhuǎn)換。不瞞你們說,我小時(shí)候也有過這種想法。但是后來發(fā)現(xiàn),不行。舉個(gè)例子,現(xiàn)在有一個(gè) MP3 的文件,要轉(zhuǎn)成 AAC。這兩個(gè)文件從編碼上來講,就是不一樣的。MP3 只能用 MP3 的方式去讀取,AAC 只能用 AAC 的方式去讀取。如果你把擴(kuò)展名直接改成 AAC,那么系統(tǒng)就被你騙了,就會用 AAC 的方式去讀取實(shí)際還是 MP3 的文件,當(dāng)然是不行了。
不同的擴(kuò)展名,就對應(yīng)了不同的讀取方式。「EXE」 就代表 Windows 系統(tǒng)下的可執(zhí)行二進(jìn)制文件,「TXT」是純文本文件,等等。
在 Linux 和 Unix 操作系統(tǒng)下,文件的定義就寬泛多了。不光軟件,硬件也可以叫文件。也就是說,硬件實(shí)際上也是當(dāng)做文件的方式來處理的。
在C語言中,文件一般分為兩種,一種是二進(jìn)制文件,就是我們編譯出來的那個(gè)東西,我們是看不懂的;另一種是文本文件,也就是我們常說的源代碼。
我們要對一個(gè)文件進(jìn)行操作,首先我們需要把文件打開,然后才能讀或者寫。對文件操作完成后,我們還要將文件關(guān)閉。
C語言中的打開文件使用fopen函數(shù),通式如下:
fopen('文件路徑', '模式')
如果打開文件成功,則會返回一個(gè)FILE結(jié)構(gòu)的指針,通過這個(gè)指針,我們就可以對這個(gè)文件進(jìn)行操作;如果打開文件失敗,則會返回NULL。
下面是所有的模式:
前面幾個(gè)都好理解,只是最后一個(gè),為啥要區(qū)分一個(gè)二進(jìn)制出來呢?
不加「b」的情況下,就是以文本的形式來打開。因?yàn)樵诓煌牟僮飨到y(tǒng)中,換行符是不同的。Unix系統(tǒng)用\n,MacOS用\r,而Windows用的是\r\n,那么在文本模式下打開,C語言會根據(jù)系統(tǒng)環(huán)境的不同,來轉(zhuǎn)化換行符。而在二進(jìn)制的模式下,就不會進(jìn)行任何的轉(zhuǎn)換。
當(dāng)你對文件操作完畢后,一定要記得把文件用fclose()函數(shù)來關(guān)閉。其實(shí)我們在打開文件后的所有操作,實(shí)際上都被記錄到了緩存里,只有執(zhí)行了關(guān)閉后,我們的更改才會生效。如果關(guān)閉成功,則函數(shù)會返回0;失敗的話,就會返回EOF。關(guān)閉成功后,我們創(chuàng)建的文件指針就會失效。
//Example 01//學(xué)習(xí)交流群:782648055#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中的內(nèi)容C programming makes me happy!
//Consequence 01C programming makes me happy!
打開了文件之后,就可以進(jìn)行我們的操作了。
讀取單個(gè)字符,我們可以用fgetc和getc這兩個(gè)來實(shí)現(xiàn)。它們的作用,就是讀取一個(gè)字符,然后將光標(biāo)移動到下一個(gè)位置。
#include <stdio.h>...int fgetc(FILE* stream);int getc(FILE* stream);
函數(shù)的參數(shù),是一個(gè)FILE結(jié)構(gòu)體的指針,也就是一個(gè)準(zhǔn)確讀取的文件流。讀取成功就會將讀取到的unsigned char內(nèi)容轉(zhuǎn)化為int并返回;文件結(jié)束或者讀取失敗就返回EOF。
這倆函數(shù)不同的地方就在于,fgetc是函數(shù)實(shí)現(xiàn),而getc是用宏實(shí)現(xiàn)。宏會產(chǎn)生大量的代碼量,但是沒有函數(shù)調(diào)用堆棧的步驟,所以速度會快很多。但是宏的展開可能會多次調(diào)用參數(shù),因此如果參數(shù)中含有自增、自減這種副作用的的方法,就只能用函數(shù)實(shí)現(xiàn)的fgetc了。
寫入單個(gè)字符,我們可以用fputc和putc,帶有f的,就是函數(shù),另一個(gè)就是宏的實(shí)現(xiàn)的了。
#include <stdio.h>...int fputc(int c, FILE* stream);int putc(int c, FILE* stream);
第一個(gè)參數(shù)是你要寫入的字符,第二個(gè)是你要寫入的文件流。
這里就要用到fgets和fputs兩個(gè)函數(shù)了。
#include <stdio.h>...char* fgets(char* s, int size, FILE* stream);int fputs(const chat* s, FILE* stream);
其中,fgets有三個(gè)參數(shù),第一個(gè)是一個(gè)字符型指針,用來存放讀取的數(shù)據(jù);第二個(gè)用來指定讀取的長度(包含'\0');第三個(gè)是用于指定讀取的文件流。
函數(shù)調(diào)用成功后,會返回第一個(gè)參數(shù)所指向的地址。如果讀取到EOF則eof指示器被設(shè)置。若一開始就讀取到EOF,第一個(gè)參數(shù)的內(nèi)容不變,返回NULL。若讀取發(fā)生錯(cuò)誤,則error指示器被設(shè)置,函數(shù)返回NULL,第一個(gè)參數(shù)內(nèi)容可能會被改變。
fputs第一個(gè)參數(shù)用于存放待寫入的數(shù)據(jù),第二個(gè)是指定待寫入的文件流。函數(shù)調(diào)用成功,返回一個(gè)非 0 值,失敗則返回EOF。
在文件里,我們就不能用我們熟悉的scanf和printf了。但是C語言也提供一組類似的函數(shù):fscanf和fprintf。
用法上,第一個(gè)參數(shù)用于指定文件流,后面的就是照搬的scanf和printf中的參數(shù)。
//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中的內(nèi)容2020-6-15
//Consequence 022020-6-15
我們用fopen函數(shù)可以用二進(jìn)制的方式來打開一個(gè)文件,但實(shí)際上我們要用二進(jìn)制的方式來讀寫,還得用相應(yīng)的函數(shù)才行。
C語言提供了fread和fwrite兩個(gè)函數(shù)來實(shí)現(xiàn)二進(jìn)制的讀取和寫入。
#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。這個(gè)函數(shù)有四個(gè)參數(shù)。第一個(gè)指向存放數(shù)據(jù)的地址,第二個(gè)指定讀取的每個(gè)元素的尺寸,第三個(gè)指定準(zhǔn)備讀取的元素個(gè)數(shù),最后一個(gè)指向待讀取的文件流。
函數(shù)調(diào)用成功,會返回讀取到的元素個(gè)數(shù),如果實(shí)際讀取的比第三個(gè)參數(shù)小,那么可能會一直讀取到文件末尾或者發(fā)生錯(cuò)誤,這種情況就要通過foef和ferror來進(jìn)一步判斷。
然后是fwrite,也是有四個(gè)參數(shù)。第一個(gè)是指向存放數(shù)據(jù)的地址,第二個(gè)是指定待寫入的每個(gè)元素的尺寸,第三個(gè)是指定待寫入的元素的個(gè)數(shù),最后一個(gè)是指向待寫入的文件流。
剛剛我們介紹的,都是從文件頭開始讀寫。但是我們實(shí)際生產(chǎn)生活中,很多時(shí)候我們是需要任意修改的。比如改一個(gè)文檔,很有可能是中間的什么地方錯(cuò)了,或者是表達(dá)有不妥。那么這個(gè)時(shí)候如果你還要從頭開始去檢索,那樣效率就太低了。
于是,C語言也為我們提供了這個(gè)功能,就是隨機(jī)讀寫。
首先,我們要了解光標(biāo)的位置,才能夠更好地運(yùn)用這個(gè)功能。C語言為我們提供了ftell函數(shù),它可以告訴我們現(xiàn)在的光標(biāo)位置。
#include <stdio.h>...long ftell(FILE* stream);
如果將一個(gè)文件看成一個(gè)數(shù)組,那么這個(gè)函數(shù)返回的就是這個(gè)數(shù)組的下標(biāo)。
//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中的內(nèi)容TechZone
//Consequence 010110
如果你想將光標(biāo)快速移動到文件頭,可以用rewind函數(shù)來實(shí)現(xiàn)。
...rewind(fp);fputs('Hello', fp);fclose(fp);...
//data.txt中的內(nèi)容Helloone
可以看到,它會覆蓋我們前面的數(shù)據(jù)。
有的同學(xué)可能會說了,你這不還是沒解決問題嗎?
好的,那就來解決下問題吧。C語言給我們提供了一個(gè)函數(shù)fseek,這個(gè)函數(shù)可以直接把光標(biāo)跳轉(zhuǎn)到我們想要的位置。
#include <stdio.h>...int fseek(FILE* stream, long int offset, int whence);
第一個(gè)參數(shù)是指的我們要讀取的文件流,第二個(gè)是偏移量(往后走是正數(shù),往前走是負(fù)數(shù)),第三個(gè)是指的開始偏移的位置。
值描述SEEK_SET文件開頭SEEK_CUR當(dāng)前位置SEEK_END文件末尾
如果我要定位到第一百個(gè)字符的位置,那么:
fseek(fp, 100, SEEK_SET)
倒數(shù)第 10 個(gè)就要這樣:
fseek(fp, -10, SEEK_END)
一般C語言程序在執(zhí)行的時(shí)候,都會有 3 個(gè)面向終端的文件流,分別是「標(biāo)準(zhǔn)輸入」,「標(biāo)準(zhǔn)輸出」和「標(biāo)準(zhǔn)錯(cuò)誤輸出」。我們之前用printf的時(shí)候,其實(shí)就是在往標(biāo)準(zhǔn)輸出流中寫入字符串;用scanf的時(shí)候,其實(shí)就是函數(shù)在從標(biāo)準(zhǔn)輸入流中讀取字符串。當(dāng)然,我們寫的程序也不可能一直都是正確的,警告和報(bào)錯(cuò)的情況時(shí)有發(fā)生,這個(gè)時(shí)候其實(shí)就是對標(biāo)準(zhǔn)錯(cuò)誤輸出中寫入數(shù)據(jù)。
這三個(gè)流,我們就將它們稱為:「標(biāo)準(zhǔn)流」
C語言分別為這三個(gè)標(biāo)準(zhǔn)流提供了對應(yīng)的文件指針:stdin,stdout,stderr
比如打開文件失敗的時(shí)候,就可以這樣顯示:
... fputs('打開文件失敗!\n', stderr); exit(EXIT_FAILURE);...
這樣就不用printf這種“不專業(yè)”的錯(cuò)誤指示方法了。
打開文件失??!
每個(gè)流的內(nèi)部都有兩個(gè)指示器。一個(gè)是「文件結(jié)束指示器feof」,當(dāng)遇到文件末尾時(shí)被設(shè)置;另一個(gè)是「錯(cuò)誤指示器ferror」,當(dāng)讀寫文件出錯(cuò)時(shí)被設(shè)置。
...if (ferror(fp)){ fputs('出錯(cuò)了!\n', stderr);}...
而使用clearerr可以人為地清除兩個(gè)指示器的狀態(tài):
... clearerr(fp);...
錯(cuò)誤指示器只能判斷是否出了錯(cuò)誤,但具體是什么錯(cuò)誤,那就要看errno和perror了。
首先看errno。這個(gè)函數(shù)包含在errno.h這個(gè)頭文件中。它會返回一個(gè)錯(cuò)誤碼。
#include <errno.h>...printf('打開文件失敗:%d\n', errno);...
舉個(gè)例子:
打開文件失?。?
但是這個(gè)錯(cuò)誤代碼不是所有人都知道它的含義。所以C語言又提供了一個(gè)函數(shù)perror,它可以直接用文字來提示我們錯(cuò)誤的地方。
#include <stdio.h>...perror('打開文件失敗,原因是');...
結(jié)果是這樣的:
打開文件失敗,原因是:No such file or directory
中間的冒號是自動加上的。
或許以后在你的開發(fā)生涯中,用的最多的不是C語言,但這門語言對你帶來的提升,那是不可忽視的。最后,祝各位學(xué)有所成!
獲取完整視頻教程,可以關(guān)注B站:https://www.bilibili.com/video/BV1QE411y7v4