【C 高階】盤點標準 C 庫文件操作函數

1. C 庫 API 的特點

在標準 C 語言庫中有提供一套完整的文件操作 API,如 fopen()、fgets()、fread() 等,使用這些 API 可以對指定文件進行讀寫操作。

C 庫 API 具備以下特徵:

  • 高兼容

有別於系統調用 API(如 Linux 下的 open()、read()、write() 等),C 庫 API 是支持跨平臺的。也就是說,無論在 Window、Linux 還是 Mac 系統環境下,C 庫 API 都能夠完美適配當前系統環境,不存在只能在特定系統條件下使用的問題。相對的,系統調用 API 如 read()、write() 等只能在 Unix/Linux 系統下使用。高兼容性是 C 庫 API 的最大特點。

  • 高性能

使用系統調用 API(如 Linux 下的 open()、read()、write() 等)爲系統層級的直接調用,每次調用該類 API 時將發生系統上下文切換,系統開銷較大。而 C 庫 API 實際爲系統調用 API 的上層封裝,例如在 Linux 下 fread() 爲 read() 的上層封裝。C 庫 API 在封裝中使用了緩衝區來緩存輸入輸出,即多次調用 C 庫 API 使緩衝區滿後纔將實際調用一次系統調用 API,這樣能夠減少上下文的切換次數,有效提升系統性能。緩衝區的大小缺省爲一個宏定義 BUFSIZE,具體數值在不同系統中可能不同,可使用 API 設置緩衝區的大小。

  • 文件流

文件流爲 C 語句中提出的概念。雖然文件分爲許多類型,但文件的內容都是以二進制格式存儲的。於是,C 語言對待文件時並不會區分具體的文件類型,而是都把文件看作爲流,按字節處理。文件流的優點在於無具體的數據邊界,完全由程序所控制。文件流的類型爲 FILE,其包含文件句柄、打開狀態、緩衝區等信息。

  • 侷限性

在 Linux 系統中,雖有一切皆文件的說法,但使用 C 庫的文件操作 API 也無法操作設備類型、管道類型等文件,只能操作普通類型的文本文件。需要對其他類型的文件時,需使用其對應的系統調用 API。


2. 常用文件操作 API

文件操作的 API 都在 C 標準庫 <stdio.h> 中,在使用時只需包含該頭文件即可。爲了方便查閱,將使用索引來快速跳轉到目標 API 介紹。索引列表如下:

fopen()

  • 函數聲明
FILE* fopen (const char* filename,   // 打來文件的名稱
             const char* mode        // 文件打開方式
             );
  • 函數說明

以形參 mode 的方式打開文件 filename

mode 的基本選項如下:

mode 描述
“r” 以可讀方式打開文件,該文件必須存在。
“w” 以可寫方式打開文件,若存在同名文件則將清空覆蓋,若無同名文件將創建新文件。
“a” 以追加只寫方式打開文件,若無同名文件將創建新文件。
“+” 以讀寫模式打開文件,該文件必須存在。
“b” 以二進制模式打開文件,該文件必須存在。
“t” 以文本模式打開文件,爲默認屬性,該文件必須存在。

其中,"r""w""a" 爲三種不同的打開方式,對應不同的操作權限,在使用時必須指定其中一個。如果同時指定多個方式時,以排在第一個的方式爲準。"+""b""t" 爲三種不同的打開模式。

在指定 mode 時,打開方式是必須的,打開模式是可選的,且必須打開方式在前打開模式在後。

打開成功時返回文件對應的文件流,失敗時將返回 NULL。

  • 使用示例
FILE* fp = fopen("test.txt", "r+");
if (fp == NULL)
{
    printf("open test.txt failed !!\n");
}

返回索引列表

fclose()

  • 函數聲明
int fclose (FILE* stream    // 目標文件流
            );
  • 函數說明

刷新文件流,釋放文件流相關資源,設置文件流的文件結束標識。

關閉成功時將返回 0,失敗時將返回 EOF(-1)並設置文件錯誤標識。

  • 使用示例
FILE* fp = fopen("test.txt", "r+");

if (fclose(fp) == EOF)
{
    printf("close test.txt failed !!\n");
}

返回索引列表

feof()

  • 函數聲明
int feof (FILE* stream    // 目標文件流
          );
  • 函數說明

