C語言之文件操做

前些時候,咱們學習的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!
順序讀寫文件

打開了文件以後,就能夠進行咱們的操做了。

讀寫單個字符

讀取單個字符,咱們能夠用fgetcgetc這兩個來實現。它們的做用,就是讀取一個字符,而後將光標移動到下一個位置。

#include <stdio.h>
...
int fgetc(FILE* stream);
int getc(FILE* stream);

函數的參數,是一個FILE結構體的指針,也就是一個準備讀取的文件流。讀取成功就會將讀取到的unsigned char內容轉化爲int並返回;文件結束或者讀取失敗就返回EOF

這倆函數不一樣的地方就在於,fgetc是函數實現,而getc是用宏實現。宏會產生大量的代碼量,可是沒有函數調用堆棧的步驟,因此速度會快不少。可是宏的展開可能會屢次調用參數,所以若是參數中含有自增、自減這種反作用的的方法,就只能用函數實現的fgetc了。

寫入單個字符,咱們能夠用fputcputc,帶有f的,就是函數,另外一個就是宏的實現的了。

#include <stdio.h>
...
int fputc(int c, FILE* stream);
int putc(int c, FILE* stream);

第一個參數是你要寫入的字符,第二個是你要寫入的文件流。

讀寫整個字符串

這裏就要用到fgetsfputs兩個函數了。

#include <stdio.h>
...
charfgets(char* s, int size, FILE* stream);
int fputs(const chat* s, FILE* stream);

其中,fgets有三個參數,第一個是一個字符型指針,用來存放讀取的數據;第二個用來指定讀取的長度(包含'\0');第三個是用於指定讀取的文件流。

函數調用成功後,會返回第一個參數所指向的地址。若是讀取到EOF則eof指示器被設置。若一開始就讀取到EOF,第一個參數的內容不變,返回NULL。若讀取發生錯誤,則error指示器被設置,函數返回NULL,第一個參數內容可能會被改變。

fputs第一個參數用於存放待寫入的數據,第二個是指定待寫入的文件流。函數調用成功,返回一個非 0 值,失敗則返回EOF

格式化讀寫文件

在文件裏,咱們就不能用咱們熟悉的scanfprintf了。可是C語言也提供一組相似的函數:fscanffprintf

用法上,第一個參數用於指定文件流,後面的就是照搬的scanfprintf中的參數。

//Example 02
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main(void)
{
    FILE* fp;
    struct  tmp;
    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語言提供了freadfwrite兩個函數來實現二進制的讀取和寫入。

#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。這個函數有四個參數。第一個指向存放數據的地址,第二個指定讀取的每一個元素的尺寸,第三個指定準備讀取的元素個數,最後一個指向待讀取的文件流。

函數調用成功,會返回讀取到的元素個數,若是實際讀取的比第三個參數小,那麼可能會一直讀取到文件末尾或者發生錯誤,這種狀況就要經過foefferror來進一步判斷。

而後是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語言分別爲這三個標準流提供了對應的文件指針:stdinstdoutstderr

好比打開文件失敗的時候,就能夠這樣顯示:

...
    fputs("打開文件失敗!\n"stderr);
 exit(EXIT_FAILURE);
...

這樣就不用printf這種「不專業」的錯誤指示方法了。

打開文件失敗!

錯誤處理

每一個流的內部都有兩個指示器。一個是「文件結束指示器feof,當遇到文件末尾時被設置;另外一個是「錯誤指示器ferror,當讀寫文件出錯時被設置。

...
if (ferror(fp))
{
    fputs("出錯了!\n"stderr);
}
...

而使用clearerr能夠人爲地清除兩個指示器的狀態:

...
    clearerr(fp);
...

錯誤指示器只能判斷是否出了錯誤,但具體是什麼錯誤,那就要看errnoperror了。

首先看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

相關文章
相關標籤/搜索