今天我們圍繞標準I/O做一些詳細的討論。
首先,我們先來看一些重要的概念。
流和文件指針
文件I/O操作都是針對文件描述符進行的,相對的,標準I/O的操作都是圍繞一種叫做流(stream)的東西進行的,當使用標準 I/O 庫打開或創建一個文件時,我們就已使一個流與這個文件相關聯,通過流的讀入和輸出完成所需要的 I/O操作。
用fopen打開一個流會返回一個指向FILE對象的指針,即文件指針,FILE對象通常是一個結構,包含了標準I/O庫爲管理該流需要的所有信息,如用於實際I/O的文件描述符、指向用於該流緩衝區的指針、緩衝區長度、出錯標誌等。
系統爲每個進程預定義了3個可以自動被使用的流:標準輸入、標準輸出和標準錯誤,這3個標準I/O流通過預定義的文件指針stdin、stdout、stderr加以引用。
緩衝
標準I/O庫提供緩衝的目的是儘可能減少使用read和write系統調用的次數,緩衝類型有三種:
(1)全緩衝:填滿標準I/O緩衝區後才進行實際I/O操作,磁盤中的文件通常是全緩衝,術語沖洗(flush)說明標準I/O緩衝區的寫操作。
(2)行緩衝:在輸入和輸出中遇到換行符或行緩衝區被填滿時執行實際I/O操作,當流涉及一個終端(如標準輸入和標準輸出)時,通常使用行緩衝。
(3)不帶緩衝:標準I/O不對字符進行緩衝存儲,標準錯誤stderr通常默認爲不帶緩衝的。
ISO C規定:
當且僅當stdin和stdout不指向交互式設備時,他們纔是全緩衝。
stderr決不會是全緩衝。
Linux系統默認:
stderr是不帶緩衝的。
若流指向終端設備,則是行緩衝,否則爲全緩衝。
對於一個給定的已經打開且尚未執行任何操作的流,我們可以調用setbuf和setvbuf來更改系統默認的緩衝類型。
#include <stdio.h>
void setbuf(FILE *restrict fp, char *restrict buf);
int setvbuf(FILE *restrict fp, char *restrict buf, int mode, size_t size);
/*返回值:若成功。返回0;若出錯,返回非0*/
我們來看一下這兩個函數的區別:
setbuf只提供兩種功能——打開或關閉由第一個參數fp指定的流的緩衝機制。若爲打開緩衝,第二個參數buf必須指向一個長度爲BUFSIZ的緩衝區,BUFSIZ定義在stdio.h中,至於調用函數之後究竟是全緩衝還是行緩衝,那就取決於該流是與終端相關還是與文件相關了;若想關閉該流的緩衝,只需把buf設爲NULL即可。
setvbuf功能就要強大一些,它多了兩個參數mode和size,不僅可以實現setbuf的功能,還可以在打開緩衝時由mode指定具體的指定緩衝類型,由size指定緩衝區的長度。
mode參數 | 緩衝類型 |
---|---|
_IOFBF | 全緩衝 |
_IOLBF | 行緩衝 |
_IONBF | 不帶緩衝 |
關於這兩個函數更詳細的動作,可以看下下面這張表,這裏就不再多費口舌了。
關鍵字restrict:
大家可能都注意到了,這兩個函數的參數中都帶有restrict這個關鍵字,查了下發現是C99新增的,它只用於限定指針,作用是告訴編譯器,所有修改該指針所指向內容的操作都必須通過該指針進行,而不能通過其它途徑(其它變量或指針)來修改。這樣做的好處是能幫助編譯器進行更好的代碼優化,生成更有效率的彙編代碼。
接下來,我們圍繞對流的操作介紹一些函數,包括打開和關閉流、讀和寫流。
打開和關閉流
1、打開流
以下三個函數可以打開一個標準I/O流。
#include <stdio.h>
FILE *fopen(const char *restrict pathname, const char *restrict type);
FILE *freopen(const char *restrict pathname, const char *restrict type,
FILE *restrict fp);
FILE *fdopen(int fd, const char *type);
/*三個函數返回值:若成功,返回文件指針;若失敗,返回NULL*/
三個函數的區別:
(1)fopen:打開路徑名爲pathname的一個指定文件
(2)freopen:在一個指定的流上打開一個指定的文件,該函數一般用於將一個指定的文件打開爲一個預定義的流,也就是前面說的標準輸入、標準輸出和標準錯誤。
(3)fdopen:讀取一個現有的文件描述符,並使一個標準I/O流與其結合,常用於由創建管道和網絡通信通道函數返回的描述符。
其中,type參數可以用來指定對該流的讀寫方式,如下圖所示:
2、關閉流
#include <stdio.h>
int fclose(FILE *fp);
/*返回值:若成功,返回0;若失敗,返回EOF*/
當我們使用完一個文件之後,需要調用 fclose函數關閉該文件、釋放相關的資源,否則會造成內存泄漏,但在文件關閉前,系統還會進行一些操作,如沖洗緩衝區中輸出數據、丟棄緩衝區中的所有輸入數據、釋放爲流分配的緩衝區等。
讀和寫流
讀寫操作分成兩大類:非格式化I/O、格式化I/O
非格式化I/O又包括三種:每次一個字符I/O、每次一行I/O、每次一個結構I/O。
(一)非格式化I/O
1、每次一個字符的I/O
#include <stdio.h>
/*輸入函數*/
int getc(FILE *fp);
int fgetc(FILE *fp);
int getchar(void); /*等同於getc(stdin)*/
/*三個函數返回值:若成功,返回下一個字符;若已到達文件尾或出錯,返回EOF*/
/*對應的輸出函數*/
int putc(int c, FILE *fp);
int fputc(int c, FILE *fp);
int putchar(int c);
/*三個函數返回值:若成功,返回c;若出錯,返回EOF*/
關於三個輸入函數的返回值,注意這樣一句話:若已到達文件尾或出錯,返回EOF,所以如果想知道是哪一種情況,可以調用ferror或feof進行判斷。
#include <stdio.h>
int ferror(FILE *fp);
int feof(FILE *fp);
/*兩個函數返回值:若條件爲真,返回真;否則,返回假*/
那麼,ferror和feof是怎麼知道條件是否爲真呢?原來系統爲每個流在FILE對象中維護了兩個標誌:出錯標誌和文件結束標誌,所以當把FILE指針作爲參數傳遞給這兩個函數時,就可以通過這兩個標誌來判斷條件是真還是假了。
2、每次一行I/O
#include <stdio.h>
/*buf:緩衝區地址; n:緩衝區長度; fp:指定的流*/
char *fgets(char *restrict buf, int n, FILE *restrict fp);
char *gets(char *buf);
/*兩個函數返回值:若成功,返回buf;若已到達文件尾或出錯,返回NULL*/
int fputs(const char *restrict str, FILE *restrict fp);
int puts(const char *str);
/*兩個函數返回值:若成功,返回非負值;若出錯,返回EOF*/
我們先來分析兩個輸入函數:
fgets和gets都指定了緩衝區的地址,讀入的行送入其中,只不過gets從標準輸入讀,而fgets從指定的流讀而已。
gets函數並不檢查輸入行的長度是否超過緩衝區長度,因此有緩衝區溢出的危險,歷史上的蠕蟲病毒就是利用這個漏洞做的,所以gets一般不推薦使用。fgets彌補了gets的缺點,我們必須給它指定緩衝區長度n,當輸入行長度超過緩衝區長度時就會出錯。
關於fgets、緩衝區長度、輸入行長度要注意一個小問題:
fgets讀取輸入行直到遇到換行符,注意fgets也讀入換行符,而緩衝區總以null字節結尾,所以輸入行包括換行符在內的字符數不能超過n-1,也就是說除換行符外的實際字符數最多爲n-2,否則fgets返回一個不完整的行,該行的剩餘部分會在下一次調用fgets時接着讀取。
測試代碼
#include <stdio.h>
#include <stdlib.h>
#define MAXLINE 20
int main()
{
char buf[MAXLINE];
if (fgets(buf, MAXLINE, stdin) != NULL)
if (fputs(buf, stdout) == EOF)
printf("output error\n");
if (ferror(stdin))
printf("input error\n");
exit(0);
}
運行結果可看出,緩衝區長度20,第一次輸入18個字符,fgets將這些字符與換行符一起讀入,fputs輸出有換行;第二次輸入19字符,fgets沒有讀入換行符,fpus輸出無換行。
分析完了輸入,咱們再來看輸出。
puts將一個字符串寫到標準輸出,尾部終止符’\0’不寫出,隨後puts會補一個換行符到標準輸出,也就是說puts會自動換行。
fputs將一個字符串寫到指定的流,尾部終止符’\0’不寫出,但並不一定是每次輸出一行,因爲字符串不需要換行符作爲最後一個非NULL字節。
3、二進制I/O(每次讀寫一個結構)
該方式可一次讀寫一個完整的結構,通過fread和fwrite實現。
#include <stdio.h>
size_t fread(void *restrict ptr, size_t size, size_t nobj, FILE *restrict fp);
sizt_t fwrite(const void *restrict ptr, size_t size, size_t nobj, FILE *restrict fp);
/*兩個函數返回值:讀或寫的對象數*/
/*對於讀,若出錯或到達文件尾,返回值可少於nobj,需調用ferror或feof判斷是哪一種*/
/*對於寫,若返回值少於要求的nobj,則爲出錯*/
這兩個函數有以下常見用法:
(1)讀或寫一個二進制數組:例如將一個數組的第2~5個元素寫到一文件上,可以使用如下代碼。其中,size爲每個數組元素的長度,nobj爲欲寫的元素個數
int data[10];
if (fwrite(&data[1], sizeof(int), 4, fp) != 4)
printf("fwrite error\n");
(2)讀或寫一個結構:如下代碼,其中,size爲結構的長度,nobj爲1(要寫的結構體個數)
struct
{
short count;
long total;
char name[NAMESIZE];
} item;
if (fwrite(&item, sizeof(item), 1, fp) != 1)
printf("fwrite error\n");
(3)將(1)(2)結合起來就可以讀或寫一個結構數組:代碼如下,其中size是結構的sizeof,nobj是數組的元素個數。
/*定義一個結構數組*/
struct student
{
char name[20];
char sex[2];
int age;
char address[100];
} student[40];
if (fwrite(student, sizeof(struct student), 40, fp) != 40)
printf("fwrite error\n");
(二)格式化I/O
1、格式化輸出
#include <stdio.h>
int printf(const char *restrict format, ...);
int fprintf(FILE *restrict fp, const char *restrict format, ...);
int dprintf(int fd, const char *restrict format, ...);
int sprintf(char *restrict buf, const char *restrict format, ...);
int snprintf(char *restrict buf, size_t n,
const char *restrict format, ...);
我們來比較一下這5個函數:
printf將格式化數據寫到標準輸出,注意它的返回值是成功打印的字符數(不包括\0字符);
fdprintf將格式化數據寫到指定的流,dprintf寫到指定的文件描述符,這兩個函數的返回值也是成功打印的字符數(不包括\0字符);
sprintf將格式化數據寫到數組buf中,並在數組尾端自動加一個\0,若成功,函數返回寫入數組的字符數(不包括爲數組自動添加的\0),否則返回負值。
與gets類似,sprintf也有可能造成緩衝區溢出,所以有了snprintf,需要顯示指定緩衝區長度n,超過緩衝區尾端的所有字符都被丟棄。如果n足夠大,返回寫入buf的字符數,否則返回負值。
2、格式化輸入
#include <stdio.h>
int scanf(const char *restrict format, ...);
int fscasnf(FILE *restrict fp, const char *restrict format, ...);
int sscanf(char *restrict buf, const char *restrict format, ...);
這三個函數的區別和上面的printf函數族類似,大家比較着看下就行了,這裏不再詳細闡述了。
最後再來看下面這個函數:
#include <stdio.h>
int fileno(FILE *fp);
每個標準I/O流都有一個與其相關聯的文件描述符,可以調用fileno獲得其描述符並返回。