C語言探索之旅 | 第二部分第七課:文件讀寫

做者 謝恩銘,公衆號「程序員聯盟」(微信號:coderhub)。 轉載請註明出處。 原文:www.jianshu.com/p/4adb95073…程序員

《C語言探索之旅》全系列數組

內容簡介


  1. 前言
  2. 文件的打開和關閉
  3. 讀寫文件的不一樣方法
  4. 在文件中移動
  5. 文件的重命名和刪除
  6. 第二部分第八課預告

1. 前言


上一課 C語言探索之旅 | 第二部分第六課:建立你本身的變量類型 以後,咱們來學習很經常使用的文件讀寫。bash

咱們學過了這麼多變量的知識,已經知道變量實在是很強大的,能夠幫助咱們實現不少事情。微信

變量當然強大,仍是有缺陷的,最大的缺陷就是:不能永久保存app

由於 C語言的變量儲存在內存中,在你的程序退出時就被清除了,下次程序啓動時就不能找回那個值了。編輯器

「驀然回首,那人不在燈火闌珊處...」函數

「今天的你我, 怎樣重複昨天的故事? 這一張舊船票, 還可否登上你的破船?」學習

不可以啊,「濤聲不能依舊」啊...測試

若是這樣的話,咱們如何在 C語言編寫的遊戲中保存遊戲的最高分呢?怎麼用 C語言寫一個退出時依然保存文本的文本編輯器呢?ui

幸虧,在 C語言中咱們能夠讀寫文件。這些文件會儲存在咱們電腦的硬盤上,就不會在程序退出或電腦關閉時被清除了。

爲了實現文件讀寫,咱們就要用到迄今爲止咱們所學過的知識:

指針,結構體,字符串,等等。

也算是複習吧。

2. 文件的打開和關閉


爲了讀寫文件,咱們須要用到定義在 stdio.h 這個標準庫頭文件中的一些函數,結構,等。

是的,就是咱們所熟知的 stdio.h,咱們的「老朋友」 printf 和 scanf 函數也是定義在這個頭文件裏。

下面按順序列出咱們打開一個文件,進行讀或寫操做所必須遵循的一個流程:

  1. 調用「文件打開」函數 fopen(f 是 file(表示「文件」)的首字母;open 表示「打開」),返回一個指向該文件的指針。

  2. 檢測文件打開是否成功,經過第 1 步中 fopen 的返回值(文件指針)來判斷。若是指針爲 NULL,則表示打開失敗,咱們須要中止操做,而且返回一個錯誤。

  3. 若是文件打開成功(指針不爲 NULL),那麼咱們就能夠接着用 stdio.h 中的函數來讀寫文件了。

  4. 一旦咱們完成了讀寫操做,咱們就要關閉文件,用 fclose(close 表示「關閉」)函數。

首先咱們來學習如何使用 fopen 和 fclose 函數,以後咱們再學習如何讀寫文件。

fopen:打開文件


函數 fopen 的原型是這樣的:

FILE* fopen(const char* fileName, const char* openMode);
複製代碼

不難看出,這個函數接收兩個參數:

  • fileName:文件名(name 表示「名字」)。是一個字符串類型,並且是 const,意味着不能改變其值。

  • openMode:打開方式(open 表示「打開」,mode 表示「方式」)。代表咱們打開文件以後要幹什麼的一個指標。只讀、只寫、讀寫,等等。

這個函數的返回值,是 FILE *,也就是一個 FILE(file 表示「文件」)指針。

FILE 定義在 stdio.h 中。有興趣的讀者能夠本身去找一下 FILE 的定義。

咱們給出 FILE 的通常定義:

typedef struct {
    char *fpos; /* Current position of file pointer (absolute address) */
    void *base; /* Pointer to the base of the file */
    unsigned short handle; /* File handle */
    short flags; /* Flags (see FileFlags) */
    short unget; /* 1-byte buffer for ungetc (b15=1 if non-empty) */
    unsigned long alloc; /* Number of currently allocated bytes for the file */
    unsigned short buffincrement; /* Number of bytes allocated at once */
} FILE;
複製代碼

