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

1240

-- 簡書作者 謝恩銘 轉載請註明出處

第二部分第七課:文件讀寫


上一課C語言探索之旅 | 第二部分第六課:創建你自己的變量類型之後,我們來學習很常用的文件讀寫。

我們學過了這麼多變量的知識,已經知道變量實在是很強大的,可以幫助我們實現很多事情。

變量固然強大,還是有缺陷的,最大的缺陷就是:不能永久保存。

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

“驀然回首,那人不在燈火闌珊處...”

“今天的你我,
怎樣重複昨天的故事?
這一張舊船票,
還能否登上你的破船?”

不能夠啊,"濤聲不能依舊"啊。

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

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

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

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

也算是複習吧。

文件的打開和關閉


爲了讀寫文件,我們需要用到定義在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:文件名。是一個字符串類型,而且是const,意味着不能改變其值。

  • openMode:打開方式。表明我們打開文件之後要幹什麼的一個指標。只讀、只寫、讀寫。

這個函數的返回值,是 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是一個結構體,裏面有7個變量。當然此處我們不必深究FILE的定義,我們只要會使用FILE就好了,而且不同操作系統對於FILE的定義不盡相同。

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

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

這隻能說明編寫stdio.h的前輩並不一定遵循這個“規範”而已。當然,這對我們並沒什麼影響。

所以小編你就是講廢話咯,好吧,好吧...

以下列出幾種可供使用的openMode(打開方式):

  • r:只讀。r是英語“read”(讀)的首字母。這個模式下,我們只能讀文件,而不能對文件寫入。文件必須已經存在。

  • w:只寫。w是英語“write”(寫)的首字母。這個模式下,只能寫入,不能讀出文件的內容。如果文件不存在,將會被創建。

  • a:追加。a是英語“append”(追加)的首字母。這個模式下,從文件的末尾開始寫入。如果文件不存在,將會被創建。

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

  • w+:讀,預先會刪除文件內容。這個模式下,文件的內容首先會被清空。

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

一般來說,“r”,“w”和“r+”用得比較多,“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(一般是-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;
}

讀寫文件的不同方法


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

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

對文件寫入

用於寫入文件的函數有好幾個,我們可以根據情況選擇最適合的函數來使用。

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

  • fputc:在文件中寫入一個字符(一次只寫一個)。是file put (put是英語“放入”的意思) character(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(get是英語“取得”的意思) character的縮寫。

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

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

這次介紹這三個函數我們會簡略一些,因爲如果大家掌握好了前面那三個寫入的函數,那這三個讀出的函數是類似的。只是操作相反了。

fgetc


首先給出函數原型:

int fgetc(FILE* pointerOnFile);

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

但是如何知道我們從文件的哪個位置讀取呢?是第三個字符處,還是第十個字符處呢?

其實,在我們讀取文件時,有一個“遊標”,會跟隨移動。

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

1240

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

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”;Mac系統裏,每行結尾是“<回車>”。
一個直接後果是,Unix/Linux/Mac系統下的文件在Windows裏打開的話,所有文字會變成一行;而Windows裏的文件在Unix/Linux/Mac下打開的話,在每行的結尾可能會多出一個^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

輸入的形式可以是類似下面這樣:

  • 每個數之間有空格
1240
  • 每個數之間換一行
1240
#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

在文件中移動


前面我們提到一個虛擬的“遊標”,現在我們仔細地來學習一下。

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

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

總結來說,遊標系統使得我們可以在文件中指定位置進行讀寫操作。

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

  • 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是英語“結尾”的意思。

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

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

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

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

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


這個函數的作用就相當於使用fseek來使遊標回到0的位置

void rewind(FILE* pointerOnFile);

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

文件的重命名和刪除


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

  • 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;
}

第二部分第八課預告:


今天的課就到這裏,一起加油吧。

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章