C語言之文件操作

2021-02-23 C語言與C++編程

前些時候,我們學習的C語言程序都是由輸入輸出和算法組成的控制臺程序。我們在終端上來輸入我們提供的數據,然後程序也會通過終端來告訴我們最終運行的結果。

但是,可能有的同學已經觀察到了,我們日常使用的別人開發的程序,大多數都是通過文件來提供數據的。比如一個Excel的報表,程序可以直接來分析裡面的數據。再比如,一個TXT格式的電子書,程序可以直接分析有多少字、多少個章節,甚至還可以生成出一個目錄來。

擁有這樣能力的程序,是不是感覺功能強大了許多?這就要用到我們今天要講到的內容——「文件操作」

關於文件

在我們比較熟悉的Windows系統下,文件類型的區分是用「擴展名」來進行的。但其實擴展名並不是指「文件格式」,它只是一個「門牌號」而已。至於它到底對不對,那系統就不知道了。可能有很多的新手,在遇到格式的問題的時候,會認為直接更改擴展名,就能實現格式轉換。不瞞你們說,我小時候也有過這種想法。但是後來發現,不行。舉個例子,現在有一個 MP3 的文件,要轉成 AAC。這兩個文件從編碼上來講,就是不一樣的。MP3 只能用 MP3 的方式去讀取,AAC 只能用 AAC 的方式去讀取。如果你把擴展名直接改成 AAC,那麼系統就被你騙了,就會用 AAC 的方式去讀取實際還是 MP3 的文件,當然是不行了。

不同的擴展名,就對應了不同的讀取方式。「EXE」 就代表 Windows 系統下的可執行二進位文件,「TXT」是純文本文件,等等。

在 Linux 和 Unix 作業系統下,文件的定義就寬泛多了。不光軟體,硬體也可以叫文件。也就是說,硬體實際上也是當做文件的方式來處理的。

在C語言中,文件一般分為兩種,一種是二進位文件,就是我們編譯出來的那個東西,我們是看不懂的;另一種是文本文件,也就是我們常說的原始碼。

打開和關閉文件

我們要對一個文件進行操作,首先我們需要把文件打開,然後才能讀或者寫。對文件操作完成後,我們還要將文件關閉。

C語言中的打開文件使用fopen函數,通式如下:

fopen("文件路徑", "模式")

如果打開文件成功,則會返回一個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語言的文章自此就告一段落,以後還會寫一些進階的內容,但不會連續發布了。如果你有什麼好的題材或者是問題,都可以私信提供給我,我會考慮把它們寫進文章的。最後,祝各位學有所成!

●編號767,輸入編號直達本文

●輸入m獲取文章目錄

分享C/C++技術文章