能夠看到 FILE 是一個結構體(struct),裏面有 7 個變量。固然咱們沒必要深究 FILE 的定義,只要會使用 FILE 就行了,並且不一樣操做系統對於 FILE 的定義不盡相同。

細心的讀者也許會問:「以前不是說結構體的名稱最好是首字母大寫麼,爲何 FILE 這個結構體每個字母都是大寫呢?怎麼和常量的命名方式同樣呢?」

好問題。其實咱們以前建議的命名方式(對於結構體,首字母大寫,例如:StructName)只是一個「規範」(雖然大多數程序員都喜歡遵循),並非一個強制要求。

這隻能說明編寫 stdio.h 的前輩並不必定遵循這個「規範」而已。固然,這對咱們並沒什麼影響。

如下列出幾種可供使用的 openMode :

  • r :只讀。r 是 read(表示「讀」)的首字母。這個模式下,咱們只能讀文件,而不能對文件寫入。文件必須已經存在。

  • w :只寫。w 是 write(表示「寫」)的首字母。這個模式下,只能寫入,不能讀出文件的內容。若是文件不存在,將會被建立。

  • a :追加。a 是 append(表示「追加」)的首字母。這個模式下,從文件的末尾開始寫入。若是文件不存在,將會被建立。

  • r+ :讀和寫。這個模式下,能夠讀和寫文件,但文件也必須已經存在。

  • w+ :讀和寫。預先會刪除文件內容。這個模式下,若是文件存在且內容不爲空,則內容首先會被清空。若是文件不存在,將會被建立。

  • a+ :讀寫追加。這個模式下,讀寫文件都是從文件末尾開始。若是文件不存在,將會被建立。

上面所列的模式,其實還能夠組合上 b 這個模式。b 是 binary 的縮寫,表示「二進制」。 對於上面的每個模式,若是你添加 b 後,會變成 rbwbabrb+wb+ab+ ),該文件就會以二進制模式打開。不過二進制的模式通常不是那麼經常使用。

通常來講,rwr+ 用得比較多。w+ 模式要慎用,由於它會首先清空文件內容。當你須要往文件中添加內容時,a 模式會頗有用。

下面的例子程序就以 r+(讀寫)的模式打開文件:

#include <stdio.h>

int main(int argc, char *argv[])
{
    FILE* file = NULL;

    file = fopen("test.txt", "r+");

    return 0;
}
複製代碼

因而,file 成爲了指向 test.txt 文件的一個指針。

你會問:「咱們的 test.txt 文件位於哪裏呢?」

text.txt 文件和可執行文件位於同一目錄下。

「文件必定要是 .txt 結尾的嗎?」

不是,徹底由你決定文件的後綴名。你大能夠建立一個文件叫作 xxx.level,用於記錄遊戲的關卡信息。

「文件必定要和可執行文件在同一個文件夾下麼?」

也不是。理論上能夠位於當前系統的任意文件夾裏,只要在 fopen 函數的文件名參數裏指定文件的路徑就行了,例如:

file = fopen("folder/test.txt", "w");
複製代碼

這樣,文件 test.txt 就是位於當前目錄的文件夾 folder 裏。這裏的 folder/test.txt 稱爲「相對路徑」。

咱們也能夠這樣:

file = fopen("/home/user/folder/test.txt", "w");
複製代碼

這裏的 /home/user/folder/test.txt 是「絕對路徑」。

測試打開文件


在調用 fopen 函數嘗試打開文件後,咱們須要檢測 fopen 的返回值,以判斷打開是否成功。

檢測方法也很簡單:若是 fopen 的返回值爲 NULL,那麼打開失敗;若是不爲 NULL,那麼表示打開成功。示例以下:

#include <stdio.h>

int main(int argc, char *argv[])
{
    FILE* file = NULL;

    file = fopen("test.txt", "r+");

    if (file != NULL)
    {
        // 讀寫文件
    }
    else
    {
        // 顯示一個錯誤提示信息
        printf("沒法打開 test.txt 文件\n");
    }

    return 0;
}
複製代碼

