前些時候,咱們學習的C語言程序都是由輸入輸出和算法組成的控制檯程序。咱們在終端上來輸入咱們提供的數據,而後程序也會經過終端來告訴咱們最終運行的結果。算法
可是,可能有的同窗已經觀察到了,咱們平常使用的別人開發的程序,大多數都是經過文件來提供數據的。好比一個Excel的報表,程序能夠直接來分析裏面的數據。再好比,一個TXT格式的電子書,程序能夠直接分析有多少字、多少個章節,甚至還能夠生成出一個目錄來。數組
擁有這樣能力的程序,是否是感受功能強大了許多?這就要用到咱們今天要講到的內容——「文件操做」。緩存
關於文件在咱們比較熟悉的Windows系統下,文件類型的區分是用「擴展名」來進行的。但其實擴展名並非指「文件格式」,它只是一個「門牌號」而已。至於它到底對不對,那系統就不知道了。可能有不少的新手,在遇到格式的問題的時候,會認爲直接更改擴展名,就能實現格式轉換。不瞞大家說,我小時候也有過這種想法。可是後來發現,不行。舉個例子,如今有一個 MP3 的文件,要轉成 AAC。這兩個文件從編碼上來說,就是不同的。MP3 只能用 MP3 的方式去讀取,AAC 只能用 AAC 的方式去讀取。若是你把擴展名直接改爲 AAC,那麼系統就被你騙了,就會用 AAC 的方式去讀取實際仍是 MP3 的文件,固然是不行了。app
不一樣的擴展名,就對應了不一樣的讀取方式。「EXE」 就表明 Windows 系統下的可執行二進制文件,「TXT」是純文本文件,等等。ide
在 Linux 和 Unix 操做系統下,文件的定義就寬泛多了。不光軟件,硬件也能夠叫文件。也就是說,硬件實際上也是當作文件的方式來處理的。函數
在C語言中,文件通常分爲兩種,一種是二進制文件,就是咱們編譯出來的那個東西,咱們是看不懂的;另外一種是文本文件,也就是咱們常說的源代碼。學習
打開和關閉文件咱們要對一個文件進行操做,首先咱們須要把文件打開,而後才能讀或者寫。對文件操做完成後,咱們還要將文件關閉。ui
C語言中的打開文件使用fopen
函數,通式以下:編碼
fopen("文件路徑", "模式")
url
若是打開文件成功,則會返回一個FILE結構的指針,經過這個指針,咱們就能夠對這個文件進行操做;若是打開文件失敗,則會返回NULL。
下面是全部的模式:
模式 | 功能 |
---|---|
"r" | 以只讀的形式打開文件,並從頭開始讀取 文件必須存在 |
"w" | 以只寫的形式打開文件,從頭開始寫入 若文件不存在,則建立一個文件 若文件存在,則所有被覆蓋 |
"a" | 以追加的形式打開文件,從文件末尾追加內容 若文件不存在,則建立一個新的文件 |
"r+" | 以讀寫的形式打開文件,從頭開始讀寫 文件必須存在,若本來有內容,則寫入的部分被覆蓋 |
"w+" | 以讀寫的形式打開文件,從頭開始讀寫 若文件不存在,則被建立 若文件存在,則被所有覆蓋 |
"a+" | 以讀取和追加的形式打開文件 若文件不存在,則建立一個新的文件 讀取是從頭開始,追加是從末尾開始 |
"b" | 代表打開的是二進制文件,使用時與上面的任意一個疊加 如:"wb", "r+b" |
前面幾個都好理解,只是最後一個,爲啥要區分一個二進制出來呢?
不加「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語言的文章自此就告一段落,之後還會寫一些進階的內容,但不會連續發佈了。若是你有什麼好的題材或者是問題,均可以私信提供給我,我會考慮把它們寫進文章的。最後,祝各位學有所成!
來自公衆號:TechZone