[单刷APUE系列]第五章——标准I/O库
目錄
[單刷APUE系列]第一章——Unix基礎知識[1]
[單刷APUE系列]第一章——Unix基礎知識[2]
[單刷APUE系列]第二章——Unix標準及實現
[單刷APUE系列]第三章——文件I/O
[單刷APUE系列]第四章——文件和目錄[1]
[單刷APUE系列]第四章——文件和目錄[2]
[單刷APUE系列]第五章——標準I/O庫
[單刷APUE系列]第六章——系統數據文件和信息
[單刷APUE系列]第七章——進程環境
[單刷APUE系列]第八章——進程控制[1]
[單刷APUE系列]第八章——進程控制[2]
[單刷APUE系列]第九章——進程關系
[單刷APUE系列]第十章——信號[1]
流和FILE對象
在學習C語言的時候,肯定也對標準I/O庫有所了解,這個庫是由ISO C標注制定的,前面也說過,ISO C被包含在SUS標準中,所以SUS在ISO C的標準上,又進行了擴充。
標準I/O庫最大的好處就是不需要再和底層內核調用打交道了,非常方便的就能跨平臺使用,在前面幾節中,大家也對I/O各種繁瑣的細節也有些頭暈,而標準I/O庫就很便于使用,但是如果不對底層有所了解,在使用的時候也會出現問題的。
在第三章中,所有的內容都是由`stat'引出,然后圍繞著文件描述符進行講述,但是標準I/O庫則是圍繞著流來進行。
眾所周知,現有的所有字符,可以分為ASCII字符和寬字符,所以標準I/O函數是圍繞著單字符和寬字符的。就像是汽車和火車,汽車只能在馬路上跑,火車只能在鐵軌上跑,當一個流被創建的時候,是不能確定是寬字符還是單字節的,只有后續I/O操作才會確定下來。
如果流的方向被確定了,那么fwide函數不會改變流的方向。否則,fwide會設置流的方向
如果mode小于0,流將被設置為字節方向;如果mode大于0,流將被設置為寬方向。如果mode為0,則不改變方向
無論是否改變,返回值都會存在,用于確定流的方向
標準輸入、標準輸出和標準錯誤
就像前文提到的一樣,進程自動會打開三個文件描述符。我們知道文件描述符和文件關聯,就像是預定義了三個文件描述符STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO,標準庫也提供了預定義的stdin、stdout、stderr文件指針。
緩沖
前面提到過,Unix系統自己提供的是不帶緩沖的I/O函數。緩沖的意義就是為了減少調用read和write的次數,標準庫對每個流都自動管理緩沖,這樣開發者就不會為了緩沖區到底用多大、跨平臺標準不一致而苦惱了,但是也應當注意到,標準I/O函數庫的提出已經經過很多年了,而且幾乎沒有改動過,所以就算是緩沖,也有很多的困擾出現。
在前面提到過,Unix系統實現在內核中都設有高速緩沖,大多數的磁盤IO都通過緩沖區進行,為了保證實際文件系統的一致性,系統還提供了一些磁盤同步函數。對于標準IO來說,它有三種緩沖類型
全緩沖,或者說,叫做塊緩沖。相關標準IO函數會先使用malloc來獲得固定大小的緩沖區,每當緩沖區被填滿后就會進行實際的磁盤寫入,開發者也可以手動調用fflush函數來強制將緩沖區寫入磁盤,請記住,這個是標準C函數庫的函數,和前面提到的Unix系統提供的磁盤同步系統調用時兩碼事
行緩沖。在輸入輸出時遇到換行符,自動進行IO寫入。我們知道,標準IO庫的緩沖區是固定的,所以只要填滿了緩沖區,即使沒有遇上換行符也會執行IO操作
不帶緩沖。就如同Unix系統提供的write函數一樣,標準庫不對字符進行緩沖存儲
需要注意的是,stderr通常是不帶緩沖的,因為錯誤信息通常需要得到立即的輸出處理。
ISO C標準只規定了
當且僅當標準輸入輸出不指向交互式設備時,他們才是全緩沖的
標準錯誤絕對不是全緩沖
非常曖昧的定義,結果開發者還得自己注意不同平臺的實現。但是目前來說,大部分的系統規定都是一樣的
標準錯誤不帶緩沖
指向終端的流是行緩沖,其他則是全緩沖
從Mac OS X的系統手冊上可以找到上面提到的東西
Three types of buffering are available: unbuffered, block buffered, and line buffered. When an output stream is unbuffered, information appears on the des-tination file or terminal as soon as written; when it is block buffered, many characters are saved up and written as a block; when it is line buffered,
characters are saved up until a newline is output or input is read from any stream attached to a terminal device (typically stdin). The function fflush(3) may be used to force the block out early. (See fclose(3).)
Normally, all files are block buffered. When the first I/O operation occurs on a file, malloc(3) is called and an optimally-sized buffer is obtained. If a stream refers to a terminal (as stdout normally does), it is line buffered. The standard error stream stderr is always unbuffered.
下面是更改緩沖類型的函數
void setbuf(FILE *restrict stream, char *restrict buf); int setvbuf(FILE *restrict stream, char *restrict buf, int type, size_t size);setbuf函數用于打開關閉緩沖機制,將一個長度為BUFSIZ的緩沖區傳入參數,就會打開緩沖區,而傳入null則會關閉緩沖區。
setvbuf函數功能十分強大,我們可以精確的說明緩沖類型
_IONBF->unbuffered 無緩沖
_IOLBF->line buffered 行緩沖
_IOFBF->fully buffered 全緩沖
一個不帶緩沖的流可以忽略type和size參數,當一個緩沖傳入的buf是null時,系統會自動分配緩沖區。
其實除了上面兩個函數外,還有兩個函數,但是由于原著未講,所以這里也不提及。
其實setbuf函數除了沒有返回值,就等同于setvbuf(stream, buf, buf ? _IOFBF : _IONBF, BUFSIZ);
這個函數就是一個強制將緩沖區寫入磁盤的函數,當stream是null時,將清洗所有緩沖區,還有一個fpurge函數,這里不提及。
打開流
FILE *fopen(const char *restrict filename, const char *restrict mode); FILE *freopen(const char *restrict filename, const char *restrict mode, FILE *restrict stream); FILE *fdopen(int fildes, const char *mode);從字面意義上就能看出這些函數的作用,fopen就是打開一個文件,freopen則是在一個指定的流上重新打開文件,一般用于將文件在一個預定義流上打開,fdopen則是將一個文件描述符打開,主要用于管道和網絡通信。
mode參數指定對IO流的讀寫方式,在學習C語言的時候可能就已經有所接觸了
| r/rb | 讀打開 | O_RDONLY |
| w/wb | 寫打開 | O_WRONLY or O_CREAT or O_TRUNC |
| a/ab | 追加 | O_WRONLY or O_CREAT or O_APPEND |
| r+/r+b/rb+ | 讀寫打開 | O_RDWR |
| w+/w+b/wb+ | 讀寫打開 | O_RDWR or O_CREAT or O_TRUNC |
| a+/a+b/ab+ | 文件尾讀寫打開 | O_RDWR or O_CREAT or O_APPEND |
在系統手冊中也有一段關于mode的講解
The argument mode points to a string beginning with one of the following sequences (Additional characters may follow these sequences.):
``r'' Open text file for reading. The stream is positioned at the beginning of the file.
``r+'' Open for reading and writing. The stream is positioned at the beginning of the file.
``w'' Truncate to zero length or create text file for writing. The stream is positioned at the beginning of the file.
``w+'' Open for reading and writing. The file is created if it does not exist, otherwise it is truncated. The stream is positioned at the beginning of the file.
``a'' Open for writing. The file is created if it does not exist. The stream is positioned at the end of the file. Subsequent writes to the file will always end up at the then current end of file, irrespective of any intervening fseek(3) or similar.
``a+'' Open for reading and writing. The file is created if it does not exist. The stream is positioned at the end of the file. Subsequent writes to the file will always end up at the then current end of file, irrespective of any intervening fseek(3) or similar.
The mode string can also include the letter b'' either as last character or as a character between the characters in any of the two-character strings described above. This is strictly for compatibility with ISO/IEC 9899:1990 (ISO C90'') and has no effect; the ``b'' is ignored.
Finally, as an extension to the standards (and thus may not be portable), mode string may end with the letter x'', which insists on creating a new file when used with w'' or ``a''. If path exists, then an error is returned (this is the equivalent of specifying O_EXCL with open(2)).
上面就多了一個x參數,等價于open函數的O_EXCL參數。還有就是對于Unix緩解實際上二進制和普通文件沒有任何區別,所以b參數將被忽視。
The fdopen() function associates a stream with the existing file descriptor, fildes. The mode of the stream must be compatible with the mode of the file descriptor. When the stream is closed via fclose(3), fildes is closed also.
原著中關于fdopen的講解過于繁瑣,實際上就是由于文件描述符已經存在,流的模式必須兼容文件描述符,并且當使用fclose關閉時,文件描述符也被關閉。
Any created files will have mode "S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH" (0666), as modified by the process' umask value (see umask(2)).
還記得前面到的open和creat函數可以指定文件的權限,但是標準庫對于文件只有一種方式,就是以0666的模式創建文件,但是會被umask掩碼字刪減權限。
int fclose(FILE *stream);很簡單,就是將緩沖區內容寫入磁盤并關閉文件,如果緩沖區是自動分配則會自動回收緩沖區。
讀寫流
在學習C語言輸入輸出的時候,書上講的是scanf和printf格式化輸入輸出函數,但是除了格式化輸入輸出以外,還有三種非格式化IO
一次讀寫一個字符
一次讀寫一行
直接IO,也叫作二進制讀寫
The fgetc() function obtains the next input character (if present) from the stream pointed at by stream, or the next character pushed back on the stream via ungetc(3).
The getc() function acts essentially identically to fgetc(), but is a macro that expands in-line.
The getchar() function is equivalent to getc(stdin).
實際上還有三個讀取函數,但是不在介紹范圍內。上面已經把三個函數都介紹了,fget就是獲得下一個輸入字符,getc函數等價于fgetc,但是可以被實現為宏定義用于內聯,getchar函數等同于getc(stdin),所以在實際開發中,應當注意fgetc和getc的區別。
If successful, these routines return the next requested object from the stream. Character values are returned as an unsigned char converted to an int. If the stream is at end-of-file or a read error occurs, the routines return EOF. The routines feof(3) and ferror(3) must be used to distinguish between end-of-file and error. If an error occurs, the global variable errno is set to indicate the error. The end-of-file condition is remembered, even on a termi-nal, and all subsequent attempts to read will return EOF until the condition is cleared with clearerr(3).
書上寫的挺繁瑣的,筆者就將手冊說明抄錄在上面,看不懂原著的解釋,看上面的解釋就懂了,
int ferror(FILE *stream); int feof(FILE *stream);void clearerr(FILE *stream);這兩個函數用于區分EOF和錯誤發生,我們注意到,這里的參數是一個stream的文件指針,那我們是否可以猜測錯誤碼和文件結束標志是和文件結構體相關,筆者并沒有在文件結構體中找到這兩個標志,所以也只能當做猜測。原著中則明確指出了大多數實現中存在這兩個標志。最后一個函數則是用于清除這兩個標志。
int ungetc(int c, FILE *stream);這個函數就是將已經讀取的字符反壓回流。
int putc(int c, FILE *stream); int fputc(int c, FILE *stream); int putchar(int c);就如同前面的輸入函數一樣,這里就不在重復講述了。
一次讀取一行IO
char *fgets(char * restrict str, int size, FILE * restrict stream); char *gets(char *str);The fgets() function reads at most one less than the number of characters specified by size from the given stream and stores them in the string str. Read-ing stops when a newline character is found, at end-of-file or error. The newline, if any, is retained. If any characters are read and there is no error,a `\0' character is appended to end the string.
The gets() function is equivalent to fgets() with an infinite size and a stream of stdin, except that the newline character (if any) is not stored in the string. It is the caller's responsibility to ensure that the input line, if any, is sufficiently short to fit in the string.
fgets函數讀取不超過size參數規定的字符串從給定的流中,并將其存儲在str字符串中,讀取一直會持續到新行、EOF、錯誤發生,并且null字節永遠會在字符串末尾,這也就是說,讀取的字符串最大長度是size - 1。
gets函數等價于一個指定了無限的size和標準輸入的fgets函數,并且函數不會將換行符存儲在str參數中,但是調用者得自行保證輸入足夠短能存放在str參數中。由于可能會導致緩沖區溢出,所以實際上這個函數不被推薦使用。
puts函數不像gets函數一樣不安全,但是實際上也應當少用,因為它會在一行輸出后,再次輸出一個換行符,而fgets和fputs則需要我們自己處理換行符。
二進制IO
size_t fread(void *restrict ptr, size_t size, size_t nitems, FILE *restrict stream); size_t fwrite(const void *restrict ptr, size_t size, size_t nitems, FILE *restrict stream);The function fread() reads nitems objects, each size bytes long, from the stream pointed to by stream, storing them at the location given by ptr.
The function fwrite() writes nitems objects, each size bytes long, to the stream pointed to by stream, obtaining them from the location given by ptr.
fread讀取nitems個對象,每個size字節長,從stream流中讀取,存儲在ptr位置,fwrite寫入nitems個對象,每個size字節長,寫到stream流中,從ptr位置讀取。兩句話就能說明這兩個函數的作用。
The functions fread() and fwrite() advance the file position indicator for the stream by the number of bytes read or written. They return the number of objects read or written. If an error occurs, or the end-of-file is reached, the return value is a short object count (or zero).
The function fread() does not distinguish between end-of-file and error; callers must use feof(3) and ferror(3) to determine which occurred. The function fwrite() returns a value less than nitems only if a write error has occurred.
返回值是讀寫的對象數目,如果到達了底部或者出錯,則返回實際寫入的對象數和0,需要feof和ferror來判斷區分。
定位流
和Unix系統提供的無緩沖IO一樣,標準C庫也提供了流定位函數
ftell和fseek函數。非常古老的函數,最好少用
ftello和fseeko函數。只是把穩健偏移量類型從long換成了off_t
fgetpos和fsetpos函數。被ISO C引入的,使用抽象文件位置記錄位置,跨平臺推薦使用
這些函數單位是都是字節,其中whence和Unix系統的lseek函數是一樣的,rewind就是把流設置到頭位置。
off_t ftello(FILE *stream); int fseeko(FILE *stream, off_t offset, int whence);除了單位不同,和ftell、fseek沒有區別
int fgetpos(FILE *restrict stream, fpos_t *restrict pos); int fsetpos(FILE *stream, const fpos_t *pos);格式化IO
格式化IO函數可能是我們使用的最多最熟悉的,就5個函數
int printf(const char * restrict format, ...); int fprintf(FILE * restrict stream, const char * restrict format, ...); int dprintf(int fd, const char * restrict format, ...); int sprintf(char * restrict str, const char * restrict format, ...); int snprintf(char * restrict str, size_t size, const char * restrict format, ...);實際上還有其他輸出函數,但是這里也不提及,printf就是向標準輸出寫,fprintf是向指定流寫,dprintf是向文件描述符寫,sprintf和snprintf都是是向一個字符串寫,但是snprintf加入了size參數確定大小,sprintf由于存在緩沖區溢出的隱患,所以也不建議使用了。
至于格式控制字符串如何書寫,原著中有,手冊中也非常詳細,但是由于過長,所以不再摘錄出來。
下面是printf函數族的變體
這些函數被放在<stdarg.h>文件中,只是將可變參數表改成了va_list。
格式化輸入
int scanf(const char *restrict format, ...); int fscanf(FILE *restrict stream, const char *restrict format, ...); int sscanf(const char *restrict s, const char *restrict format, ...);與輸出相同,輸入函數也有對應的變體
int vscanf(const char *restrict format, va_list arg); int vfscanf(FILE *restrict stream, const char *restrict format, va_list arg); int vsscanf(const char *restrict s, const char *restrict format, va_list arg);實現細節
實際上在Unix系統中,標準C庫最終都是會調用系統提供的接口,所以在FILE結構體中,我們可以看到文件描述符的存在
int fileno(FILE *stream);雖然fileno函數是一個標準C庫函數,但是卻是POSIX規定的。
在學習Unix系統開發的時候,多查系統手冊,有了疑問多看看源代碼,是永遠不會錯的,而且有一些特殊的小技巧可以幫助我們開發找到錯誤
就比如上面,讓C編譯器只進行預處理,然后就能得到預處理后的文件,這樣看起來就更容易了,不需要我們在多個頭文件中反復跳轉。
臨時文件
char *tmpnam(char *s); FILE *tmpfile(void);tmpnam函數產生一個有效路徑名字符串,如果s參數為null,則所產生的路徑存放在靜態區,然后將指針返回,當繼續調用時,將會重寫整個靜態區,如果ptr不是null,則將其存放在ptr中,ptr也作為函數值返回。
tmpfile創建一個臨時文件,并且在文件關閉時刪除文件,我們知道,進程結束時會自動關閉所有文件,所以當進程結束時,也會刪除文件。
通常開發者是先調用tmpnam產生唯一路徑,然后使用該路徑創建文件,并立即unlink,前文說過,對一個打開的文件使用unlink等命令時,不會立即刪除文件,而是等到最后文件關閉才刪除。
char *mkdtemp(char *template); int mkstemp(char *template);The mkstemp() function makes the same replacement to the template and creates the template file, mode 0600, returning a file descriptor opened for reading and writing. This avoids the race between testing for a file's existence and opening it for use.
The mkdtemp() function makes the same replacement to the template as in mktemp() and creates the template directory, mode 0700.
模板字符串是長這樣的/tmp/temp.XXXXXX,最后六位是被替換掉的。還有,要注意不要使用常亮字符串作為template。
內存流
在普通的流中,是和磁盤上的實際文件關聯在一起的,那么,是否存在一種虛擬的文件,將一塊內存區域當做文件來讀寫呢,實際上,是存在的,原著中提到了三個標準庫函數用于內存流的創建,但是非常遺憾,這是glibc專屬的,沒有glibc的系統只能自己實現,所以這里也不講述內存流,如果有需要的朋友可以對照Linux下的手冊和原著學習。
標準IO的替代
在文章最前面也提到了,標準函數庫已經有很久沒有做出修改了,而且從一路的學習下來,我們也看到了標準函數庫實際上存在著許多的缺陷。甚至很多東西在不同的系統上有著不同的表現,我們甚至還無法確定這個標準的存在。所以現在也有許多的替代品被提出來方便開發者使用。但是要記得,不管庫怎么變化,實際上的系統調用還是那個。
總結
以上是生活随笔為你收集整理的[单刷APUE系列]第五章——标准I/O库的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: android项目中刷新activity
- 下一篇: 计算机的前世