記得每次使用 fopen 函數時都要對返回值做判斷,由於若是文件不存在或者正被其餘程序佔用,那可能會使當前程序運行失敗。

fclose:關閉文件


close 表示「關閉」。

若是咱們成功地打開了一個文件,那麼咱們就能夠對文件進行讀寫了(讀寫的操做咱們下一節再詳述)。

若是咱們對文件的操做已經結束,那麼咱們應該關閉這個文件,這樣作是爲了釋放佔用的文件指針。

咱們須要調用 fclose 函數來實現文件的關閉,這個函數能夠釋放內存,也就是從內存中刪除你的文件(指針)。

函數原型:

int fclose(FILE* pointerOnFile);
複製代碼

這個函數只有一個參數:指向文件的指針。

函數的返回值(int)有兩種狀況:

  • 0 :當關閉操做成功時。
  • EOF(是 End Of File 的縮寫,表示「文件結束」。通常等於 -1):若是關閉失敗。

示例以下:

#include <stdio.h>

int main(int argc, char *argv[])
{
    FILE* file = NULL;

    file = fopen("test.txt", "r+");

    if (file != NULL)
    {
        // 讀寫文件

        // ...

        fclose(file);  // 關閉咱們以前打開的文件
    }

    return 0;
}
複製代碼

3. 讀寫文件的不一樣方法


如今,咱們既然已經知道怎麼打開和關閉文件了,接下來咱們就學習如何對文件進行讀出和寫入吧。

咱們首先學習如何寫入文件(相比讀出要簡單一些),以後咱們再看如何從文件讀出。

對文件寫入

用於寫入文件的函數有好幾個,咱們能夠根據狀況選擇最適合的函數來使用。

咱們來學習三個用於文件寫入的函數:

  • fputc:在文件中寫入一個字符(一次只寫一個)。是 file put character 的縮寫。put 表示「放入」,character 表示「字符」。

  • fputs:在文件中寫入一個字符串。是 file put string 的縮寫。string 表示「字符串」。

  • fprintf:在文件中寫入一個格式化過的字符串,用法與 printf 是幾乎相同的,只是多了一個文件指針。

fputc

此函數用於在文件中一次寫入一個字符。

函數原型:

int fputc(int character, FILE* pointerOnFile);
複製代碼

這個函數包含兩個參數:

  • character:int 型變量,表示要寫入的字符。咱們也能夠直接寫 'A' 這樣的形式,以前 ASCII 那節的知識點沒有忘吧。

  • pointerOnFile:指向文件的指針。

函數返回 int 值。若是寫入失敗,則爲 EOF;不然,會是另外一個值。

示例:

#include <stdio.h>

int main(int argc, char *argv[])
{
    FILE* file = NULL;

    file = fopen("test.txt", "w");

    if (file != NULL)
    {
        fputc('A', file);  // 寫入字符 A
        fclose(file);
    }

    return 0;
}
複製代碼

上面的程序用於向 test.txt 文件寫入字符 'A'。

fputs

這個函數和 fputc 相似,區別是 fputc 每次是寫入一個字符,而 fputs 每次寫入一個字符串。

函數原型:

int fputs(const char* string, FILE* pointerOnFile);
複製代碼

相似地,這個函數也接受兩個參數:

  • string:要寫入的字符串。

  • pointerOnFile:指向文件的指針。

若是出錯,函數返回 EOF;不然,返回不一樣於 EOF 的值。

示例:

#include <stdio.h>

int main(int argc, char *argv[])
{
    FILE* file = NULL;

    file = fopen("test.txt", "w");

    if (file != NULL)
    {
        fputs("你好,朋友。\n最近怎麼樣?", file);
        fclose(file);
    }

    return 0;
}
複製代碼
fprintf

這個函數頗有用,由於它不只能夠向文件寫入字符串,並且這個字符串是能夠由咱們來格式化的。用法其實和 printf 函數相似,就是多了一個文件指針。

