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