與文件I/O圍繞文件描述符操作不同,標准I/O的操作是圍繞流進行的。
對於流,《C和指針》裡有一段解釋得很好:
ANSI C進一步對I/O的概念進行了抽象。就C程序而言,所有的I/O操作只是簡單地從程序移進或移出字節的事情。因此,毫不驚奇的是,這種字節流便被稱為流(stream)。程序只需要關心創建正確的輸出字節數據,以及正確地解釋從輸入讀取的字節數據。特定I/O設備的細節對程序員是隱藏的。
TCPL Appendix B.1中這麼解釋:
A stream is a source or destination of data that may be associated with a disk or other peripheral.(流是一個可能與硬盤或者其他設備關聯的數據的源或者目的地)
簡單地說,流是對信息的一種抽象。C系統在處理文件(文本文件和二進制文件)時,並不區分類型,都看成是字符流,按字節進行處理。
輸入輸出字符流的開始和結束只由程序控制而不受物理符號(如回車符)的控制。
流有最小的信息單元就是二進制位,含有最小的信息包就是字節,C標准庫提供兩種類型的流:二進制流(binary stream)和文本流(text stream)。二進制流是有未經處理的字節構成的序列;文本流是由文本行(每行有0個或多個字符,並以'\n'結束)組成的序列。注意在UNIX中,並不區別兩種流。
當一個程序啟動時,,標准輸入、輸出、出錯三個流就已經被自動打開,並對應到默認的物理終端。這三個標准I/O流通過預定義(stdio.h)文件指針stdin,stdout,stderr加以引用當一個進程正常終止時(直接調用exit(),或從main返回)所有打開的標准I/O流都會被關閉,所有帶未寫緩沖數據的I/O流都會被沖洗。
PS:在main()中return(expr)等價於exit(expr),而exit則調用fclose()關閉每個文件描述符並刷洗對應緩存。
在Linux的應用程序中,通常用文件描述符0,1,2與標准輸入,標准輸出,標准出錯輸出相關聯。為符合POSIX規范,在
(詳細內容可以參考APUE3.9和5.4,本段純屬摘抄)
標准I/O提供緩存的目的是盡可能減少使用read和write調用的數量(系統調用比普通函數調用開銷大)。它也對每個I/O流自動地進行緩存管理,避免了應用程序需要考慮這一點所帶來的麻煩。
標准I/O提供了三種類型的緩存:
(1) 全緩存。在這種情況下,當填滿標准I/O緩存後才進行實際I/O操作。對於駐在磁盤上的文件通常是由標准I/O庫實施全緩存的。
(2) 行緩存。在這種情況下,當在輸入和輸出中遇到新行符時,標准I/O庫執行I/O操作。這允許我們一次輸出一個字符(用標准I/O fputc函數),但只有在寫了一行之後才進行實際I/O操作。
(3) 不帶緩存。標准I/O庫不對字符進行緩存。如果用標准I/O函數寫若干字符到不帶緩存的流中,則相當於用write系統調用函數將這些字符寫至相關聯的打開文件上。
標准出錯流stderr通常是不帶緩存的,這就使得出錯信息可以盡快顯示出來,而不管它們是否含有一個新行字符。
ANSI C要求下列緩存特征:
(1) 當且僅當標准輸入和標准輸出並不涉及交互作用設備時,它們才是全緩存的。
(2) 標准出錯決不會是全緩存的
但是,這並沒有告訴我們如果標准輸入和輸出涉及交互作用設備時,它們是不帶緩存的還是行緩存的,以及標准輸出是不帶緩存的,還是行緩存的。
SVR4和4.3 + BSD的系統默認使用下列類型的緩存:
? 標准出錯是不帶緩存的。
? 如若是涉及終端設備的其他流,則它們是行緩存的;否則是全緩存的。
可以通過下面的函數改變緩存類型(APUE5.4):
void setbuf(FILE *restrict fp, char *restrict buf); int setvbuf(FILE *restrict fp, char *restrict buf, int mode, size_t size);這些函數必須在流打開之後、但是未對流做任何操作之前被調用
參數buf通常指向一個長度為BUFSIZ的緩沖區,BUFSIZ在stdio.h中定義,可自行輸出查看
stdio.h: #ifndef BUFSIZ # define BUFSIZ _IO_BUFSIZ libio.h: #define _IO_BUFSIZ _G_BUFSIZ _G_config.h: #define _G_BUFSIZ 8192如要關閉緩沖,將buf置為NULL
Liunx上的默認情況是,當標准輸入輸出連接終端時是行緩沖的,緩沖區大小1024字節,重定向到普通文件時,他們變為全緩沖(APUE 5.12 程序5.3提供查看I/O相關信息的方法)
強制沖洗一個流
int fflush(FILE *fp)
使該流所有未寫數據傳送至內核。如fp為NULL,將使所有輸出流被清洗。
應當注意的是:fflush(NULL)並不能有效地清空輸入緩存。後面詳細討論
FILE * fopen ( const char * filename, const char * mode );
FILE * freopen ( const char * filename, const char * mode, FILE * stream );常用freopen進行輸入輸出重定向。
int getc(FILE *fp) int fgetc(FILE *fp) int getchar(void)getchar等價於getc(stdin)。前兩個函數區別在於,getc可被實現為宏,意味著fgetc調用時間略長。
不管是出錯還是到達文件尾端,三個函數都返回-1.
在大多數實現中,FILE維護了兩個標志:出錯標志和文件結束標志。可用下面三個函數判斷流是出錯還是結束,最後一個函數是清除兩標志:
int ferror(FILE *fp) int foef(FILE *fp) void clearerr(FILE *fp)
int ungetc(int c, FILE *fp)注意不能回送EOF
類似地,輸出函數:
int putc(int c, FILE *fp) int fputc(int c, FILE *fp) int putchar(int c)putchar(c)等價於putc(c,stdout),putc可被實現為宏。
通常為了避免過多的函數調用開銷,putchar和getchar都被實現為宏。
char * fgets ( char * str, int num, FILE * stream );它會讀取不超過num-1個字符,然後在末尾加上結束符 '\0' ,或者遇到換行符結束輸入,同時換行符也被傳入。
另一個函數:
char * gets(char* buf);
由於存在緩沖區溢出漏洞,不推薦使用。
相應地,輸出
int fputs(const char * str, FILE * fp); int puts(const char *char);fputs()將一個以NULL終止的字符串寫到指定的流,終止符NULL不寫出。
因此,我們貫徹這樣的方針,堅持使用fgets和fputs,並自己處理換行符。
格式化輸出:
int printf ( const char * format, ... ); int fprintf (FILE *fp, const char * format, ... ); int snprintf (char *buf, size_t n, const char * format, ... );
注意參數轉換說明中 %[flags][fldwidth][lenmodifier]convtype,寬度和精度字段可被置為*,而後用一個整形參數指定其值。
printf的實現:
printf是C中為數不多的變參函數之一,主要通過stdarg.h中的一系列宏來對參數列表進行處理。其源碼實現可以參看:點擊打開鏈接和點擊打開鏈接
使用變參函數機制,簡單模擬printf
#include#include #include #include void simon_printf(char *fmt, ...) { char buf[10]; char *p = fmt; char c_tmp, *s_tmp; int i_tmp; double f_tmp; va_list ap; va_start(ap, fmt); while (*p) { if (*p != '%') { putchar(*p++); continue; } else { switch (*++p) { case 'd': { i_tmp = va_arg(ap, int); // sprintf(buf, "%d", i_tmp); // write(STDOUT_FILENO, buf, strlen(buf)); printf("%d", i_tmp); break; } case 'f'://float在內部被提升為double { f_tmp = va_arg(ap, double); printf("%f", f_tmp); break; } case 'c'://char在內部被提升為int { i_tmp = va_arg(ap, int); printf("%c", i_tmp); break; } case 's': { for(s_tmp = va_arg(ap, char*); *s_tmp; s_tmp++) printf("%c", *s_tmp); break; } } p++; } } va_end(ap); } int main() { int a = 1; float b = 2.0; char c = 'a'; char *str = {"test"}; simon_printf("This is a test Message:\n int:%d\n float:%f\n string: %s\n char:%c\n ", a, b, str, c); return 0; }
格式化輸入:
int scanf(const char *format, ...); int fscanf(FILE *fp, const char *format, ...); int sscanf(const char *buf, const char *format, ...);
scanf中*表示抑制,不把該輸入賦值給對應變量,即跳過。
scanf()還有一些正則用法:[]表示輸入字符集,可以使用連字符表示范圍,scanf() 連續吃進集合中的字符並放入對應的字符數組,直到發現不在集合中的字符為止。用字符 ^ 可以說明補集。把 ^ 字符放為掃描集的第一字符時,構成其它字符組成的命令的補集合。
通常並不推薦使用scanf()的正則用法,用法復雜, 容易出錯。編譯器作語法分析時會很困難, 從而影響目標代碼的質量和執行效率。
1)fflush(NULL)
fflush的定義說得很清楚了,這種用法導致的結果不確定
If the given stream was open for writing (or if it was open for updating and the last i/o operation was an output operation) any unwritten data in its output buffer is written to the file.
If stream is a null pointer, all such streams are flushed.
In all other cases, the behavior depends on the specific library implementation. In some implementations, flushing a stream open for reading causes its input buffer to be cleared (but this is not portable expected behavior).
The stream remains open after this call.
When a file is closed, either because of a call to fclose or because the program terminates, all the buffers associated with it are automatically flushed.
如果stream指向輸出流或者更新流(update stream),並且這個更新流最近執行的操作不是輸入,那麼fflush函數將把任何未被寫入的數據寫入stream指向的文件(如標准輸出文件stdout)。
fflush(NULL)清空所有輸出流和上面提到的更新流。
否則,fflush函數的行為是不確定的。取決於編譯器,某些編譯器(如VC6)支持用 fflush(stdin) 來清空輸入緩沖,而gcc就不支持。
2)setbuf(stdin, NULL);
setbuf(stdin, NULL);是使stdin輸入流由默認緩沖區轉為無緩沖區,在沒有特殊要求的情況下還是適用的
3)int c;
while((c = getchar()) != '\n' && c != EOF);
由代碼知,不停地使用getchar()獲取緩沖區中字符,直到獲取的字符c是換行符’\n’或者是文件結尾符EOF為止。這個方法可以完美清除輸入緩沖區,並且具備可移植性。
4)scanf("%[^\n]%*c");
這裡用到了scanf格式化符中的“*”,即賦值屏蔽;“%[^集合]”,匹配不在集合中的任意字符序列。這也帶來個問題,緩沖區中的換行符’\n’會留下來,需要額外操作來單獨丟棄換行符。