函數原型:

int fprintf(FILE *stream, const char *format, ...)
複製代碼

示例:

#include <stdio.h>

int main(int argc, char *argv[])
{
    FILE* file = NULL;
    int age = 0;

    file = fopen("test.txt", "w");

    if (file != NULL)
    {
        // 詢問用戶的年齡
        printf("您幾歲了 ? ");
        scanf("%d", &age);

        // 寫入文件
        fprintf(file, "使用者年齡是 %d 歲\n", age);
        fclose(file);
    }

    return 0;
}
複製代碼

從文件中讀出


咱們能夠用與寫入文件時相似名字的函數,只是略微修改了一些,也有三個:

  • fgetc:讀出一個字符。是file get character 的縮寫。get 表示「獲取,取得」。

  • fgets:讀出一個字符串。是 file get string 的縮寫。

  • fscanf:與 scanf 的用法相似,只是多了一個文件指針。scanf 是從用戶輸入讀取,而 fscanf 是從文件讀取。

此次介紹這三個函數咱們會簡略一些,由於若是你們掌握好了前面那三個寫入的函數,那這三個讀出的函數是相似的。只是操做相反了。

fgetc

首先給出函數原型:

int fgetc(FILE* pointerOnFile);
複製代碼

函數返回值是讀到的字符。若是不能讀到字符,那會返回 EOF。

可是如何知道咱們從文件的哪一個位置讀取呢?是第三個字符處,仍是第十個字符處呢?

其實,在咱們讀取文件時,有一個「遊標」(cursor),會跟隨移動。

這固然是虛擬的遊標,你不會在屏幕上看到它。你能夠想象這個遊標和你用記事本編輯文件時的閃動的光標相似。這個遊標指示你當前在文件中的位置。

以後的小節,咱們會學習如何移動這個遊標,使其位於文件中特定的位置。能夠是開頭,也能夠是第 7 個字符處。

fgetc 函數每讀入一個字符,這個遊標就移動一個字符長度。咱們就能夠用一個循環來讀出文件全部的字符。例如:

#include <stdio.h>

int main(int argc, char *argv[])
{
    FILE* file = NULL;
    int currentCharacter = 0;

    file = fopen("test.txt", "r");

    if (file != NULL)
    {
        // 循環讀取,每次一個字符
        do
        {
            currentCharacter = fgetc(file);  // 讀取一個字符
            printf("%c", currentCharacter);  // 顯示讀取到的字符
        } while (currentCharacter != EOF);  // 咱們繼續,直到 fgetc 返回 EOF(表示「文件結束」)爲止

        fclose(file);
    }

    return 0;
}
複製代碼
fgets

此函數每次讀出一個字符串,這樣能夠沒必要每次讀一個字符(有時候效率過低)。

這個函數每次最多讀取一行,由於它遇到第一個 '\n'(換行符)會結束讀取。因此若是咱們想要讀取多行,須要用循環。

插入一點回車符和換行符的知識: 關於「回車」(carriage return)和「換行」(line feed)這兩個概念的來歷和區別。 在計算機尚未出現以前,有一種叫作電傳打字機(Teletype Model 33)的玩意,每秒鐘能夠打 10 個字符。 可是它有一個問題,就是打完一行換行的時候,要用去 0.2 秒,正好能夠打兩個字符。要是在這 0.2 秒裏面,又有新的字符傳過來,那麼這個字符將丟失。 因而,研製人員想了個辦法解決這個問題,就是在每行後面加兩個表示結束的字符。一個叫作「回車」,告訴打字機把打印頭定位在左邊界;另外一個叫作「換行」,告訴打字機把紙向下移一行。這就是「換行」和「回車」的來歷,從它們的英語名字上也能夠看出一二。 後來,計算機被髮明瞭,這兩個概念也就被搬到了計算機上。那時,存儲器很貴,一些科學家認爲在每行結尾加兩個字符太浪費了,加一個就能夠。因而,就出現了分歧。在 Unix/Linux 系統裏,每行結尾只有「<換行>」,即 "\n";在 Windows 系統裏面,每行結尾是「<換行><回車>」,即 "\n\r";在 macOS 系統裏,每行結尾是「<回車>」,即 "\r"。 一個直接後果是,Unix/Linux/macOS 系統下的文件在Windows裏打開的話,全部文字會變成一行;而 Windows 裏的文件在 Unix/Linux/macOS 下打開的話,在每行的結尾可能會多出一個 ^M 符號。 Linux 中遇到換行符會進行「回車 + 換行」的操做,回車符反而只會做爲控制字符顯示,不發生回車的操做。 而 Windows 中要「回車符 + 換行符」纔會實現「回車+換行",缺乏一個控制符或者順序不對都不能正確的另起一行。

