文件流
標准I/O文件流可用於單字節或多字節字符集。流的定向決定了所讀寫的是單字節還是多字節。流在最初創建時,並沒有定向,此時如果在為定向的流上使用多字節I/O函數,那么該流被設置為寬定向的;如果在為定向的流中使用單字節I/O函數,那么該流被設置為字節定向的。
如下兩個函數可用於改變流的定向:
| #include <stdio.h> #include <wchar.h> int fwide(FILE* fp, int mode); 返回值:流為寬定向,返回正值;流為字節定向,返回負值;流為定向,返回0 說明: fwide不改變已定向的流的定向。 mode > 0,則fwide試圖將流設置為寬定向; mode < 0,則fwide試圖將流設置為字節定向; mode = 0,則fwide不設置流的定向,而是返回標識當前流定向的值。
FILE* freopen(const char* restrict pathname, const char* restrict type, FILE* restrict fp); 說明: 在指定的流上打開一個指定的文件,如果該流已經打開,則先關閉該流;若該流已經定向,則清除該定向。該函數常用於將一個指定的文件打開為一個預定義的流:標准輸入、標准輸出、標准錯誤 |
緩沖
標准I/O提供緩沖的目的是盡可肯地減少read和write的次數,有3種類型:
| 類型 |
說明 |
| 全緩沖 |
在填滿整個緩沖區后才進行實際的I/O操作。可以調用fflush函數將緩沖區的數據寫入磁盤中。 |
| 行緩沖 |
當在輸入和輸出中遇到換行符或者緩沖區已滿時,執行實際的I/O操作。 |
| 無緩沖 |
標准I/O不對字符進行緩沖存儲。標准錯誤流stderr通常不帶緩沖區,這就使得可以盡快顯示出錯信息。 |
對於一個給定的流,也可以使用以下函數更改系統默認的緩沖類型:
| #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;失敗,-1 說明: setbuf用於打開或關閉流緩沖機制,參數buf指向一個長度為BUFSIZ(該常量在<stdio.h>中定義)的緩沖區;如果要關閉緩沖,則將buf設置為NULL即可。 setvbuf用於精確地設置所需的緩沖類型,mode取值如下:_IOFBF(全緩沖)/_IOLBF(行緩沖)/_IONBF(無緩沖);如果指定了mode為帶緩沖類型,而buf卻為NULL,則系統會自動分配BUFSIZ個字節的緩沖區。
int fflush(FILE* fp); 說明: 強制沖刷一個流到磁盤中。如果fp為NULL,則系統中的所有輸出流將被沖刷。 |
打開流
| #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); 說明: fopen打開指定路徑pathname的文件;freopen用於將一個指定文件打開為一個預定義的流:標准輸入、標准輸出、標准錯誤;fdopen根據fd返回一個打開的流,常用於由創建管道和網絡通信函數返回的描述符。 type參數指定了對該I/O流的讀寫方式。 |
讀寫流
在打開流后,有3種不同類型的非格式化I/O(格式化I/O是諸如printf和scanf等的函數):
| 類型 |
說明 |
| 每次處理一個字符 |
#include <stdio.h> int getc(FILE* fp); int fgetc(FILE* fp); int getchar(void); 返回值:成功,返回下一個字符;失敗或到達文件尾端,返回EOF 說明: getchar等價於getc(stdin)。 getc可以被實現為宏,而fgetc一定是個函數,因此fgetc調用的時間通常長於getc。
為了區分引起EOF的原因是到達文件尾端還是讀入失敗,引入如下三個函數: int ferror(FILE* fp); int feof(FILE* fp); void clearerr(FILE* fp); 返回值:條件為真,非0;否則,0 說明: 每個流在FILE對象中維護了兩個標志:出錯標志、文件結束標志,調用clearerr可以清除這兩個標志。
有時在讀一個輸入流時,我們需要先查看下一個字符是否是需要讀入的字符,此時需要利用ungetc函數將字符壓回輸入流中: int ungetc(int c, FILE* fp); 返回值:成功,返回c;失敗,EOF 說明: 用ungetc壓回字符,並非將其寫到底層文件或設備中,只是將其寫回標准I/O的緩沖區中。
int putc(int c, FILE* fp); int fputc(int c, FILE* fp); int putchar(int c); 返回值:成功,返回c;失敗,EOF 說明: putchar(c)等價於putc(c, stdout)。 putc可被實現為宏,而fputc只能是一個函數。
|
| 每次處理一行 |
#include <stdio.h> char* fgets(char* restrict buf, int n, FILE* restrict fp); char* gets(char* buf); 返回值:成功,返回buf;失敗,返回NULL 說明: gets從標准輸入讀,fgets從指定的文件讀。 每次讀取直至遇到換行符或者達到緩沖區長度n(由於緩沖區以null字節結尾,有效長度實則是n - 1),如果fgets讀取的行超過n – 1個字節,那么返回前n – 1個字節,對fgets的下一次調用將繼續讀取該行其余部分。 gets由於不能指定緩沖區長度n,因此在最新的ISO C中已被忽略,因此不推薦使用gets函數。
int fputs(const char* restrict str, FILE* restrict fp); int puts(const char* str); 返回值:成功,返回非負值;失敗,返回EOF 說明: fputs將一個以null字節終止的字符串寫到指定的流中,null終止符不寫出。 雖然,puts並沒有太大的安全隱患,但還是避免使用它,因為puts每次寫出數據后還附加寫出一個換行符。 |
| 直接I/O或二進制I/O |
比如我們需要一次讀寫一個完整的結構,如果使用getc/putc,那么必須循環遍歷整個結構,每次處理一個字節,非常麻煩;如果使用fgets/fputs,那么由於其遇到null字節就停止了,而在結構中可能含有null字節,因此也不能很好地工作。因此,提供了如下兩個函數: #include <stdio.h> size_t fread(void* restrict ptr, size_t size, size_t nobj, FILE* restrict fp); size_t fwrite(const void* restrict ptr, size_t size, size_t nobj, FILE* restrict fp); 返回值:成功讀寫的結構個數 例如:將data數組的第2~5個元素(一共4個)寫至一個文件上: float data[10]; if(fwrite(&data[2], sizeof(float), 4, fp) != 4) printf(“fwrite error”); |
示例程序:將標准輸入復制到標准輸出: (1)每次處理一個字符(getc/putc) [root@benxintuzi IO]# cat getc.c #include <stdio.h> int main(void) { int c; while((c = getc(stdin)) != EOF) if(putc(c, stdout) == EOF) printf("output error\n"); if(ferror(stdin)) printf("input error\n"); return 0; } [root@benxintuzi IO]# ./getc 1 hello 1 hello 2 benxintuzi 2 benxintuzi ^C [root@benxintuzi IO]# (2) 每次處理一行(fgets/fputs) [root@benxintuzi IO]# cat fgets.c #include <stdio.h> #define MAXLINE 4096 int main(void) { char buf[MAXLINE]; while(fgets(buf, MAXLINE, stdin) != NULL) if(fputs(buf, stdout) == EOF) printf("output error\n"); if(ferror(stdin)) printf("input error\n"); return 0; } [root@benxintuzi IO]# ./fgets benxin benxin tuzi tuzi ^C [root@benxintuzi IO]#
流定位
| #include <stdio.h> long ftell(FILE* fp); offset ftello(FILE* fp); 返回值:成功,返回文件當前位置;失敗,返回-1L
int fseek(FILE* fp, long offset, int whence); int fseeko(FILE* fp, off_t offset, int whence); 返回值:成功,0;失敗,-1 說明: whence取值如下:SEEK_SET/SEEK_CUR/SEEK_END。 fseek和fseeko唯一的區別是offset的類型不同。
void rewind(FILE* fp)將一個流設置到文件的起始位置。
int fgetpos(FILE* restrict fp, fpos_t* restrict pos); int fsetpos(FILE* fp, const fpos_t* pos); 返回值:成功,0;失敗,-1 說明: fgetpos將當前文件指針存入pos中;fsetpos將pos的值設為當前文件位置。 |
格式化I/O
| 格式化輸出: #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, ...); 返回值:成功,返回寫入的字符數;失敗,返回負值 說明: printf寫到標准輸出,fprintf寫到文件,dprintf寫到文件描述符,sprintf寫到buf中,但是可能會溢出,snprintf也寫到buf中,但是由於指定了緩沖區長度n,可能截斷但不會溢出。
格式化輸入: #include <stdio.h> int scanf(const char* restrict format, ...); int fscanf(FILE* restrict fp, const char* restrict format, ...); int sscanf(const char* restrict buf, const char* restrict format, ...); |
格式控制:
%[flags][fldwidth][precision][lenmodifier]convtype
| flags |
說明 |
| ' |
將整數按千位分組輸出 |
| - |
左對齊輸出 |
| + |
顯示帶符號轉換的正負號 |
| 空格 |
如果第一個字符不是正負號,則用空格代替 |
| # |
指定轉換格式,例如0x前綴 |
| 0 |
用0而非空格進行填充 |
fldwidth:最小字段寬度。若轉換后字符數小於該值,則用空格填充。該值可以是一個非負整數或*。
precision:精度表示,整數位數、浮點數小數位數、字符串最大字節數。該值可以是一個.加上非負整數或者*。
lenmodifier:參數長度。hh(signed或unsigned char)/h(signed或unsigned short)/l(signed或unsigned long)/ll(signed或unsigned long long)/j(intmax_t或uintmax_t)/z(size_t)/t(ptrdiff_t)/L(long double)。
convtype:轉換類型:
| d、i |
有符號十進制 |
| o |
無符號八進制 |
| u |
無符號十進制 |
| x、X |
無符號十六進制 |
| f、F |
雙精度浮點數 |
| e、E |
指數格式雙精度浮點數 |
| g、G |
根據轉換后的值解釋為f、F、e、E |
| a、A |
十六進制指數格式雙精度浮點數 |
| c |
字符 |
| s |
字符串 |
| C |
寬字符 |
| S |
寬字符串 |
| % |
%本身 |
| p |
void*指針 |
臨時文件
每個標准I/O流都有一個相關聯的文件描述符,可以調用int fileno(FILE* fp)來獲得這個描述符。如下兩個函數用於創建臨時文件:
| #inlcude <stdio.h> char* tmpnam(char* ptr); FILE* tmpfile(void); 說明: tmpnam產生一個與現有文件名不同的臨時文件名,每次調用時,都產生一個不同的臨時文件名。最多的調用次數為TMP_MAX(定義在<stdio.h>中)。若ptr為NULL,則所產生的臨時文件名存放在一個靜態區中,指向該靜態區的指針作為函數值返回;如果ptr不為NULL,則其指向長度大於等於L_tmpnam個字符的數組,所產生的臨時文件名存放在該數組中,ptr作為函數值返回。 tmpfile創建一個臨時的二進制文件(wb+),在關閉文件或程序結束時自動刪除該文件。
#include <stdlib.h> char* mkdtemp(char* template); int mkstemp(char* template); 說明: mkdtemp創建一個目錄,返回指向目錄名的指針;mkstemp創建一個文件,返回文件描述符。 template的后六位設置為XXXXXX。函數將這些占位符替換成不同的字符來構建一個唯一的名稱。 |
[root@benxintuzi IO]# cat tmp.c #include <stdio.h> int main(void) { char name[L_tmpnam], line[4096]; FILE* fp; printf("%s\n", tmpnam(NULL)); /* first temp name */ tmpnam(name); /* second temp name */ printf("%s\n", name); if((fp = tmpfile()) == NULL) /* create temp file */ printf("tmpfile error\n"); fputs("write one line to tmpfile\n", fp); /* write to temp file */ rewind(fp); /* then read it back */ if(fgets(line, sizeof(line), fp) == NULL) printf("fgets error\n"); fputs(line, stdout); /* print the line */ return 0; } [root@benxintuzi IO]# gcc tmp.c -o tmp /tmp/cc6sXVXs.o: In function `main': tmp.c:(.text+0x14): warning: the use of `tmpnam' is dangerous, better use `mkstemp' [root@benxintuzi IO]# ./tmp /tmp/filekyJuQu /tmp/fileKA5UyL write one line to tmpfile [root@benxintuzi IO]#
[root@benxintuzi IO]# cat mkstemp.c #include <errno.h> #include <stdio.h> #include <stdlib.h> #include <sys/stat.h> void make_temp(char *template); int main(void) { char good_template[] = "/tmp/dirXXXXXX"; /* right way */ char *bad_template = "/tmp/dirXXXXXX"; /* wrong way*/ printf("trying to create first temp file...\n"); make_temp(good_template); printf("trying to create second temp file...\n"); make_temp(bad_template); exit(0); } void make_temp(char *template) { int fd; struct stat sbuf; if ((fd = mkstemp(template)) < 0) printf("can't create temp file\n"); printf("temp name = %s\n", template); close(fd); if (stat(template, &sbuf) < 0) { if (errno == ENOENT) printf("file doesn't exist\n"); else printf("stat failed\n"); } else { printf("file exists\n"); unlink(template); } } [root@benxintuzi IO]# gcc mkstemp.c -o mkstemp [root@benxintuzi IO]# ./mkstemp trying to create first temp file... temp name = /tmp/dirnemrcT file exists trying to create second temp file... Segmentation fault (core dumped) [root@benxintuzi IO]# 說明: 對於第一個模板,由於使用了數組,則數組內容存儲在棧上;但是第二個模板,只有指針本身存儲在棧上,而具體字符串內容卻存放在可執行文件的只讀段中,因此當mkstemp函數試圖修改字符串時,出現segment fault。
內存流
在內存流中,所有的I/O都是通過在緩沖區與主存之間來回傳送字節來完成的。由於避免了緩沖區溢出,因此內存流非常適用於創建字符串。內存流只訪問主存,不訪問磁盤上的文件,所以性能方面會有顯著的提升。有三個函數用於內存流的創建:
| #include <stdio.h> FILE* fmemopen(void* restrict buf, size_t size, const char* restrict type); 返回值:成功,返回流指針;失敗,返回NULL 說明: fmemopen函數允許調用者指定自己的緩沖區用於內存流。buf指向緩沖區的開始位置,size指定了緩沖區的大小,type參數控制流的使用方式:r/rb/w/wb/a/ab/r+/r+b/rb+/w+/w+b/wb+/a+/a+b/ab+ 注意點: (1)以追加方式打開內存流時,當前文件位置設為緩沖區中的第一個null字節;如果緩沖區中不存在null字節,則當前文件位置設為緩沖區結尾的后一個字節。 (2)以其他方式打開內存流時,當前文件位置設為緩沖區的開始位置。 (3)如果buf為null,則打開內存流沒有任何意義。 (4)增加流緩沖區中的數據或者調用fclose、fflush、fseek、fseeko、fsetpos時都會在當前位置寫入一個null字節。
#include <stdio.h> FILE* open_menstream(char** bufp, size_t* sizep); #include <wchar.h> FILE* open_wmemstream(wchar_t** bufp, size_t sizep); 返回值:成功,返回流指針;失敗,返回NULL 說明: open_memstream函數創建的流是面向字節的,其與fmemopen的區別如下: 創建的流只能寫打開; 不能指定自己的緩沖區,可以通過bufp和sizep訪問緩沖區地址和大小; 關閉流后需要自行釋放緩沖區; 對流添加字節會增加緩沖區的大小。 |
以下程序說明了如何在我們自己提供的緩沖區上操作內存流:
[root@benxintuzi IO]# cat memstr.c #include <stdio.h> #include <stdlib.h> #define BSZ 48 int main(void) { FILE *fp; char buf[BSZ]; memset(buf, 'a', BSZ-2); buf[BSZ-2] = '\0'; buf[BSZ-1] = 'X'; if ((fp = fmemopen(buf, BSZ, "w+")) == NULL) printf("fmemopen failed\n"); printf("initial buffer contents: %s\n", buf); fprintf(fp, "hello, world"); printf("before flush: %s\n", buf); fflush(fp); printf("after fflush: %s\n", buf); printf("len of string in buf = %ld\n", (long)strlen(buf)); memset(buf, 'b', BSZ-2); buf[BSZ-2] = '\0'; buf[BSZ-1] = 'X'; fprintf(fp, "hello, world"); fseek(fp, 0, SEEK_SET); printf("after fseek: %s\n", buf); printf("len of string in buf = %ld\n", (long)strlen(buf)); memset(buf, 'c', BSZ-2); buf[BSZ-2] = '\0'; buf[BSZ-1] = 'X'; fprintf(fp, "hello, world"); fclose(fp); printf("after fclose: %s\n", buf); printf("len of string in buf = %ld\n", (long)strlen(buf)); return(0); } [root@benxintuzi IO]# ./memstr # 用a字符改寫緩沖區 initial buffer contents: # fmemopen在緩沖區開始處放置null字節 before flush: # 流沖刷后緩沖區才會變化 after fflush: hello, world len of string in buf = 12 # null字節加到字符串結尾 # 現在用b字符改寫緩沖區 after fseek: bbbbbbbbbbbbhello, world # fseek引起緩沖區沖刷 len of string in buf = 24 # 再次追加寫null字節 after fclose: hello, worldcccccccccccccccccccccccccccccccccc # 現在用c字符改寫緩沖區 len of string in buf = 46 # 沒有追加寫null字節 [root@benxintuzi IO]#