相關焦點

  • C語言——文件的基本操作
    C語言基礎合集,點我點我~~~C語言進階合集,點我點我~~~C語言高級:01.C語言實現字符串的加密和解密02.C語言數據結構——鍊表C語言中文件的基本操作包括:文件的打開、文件的關閉以及文件的輸入和輸出。
  • 乾貨|C語言文件的基本操作!
    本文轉載自【微信公眾號:手機電腦雙黑客,ID:heikestudio】經微信公眾號授權轉載,如需轉載與原文作者聯繫c語言對文件的操作主要分為:按字符操作,按行操作,按內存塊操作主要的函數:fopen():FILE * fopen(_In_z_ const char * _Filename, _In_z_ const char
  • c語言文件操作整理
    推薦《c陷阱與缺陷》FILE *fp;fp = fopen(file, "r+");編程者也許認為,程序一旦執行上述操作完畢,就可以自由地進行讀取和寫入的操作了。遺憾的是,事實總難遂人所願,為了保持與過去不能同時進行讀寫操作的程序的向下兼容性,一個輸入操作不能隨後直接緊跟輸出操作,反之亦然。
  • C語言操作EXCEL文件(讀寫)
    C語言操作EXCEL文件(讀寫)本文主要介紹通過純C語言進行EXCEL的讀寫操作:(如果運行結果均是0,請看文章最後一節)在之前需要使用
  • C語言文件操作詳解
    文件操作標準庫函數有:文件的打開操作 fopen 打開一個文件文件的關閉操作 fclose 關閉一個文件文件的讀寫操作 fgetc 從文件中讀取一個字符fputc 寫一個字符到文件中去fgets 從文件中讀取一個字符串fputs 寫一個字符串到文件中去fprintf 往文件中寫格式化數據fscanf
  • C語言文件操作
    C 語言把文件看作是一個字符(字節)的序列,即由一個一個字符(字節)的數據順序組成。根據數據的組織形式,可分為 ASCⅡ 文件和二進位文件。文件的操作包括:文件的打開、文件的關閉、文件的讀寫操作、文件狀態檢查以及文件的定位等。 1 文件的打開 1.1 函數原型 1.2 功能說明 按照 mode 規定的方式,打開由 pname指定的文件。
  • 文件操作的正確流程,C語言文件操作的函數
    引言操作文件的正確操作流程為:打開文件—>讀寫文件—>關閉文件在對文件進行讀寫操作之前,需要先打開文件,操作完成之後就要關閉文件!所謂的打開文件,就是需要獲取文件的信息,例如文件名、文件狀態以及文件位置;而對於文件的操作,就是對文件的讀(read)與寫(write),C語言對於文件的操作十分的靈活;同時在對文件完成操作之後,就需要關閉文件,不僅是為了禁止對文件的操作,同時也是為釋放儲存文件指針FILE的內存空間資源。
  • 一文搞懂C語言對文件的操作 | 經典
    10張講了有關文件的知識,以往我們寫的C程序的數據都是由鍵盤輸入的現在我們要對文件進行操作。它們分別是源程序文件.c目標文件.oASCII文件我們在剛剛接觸c語言時就了解了ASCII標,每個特定的數代表一個字符,那麼將字符形式的文件就是ASCII文件,也稱為文本文件,每個字節存放一個字符的ASCII值。
  • 現代程式語言起點,C語言之環境搭建
    但也就是因為它有指針,可以直接進行靠近硬體的操作,所以帶來很多不安全的因素二、C語言的環境搭建1、作業系統說明:推薦使用Unix系統,比如類Unix系統的Linux系統中的CentOs、Ubantu系統,或者使用Mac。
  • C語言中常用的幾個頭文件及庫函數
    來源:https://www.jb51.net/article/124594.htm這篇文章主要介紹了C語言中常用的幾個頭文件及庫函數的相關資料
  • 【C語言】02.第一個C語言程序
    不過呢,開發工具屏蔽了很多操作細節和語法細節,不利於初學者直觀、系統地學習一門語言。因此,在這裡,我們暫時使用文本編輯工具UltraEdit來寫C語言代碼。 2.寫代碼1> C程序由函數構成寫代碼之前,你首先要知道:任何一個C語言程序都是由一個或者多個程序段(小程序)構成的,每個程序段都有自己的功能,我們一般稱這些程序段為「函數」。
  • C語言項目中.h和.c文件的關係和概念
    在編譯器只認識.c(.cpp))文件,而不知道.h是何物的年代,那時的人們寫了很多的.c(.cpp)文件,漸漸地,人們發現在很多.c(.cpp)文件中的聲明語句就是相同的。
  • C語言的頭文件包含竟然有那麼多講究!
    曾以為自己寫C語言已經輕車熟路了,特別是對軟體文件的工程管理上,因為心裡對自己的代碼編寫風格還是有自信的。(畢竟剛畢業時老大對我最初的訓練就是編碼格式的規範化處理)曾以為,一個.c文件對應一個.h文件,.c文件只包含它自身的.h文件就好,若.c文件中用到其他文件中的內容,則.h文件把用到的頭文件包含進來就可以了。
  • C語言的那些小秘密之預處理
    本文引用地址:http://www.eepw.com.cn/article/275697.htm  C語言的預處理主要有三個方面:  1、文件的包含  2、宏定義  3、條件編譯  一、文件包含的形式有下面兩種  1、#include "文件名"  2
  • c語言入門之安裝code::blocks
    C語言是一門面向過程的、抽象化的通用程序設計語言,廣泛應用於底層開發。
  • C語言頭文件被include後都發生了什麼?為何不能在頭文件定義變量
    C語言的#include語法頭文件通常與C語言的#include 語法配合使用,意為「將頭文件內容包含進來」,例如在 t.c 文件裡寫下這段C語言代碼:C語言代碼編譯器在編譯這段C語言代碼之前,會有一個「預處理」的過程,在此過程中,stdio.h 裡的內容被展開到 t.c 文件裡。
  • C語言之 static
    然後是局部靜態變量,「局部」說明這個變量只能在本函數被使用,出了函數範圍內我是不管用的,另外,即使這個函數調用了其他函數,而其他函數也要用這個變量,不好意思,我的手沒那麼長,夠不著,我就守著我這巴掌大的地方,所以說其他函數也不能使用這個變量
  • C語言晉級--ANSI C文件管理
    ANSI 的 C 的標準庫封裝了文件的系統調用,為了提高文件打開的效率還加入了緩衝機制,提供記錄的形式讀寫文件,並且有很好的移植性,是linux C語言最基礎的文件編程。一、文件指針和六流文件:永久儲存的、有特定順序的、有名稱的字節組成的集合。在Linux 系統中通常能見到的目錄、設備文件、管道等都屬於文件。
  • 物聯網應用開發之C語言介紹
    當前階段,在編程領域中,C語言的運用非常之多,它兼顧了高級語言的彙編語言的優點,相較於其它程式語言具有較大優勢。計算機系統設計以及應用程式編寫是C語言應用的兩大領域。同時,C語言的普適較強,在許多計算機作業系統中都能夠得到適用,且效率顯著。
  • 第六篇:C語言中結構體與文件操作相關知識點梳理
    前面總結過,C語言中的基本數據類型有四種,分別是整型、浮點型和字符型;後面又講到可以保存字符串的字符數組。但這遠遠不夠實際應用的需要。在C語言中結構體是對數據類型的無限擴展。程式設計師可以根據需要定義各種各樣的數據類型,即:結構體。問題二:數據無法永久保存前面我們編寫運行的所有C語言程序,或多或少都會輸入一些數據。