函數原型:

char* fgets(char* string, int characterNumberToRead, FILE* pointerOnFile);
複製代碼

示例:

#include <stdio.h>

#define MAX_SIZE 1000 // 數組的最大尺寸 1000

int main(int argc, char *argv[])
{
    FILE* file = NULL;
    char string[MAX_SIZE] = "";  // 尺寸爲 MAX_SIZE 的數組,初始爲空

    file = fopen("test.txt", "r");

    if (file != NULL)
    {
        fgets(string, MAX_SIZE, file);  // 咱們讀取最多 MAX_SIZE 個字符的字符串,將其存儲在 string 中
        printf("%s\n", string);  // 顯示字符串

        fclose(file);
    }

    return 0;
}
複製代碼

這裏,咱們的 MAX_SIZE 足夠大(1000),保證能夠容納下一行的字符數。因此遇到 '\n' 咱們就中止讀取,所以以上代碼的做用就是讀取文件中的一行字符,並將其輸出。

那咱們如何可以讀取整個文件的內容呢?很簡單,加一個循環。

以下:

#include <stdio.h>

#define MAX_SIZE 1000 // 數組的最大尺寸 1000

int main(int argc, char *argv[])
{
    FILE* file = NULL;
    char string[MAX_SIZE] = "";  // 尺寸爲 MAX_SIZE 的數組,初始爲空

    file = fopen("test.txt", "r");

    if (file != NULL)
    {
        while (fgets(string, MAX_SIZE, file) != NULL)  // 咱們一行一行地讀取文件內容,只要不遇到文件結尾
        printf("%s\n", string);  // 顯示字符串

        fclose(file);
    }

    return 0;
}
複製代碼
fscanf

此函數的原理和 scanf 是同樣的。負責從文件中讀取規定樣式的內容。

函數原型:

int fscanf(FILE *stream, const char *format, ...)
複製代碼

示例:

例如咱們建立一個 test.txt 文件,在裏面輸入三個數:23, 45, 67。

輸入的形式能夠是相似下面這樣:

  • 每一個數之間有空格

  • 每一個數之間換一行

#include <stdio.h>

int main(int argc, char *argv[])
{
    FILE* file = NULL;
    int score[3] = {0};  // 包含 3 個最佳得分的數組

    file = fopen("test.txt", "r");

    if (file != NULL)
    {
        fscanf(file, "%d %d %d", &score[0], &score[1], &score[2]);
        printf("最佳得分是 : %d, %d 和 %d\n", score[0], score[1], score[2]);

        fclose(file);
    }

    return 0;
}
複製代碼

運行輸出:

最佳得分是:23, 45, 67
複製代碼

4. 在文件中移動


前面咱們提到了虛擬的「遊標」,如今咱們仔細地來學習一下。

每當咱們打開一個文件的時候,實際上都存在一個「遊標」,標識你當前在文件中所處的位置。

你能夠類比咱們的文本編輯器,每次你在文本編輯器(例如記事本)裏面輸入文字的時候,不是有一個遊標(光標)能夠處處移動麼?它指示了你在文件中的位置,也就是你下一次輸入會從哪裏開始。

總結來講,遊標系統使得咱們能夠在文件中指定位置進行讀寫操做。