查詢文件流對應的文件是否設置了文件結束標識,例如文件指針到達了文件末尾、使用 fclose() 關閉文件流的等情況。

已設置時將返回非 0,未設置時將返回 0。

  • 使用示例
if (feof(fp) == 0)
{
    // ...
}

返回索引列表

ferror()

  • 函數聲明
int ferror (FILE* stream    // 目標文件流
            );
  • 函數說明

查詢文件流對應的文件是否設置了文件錯誤標識,例如關閉文件流失敗、刷新文件流失敗等情況。

已設置時將返回非 0,未設置時將返回 0。

  • 使用示例
if (ferror(fp) == 0)
{
    // ...
}

返回索引列表

clearerr()

  • 函數聲明
int clearerr (FILE* stream    // 目標文件流
              );
  • 函數說明

清除文件的結束標識和錯誤標識。

  • 使用示例
clearerr(fp);

返回索引列表

fwrite()

  • 函數聲明
size_t fwrite (const void* ptr,      // 指向寫入文件流的內容的內存
               size_t size,          // 寫入元素大小,以字節爲單位
               size_t number,        // 元素個數
               FILE* stream          // 目標文件流
               );
  • 函數說明

ptr 所指內存的 size * number 個字節內容寫入到文件流中,。寫操作將同時移動文件指針。

返回實際寫入的元素個數。

  • 使用示例
FILE* fp = fopen("test.txt", "w");

char* p = "hello world";
if (fp != NULL)
{
    int ret = fwrite(p, 1, strlen(p) + 1, fp);      // "+1" 表示把字符串結束符 `\0` 也寫入文件中
    printf("write %d byte to file !!\n", ret);
}

fclose(fp);

返回索引列表

fread()

  • 函數聲明
size_t fread (void* ptr,         // 指向讀取文件後所寫入的內存
              size_t size,       // 讀取元素大小,以字節爲單位
              size_t number,     // 讀取元素個數
              FILE* stream       // 目標文件流
              );
  • 函數說明

從文件流中讀取 size * number 個字節內容到 ptr 所指內存中。讀取操作將同時移動文件指針。當讀到文件末尾時將設置文件結束標識。

返回實際所讀取到的元素個數。

  • 使用示例
FILE* fp = fopen("test.txt", "r");

char buf[64] = {0};
if (fp != NULL)
{
    int ret = fread(buf, 1, sizeof(buf), fp);
    printf("read %d byte from file !!\n", ret);
    printf("the file content is : %s\n", buf);
}   

fclose(fp);

返回索引列表

fgets()

  • 函數聲明
char* fgets (char* str,      // 指向讀取文件後所寫入的內存
             int n,          // 讀取最大字符數
             FILE* stream    // 目標文件流
             );
  • 函數說明

從文件流中讀取至多 n - 1 個字符寫到 str 所指內存中,並在讀取的最後插入 '\0' 作爲字符串結尾。讀取操作將同時移動文件指針。當讀到文件末尾時將設置文件結束標識。

當讀取到換行符或讀取到文件末尾時將停止讀取。讀取到文件末尾時或讀取失敗時將返回 NULL,取餘情況皆返回與 str 相同的地址。

需要注意的是,fgets 讀取文件而文件已到達末尾導致讀取失敗時纔會設置文件結束標識,而 fread 在讀取到文件末尾時就會設置文件結束標識。這是兩函數在使用上需要警惕的區別。

  • 使用示例
FILE* fp = fopen("test.txt", "r");

char buf[1024] = {0};
if (fp != NULL)
{
    printf("the file content is :\n");
    do
    {
        fgets(buf, sizeof(buf), fp);
        printf("%s", buf);
    } while (feof(fp) == 0);
}   

fclose(fp);

返回索引列表

fputs()

  • 函數聲明
int fputs (const char* str,
           FILE* stream    // 目標文件流
           );
  • 函數說明

把字符串 str 輸出到文件流中,且並不會在最後額外輸出 \0 作爲字符串結尾。寫操作同時移動文件指針。

輸出成功時將返回非負數,失敗時將返回 EOF(-1)並設置文件錯誤標識。

  • 使用示例
FILE* fp = fopen("test.txt", "w");

if (fputs("hello C", fp) == EOF)
{
    printf("fputs test.txt failed !!\n");
}

