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():打開文件流
- fclose():關閉文件流
- feof():查詢文件流對應的文件結束標識
- ferror():查詢文件流對應的文件錯誤標識
- clearerr():清除文件流對應的文件標識
- fread():讀文件流
- fwrite():寫文件流
- fgets():讀文件流
- fputs():寫文件流
- fflush():刷新文件流
- fseek():更改文件流對應的文件指針所指位置
- ftell():獲取文件流對應的文件指針所指位置
- fdopen():通過文件句柄打開對應的文件流
- fileno():通過文件流獲取對應的文件句柄
- fgetpos():獲取文件流對應的文件指針所指位置並保存下來
- fsetpos():設置文件流對應的文件指針所指位置
- setbuf():自定義文件流中緩衝區
- setvbuf():自定義文件流中緩衝區並指定緩衝模式
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 // 緩衝區大小,以字節爲單位
);
- 函數說明
根據 mode
把 buffer
設置爲文件流的緩衝區。
mode
的選項如下:
mode | 描述 |
---|---|
_IOFBF | 全緩衝模式。輸入時,一次性讀取滿緩衝區數據;輸出時,當緩衝區滿時所緩衝的數據將一次性輸出。 |
_IOLBF | 行緩衝模式。輸入時,一次性讀取到換行符爲止或讀取滿緩衝區的數據;輸出時,當寫的數據爲換行或緩衝區滿時所緩衝的數據將一次性輸出。 |
_IONBF | 無緩衝模式。每個 I/O 操作都被即時寫入。此時參數 buffer 和 size 被忽略。 |
默認情況下,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 系統下編譯生成可執行文件後也都能夠正常使用。