咱們介紹三個與文件中游標移動有關的函數:

  • ftell:告知目前在文件中哪一個位置。tell 表示「告訴」。

  • fseek:移動文件中的遊標到指定位置。seek 表示「探尋」。

  • rewind:將遊標重置到文件的開始位置(這和用 fseek 函數來使遊標回到文件開始位置是一個效果)。rewind 表示「轉回」。

ftell:指示目前在文件中的遊標位置


這個函數使用起來很是簡單,它返回一個 long 型的整數值,標明目前遊標所在位置。函數原型是:

long ftell(FILE* pointerOnFile);
複製代碼

其中,pointerOnFile 這個指針就是文件指針,指向當前文件。

相信沒必要用例子就知道如何使用了吧。

fseek:使遊標移動到指定位置


函數原型爲:

int fseek(FILE* pointerOnFile, long move, int origin);
複製代碼

此函數能使遊標在文件(pointerOnFile 指針所指)中從位置(origin 所指。origin 表示「初始」)開始移動必定距離(move 所指。move 表示「移動」)。

  • move 參數:能夠是一個正整數,代表向前移動;0,代表不移動;或者負整數,代表回退。

  • origin 參數:它的取值能夠是如下三個值(#define 所定義的常量)中的任意:

    • SEEK_SET :文件開始處。SET 表示「設置」。
    • SEEK_CUR :遊標當前所在位置。CUR 是 current(表示「當前」)的縮寫。
    • SEEK_END :文件末尾。END 表示「結尾」。

來看幾個具體使用實例吧:

// 這行代碼將遊標放置到距離文件開始處 5 個位置的地方
fseek(file, 5, SEEK_SET);

// 這行代碼將遊標放置到距離當前位置日後 3 個位置的地方
fseek(file, -3, SEEK_CUR);

// 這行代碼將遊標放置到文件末尾
fseek(file, 0, SEEK_END);
複製代碼

rewind:使遊標回到文件開始位置


這個函數的做用就至關於使用 fseek 來使遊標回到 0 的位置

void rewind(FILE* pointerOnFile);
複製代碼

相信使用難不倒你們吧,看函數原型就一目瞭然了。和 fseek(file, 0, SEEK_SET); 是一個效果。

5. 文件的重命名和刪除


咱們來學習兩個簡單的函數,以結束此次的課程:

  • rename 函數:重命名一個文件(rename 表示「重命名」)。

  • remove 函數:刪除一個文件(remove 表示「移除」)。

這兩個函數的特殊之處就在於,不一樣於以前的一些文件操做函數,它們不須要文件指針做爲參數,只須要把文件的名字傳給這兩個函數就夠了。

rename:重命名文件


函數原型:

int rename(const char* oldName, const char* newName);
複製代碼

oldName 就是文件的「舊名字」,而 newName 是文件的「新名字」。

若是函數執行成功,則返回 0;不然,返回非零的 int 型值。

如下是一個使用的例子:

int main(int argc, char *argv[])
{
      rename("test.txt", "renamed_test.txt");

      return 0;
}
複製代碼

很簡單吧。

remove:刪除一個文件


函數原型:

int remove(const char* fileToRemove);
複製代碼

fileToRemove 就是要刪除的文件名。

注意:remove 函數要慎用,由於它不會提示你是否確認刪除文件。 文件是直接從硬盤被永久刪除了,也不會先移動至垃圾箱。 想要再找回被刪除的文件就只能藉助一些特殊的軟件了,可是恢復過程可能沒那麼容易,也不必定可以成功。

實例:

int main(int argc, char *argv[])
{
    remove("test.txt");

    return 0;
}
複製代碼

6. 第二部分第八課預告


今天的課就到這裏,一塊兒加油吧!

下一課:C語言探索之旅 | 第二部分第八課:動態分配


我是 謝恩銘,公衆號「程序員聯盟」(微信號:coderhub)運營者,慕課網精英講師 Oscar 老師,終生學習者。 熱愛生活,喜歡游泳,略懂烹飪。 人生格言:「向着標杆直跑」

相關文章
相關標籤/搜索