fclose(fp);

返回索引列表

fflush()

  • 函數聲明
int fflush (FILE* stream    // 目標文件流
            );
  • 函數說明

刷新文件流,即把緩存區的內容都輸出到文件中並清空。

關閉成功時將返回 0,失敗時將返回 EOF(-1)並設置文件錯誤標識。

  • 使用示例
FILE* fp = fopen("test.txt", "r");

if (fflush(fp) == EOF)
{
    printf("flush test.txt failed !!\n");
}

fclose(fp);

返回索引列表

fseek()

  • 函數聲明
int fseek (FILE* stream          // 目標文件流
           long int offset,      // 相對於 whence 的偏移量,以字節爲單位
           int whence            // 文件位置
           );
  • 函數說明

把文件指針移動到指定的 whence + offset 位置。

whence 的選項如下:

whence 描述
SEEK_SET 文件開頭
SEEK_END 文件末尾
SEEK_CUR 當前文件位置

移動成功時將返回 0,失敗時將返回 EOF(-1)並設置文件錯誤標識。

  • 使用示例
FILE* fp = fopen("test.txt", "r");

if (fseek(fp, 0, SEEK_SET) == EOF)
{
    printf("seek test.txt failed !!\n");
}


fclose(fp);

返回索引列表

ftell()

  • 函數聲明
int ftell (FILE* stream          // 目標文件流
           );
  • 函數說明

獲取文件流對應文件的文件指針的位置。

返回文件指針與文件開頭的距離,該距離以字節爲單位。

  • 使用示例
FILE* fp = fopen("test.txt", "r");

char buf[64] = {0};
if (fp != NULL)
{
    do
    {
        ret = fgets(buf + ftell(fp), sizeof(buf), fp);
    } while (feof(fp) == 0);
    printf("the file content is : \n%s\n", buf);
} 

fclose(fp);

返回索引列表

fdopen()

  • 函數聲明
FILE* fdopen (int fd,         // 目標文件句柄
              int mode
              );
  • 函數說明

以形參 mode 的方式根據文件句柄 fd 打開文件流,mode 選項與 fopen() 一致。

打開成功時返回文件對應的文件流,失敗時將返回 NULL。

  • 使用示例
int fd = open("test.txt", O_RDWR);
FILE* fp = fdopen(fd, "r+");
if (fp == NULL)
{
    printf("open test.txt failed !!\n");
}

返回索引列表

fileno()

  • 函數聲明
int fileno (FILE* stream          // 目標文件流
            );
  • 函數說明

獲取文件流對應的文件句柄。

獲取成功時返回文件流對應文件的文件句柄,失敗時將返回 EOF(-1)並設置文件錯誤標識。

  • 使用示例
FILE* fp = fopen("test.txt", "r+");
int fd = fileno(fp);
if (fd == EOF)
{
    printf("the fp of test.txt error !!\n");
}

返回索引列表

fgetpos()

  • 函數聲明
int fgetpos (FILE* stream,       // 目標文件流
             fpos_t* pos         // 文件指針
             );
  • 函數說明

獲取文件指針並保存到 pos 中。其中,fpos_t 爲結構體類型,查看其所含地址可查看其成員 pos.__pos

獲取成功時返回 0,失敗時將返回 EOF(-1)並設置文件錯誤標識。

  • 使用示例
FILE* fp = fopen("tmp.txt", "r+");
fpos_t pos = {0};

fgetpos(fp, &pos);
printf("pos : %ld\n", pos.__pos);        // pos : 0

fseek(fp, 5, SEEK_CUR);
fgetpos(fp, &pos);
printf("pos : %ld\n", pos.__pos);        // pos : 5

fclose(fp);

返回索引列表

fsetpos()

  • 函數聲明
int fsetpos (FILE* stream,          // 目標文件流
             const fpos_t* pos      // 文件指針
             );
  • 函數說明

把文件流的文件指針設置爲 pos

設置成功時返回 0,失敗時將返回 EOF(-1)並設置文件錯誤標識。

  • 使用示例
FILE* fp = fopen("tmp.txt", "r+");
fpos_t pos = {0};

fgetpos(fp, &pos);
printf("ftell = %ld\n", ftell(fp));     // ftell = 0

fseek(fp, 20, SEEK_CUR);
printf("ftell = %ld\n", ftell(fp));     // ftell = 20

fsetpos(fp, &pos);
printf("ftell = %ld\n", ftell(fp));     // ftell = 0

fclose(fp);

返回索引列表

setbuf()

  • 函數聲明
void setbuf (FILE* stream,      // 目標文件流 
             char* buffer       // 自定義緩衝區
             );
  • 函數說明

buffer 設置爲文件流的緩衝區。

  • 使用示例
char buf[100] = {0};
setbuf(fp, buf);

返回索引列表

setvbuf()

  • 函數聲明
int setvbuf (FILE* stream,      // 目標文件流 
             char* buffer,      // 自定義緩衝區
             int mode,          // 設置緩衝模式
             size_t size        // 緩衝區大小,以字節爲單位
             );
  • 函數說明

根據 modebuffer 設置爲文件流的緩衝區。

mode 的選項如下:

mode 描述
_IOFBF 全緩衝模式。輸入時,一次性讀取滿緩衝區數據;輸出時,當緩衝區滿時所緩衝的數據將一次性輸出。
_IOLBF 行緩衝模式。輸入時,一次性讀取到換行符爲止或讀取滿緩衝區的數據;輸出時,當寫的數據爲換行或緩衝區滿時所緩衝的數據將一次性輸出。
_IONBF 無緩衝模式。每個 I/O 操作都被即時寫入。此時參數 buffersize 被忽略。

默認情況下,stdin 與 stdout 爲行緩衝,stderr 爲無緩衝,非標準文件流爲全緩衝。

設置成功是返回 0,失敗時將返回 EOF(-1)並設置文件錯誤標識。

  • 使用示例
char buf[100] = {0};
if (setvbuf(fp, buf, _IOFBF, sizeof(buf)) == EOF)
{
    // ...
}

返回索引列表


3. 實戰

以下將使用以上介紹的部分 API 來設計一個與 Linux 中 cat 命令功能相同即直接打印文本內容的程序。

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char* argv[])
{
    if (argc < 2)
    {
        printf("missing file operand !!\n");
        return 0;
    }
    
    for (int i = 1; i < argc; i++)
    {
        FILE* fp = fopen(argv[i], "r");
        if (fp == NULL)
        {
            printf("can't open %s !!\n", argv[i]);
            return 0;
        }

        fseek(fp, 0, SEEK_END);
        int file_size = ftell(fp);
        char* buf = (char*)malloc(file_size + 1);       // "+1" 保證字符串以 '\0' 結尾 
        memset(buf, 0, file_size + 1);
        fseek(fp, 0, SEEK_SET);

    	size_t ret = fread(buf, 1, file_size, fp);
		if (ret != file_size)
		{
			printf("read %s err !!\n", argv[i]);
			return 0;
		}
        printf("%s", buf);
		fflush(stdout);
		
        free(buf);
        fclose(fp);
    }

    return 0;
}

編譯生成可執行文件 Kcat:

gcc main.c -o Kcat

現有兩個文本文件 FileA 與 FileB,先使用原 Linux 命令 cat 查看執行效果:

Kong@ubuntu:/mnt/hgfs/share/pj_c$ cat FileA FileB
this is FileA !!
123456789
987654321
this is FileB !!
qwertyuiop
asdfghjkl
zxcvbnm
Kong@ubuntu:/mnt/hgfs/share/pj_c$ 

再使用剛編譯生成的 Kcat 查看執行效果:

Kong@ubuntu:/mnt/hgfs/share/pj_c$ ./Kcat FileA FileB
this is FileA !!
123456789
987654321
this is FileB !!
qwertyuiop
asdfghjkl
zxcvbnm
Kong@ubuntu:/mnt/hgfs/share/pj_c$ 

可見,cat 與 Kcat 效果是一致的。

在 Kcat 中也可以使用 fgets() 來讀取文件如下,但這樣子每次調用 fgets() 只能讀取一行且每次讀取都需要調用一次 ftell(),程序效率較低。

do
{
    fgets(buf + ftell(fp), file_size, fp);
} while (feof(fp) == 0);

由於 Kcat 完全使用 C 庫 API 來編程設計,因此在 Window 和 Mac 系統下編譯生成可執行文件後也都能夠正常使用。


更多 C 高階系列博文

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