[單刷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]segmentfault

流和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操做纔會肯定下來。安全

int fwide(FILE *stream, int mode);
  1. 若是流的方向被肯定了,那麼fwide函數不會改變流的方向。不然,fwide會設置流的方向網絡

  2. 若是mode小於0,流將被設置爲字節方向;若是mode大於0,流將被設置爲寬方向。若是mode爲0,則不改變方向app

  3. 不管是否改變,返回值都會存在,用於肯定流的方向less

標準輸入、標準輸出和標準錯誤

就像前文提到的同樣,進程自動會打開三個文件描述符。咱們知道文件描述符和文件關聯,就像是預約義了三個文件描述符STDIN_FILENOSTDOUT_FILENOSTDERR_FILENO,標準庫也提供了預約義的stdin、stdout、stderr文件指針。ide

緩衝

前面提到過,Unix系統本身提供的是不帶緩衝的I/O函數。緩衝的意義就是爲了減小調用readwrite的次數,標準庫對每一個流都自動管理緩衝,這樣開發者就不會爲了緩衝區到底用多大、跨平臺標準不一致而苦惱了,可是也應當注意到,標準I/O函數庫的提出已經通過不少年了,並且幾乎沒有改動過,因此就算是緩衝,也有不少的困擾出現。
在前面提到過,Unix系統實如今內核中都設有高速緩衝,大多數的磁盤IO都經過緩衝區進行,爲了保證明際文件系統的一致性,系統還提供了一些磁盤同步函數。對於標準IO來講,它有三種緩衝類型函數

  1. 全緩衝,或者說,叫作塊緩衝。相關標準IO函數會先使用malloc來得到固定大小的緩衝區,每當緩衝區被填滿後就會進行實際的磁盤寫入,開發者也能夠手動調用fflush函數來強制將緩衝區寫入磁盤,請記住,這個是標準C函數庫的函數,和前面提到的Unix系統提供的磁盤同步系統調用時兩碼事學習

  2. 行緩衝。在輸入輸出時遇到換行符,自動進行IO寫入。咱們知道,標準IO庫的緩衝區是固定的,因此只要填滿了緩衝區,即便沒有趕上換行符也會執行IO操做ui

  3. 不帶緩衝。就如同Unix系統提供的write函數同樣,標準庫不對字符進行緩衝存儲this

須要注意的是,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函數功能十分強大,咱們能夠精確的說明緩衝類型

  1. _IONBF->unbuffered 無緩衝

  2. _IOLBF->line buffered 行緩衝

  3. _IOFBF->fully buffered 全緩衝

一個不帶緩衝的流能夠忽略type和size參數,當一個緩衝傳入的buf是null時,系統會自動分配緩衝區。
其實除了上面兩個函數外,還有兩個函數,可是因爲原著未講,因此這裏也不說起。
其實setbuf函數除了沒有返回值,就等同於setvbuf(stream, buf, buf ? _IOFBF : _IONBF, BUFSIZ);

int fflush(FILE *stream);

這個函數就是一個強制將緩衝區寫入磁盤的函數,當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語言的時候可能就已經有所接觸了

mode 說明 open(2)標誌
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)).

還記得前面到的opencreat函數能夠指定文件的權限,可是標準庫對於文件只有一種方式,就是以0666的模式建立文件,可是會被umask掩碼字刪減權限。

int fclose(FILE *stream);

很簡單,就是將緩衝區內容寫入磁盤並關閉文件,若是緩衝區是自動分配則會自動回收緩衝區。

讀寫流

在學習C語言輸入輸出的時候,書上講的是scanfprintf格式化輸入輸出函數,可是除了格式化輸入輸出之外,還有三種非格式化IO

  1. 一次讀寫一個字符

  2. 一次讀寫一行

  3. 直接IO,也叫做二進制讀寫

int getc(FILE *stream);
int fgetc(FILE *stream);
int getchar(void);

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),因此在實際開發中,應當注意fgetcgetc的區別。

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參數中。因爲可能會致使緩衝區溢出,因此實際上這個函數不被推薦使用。

int fputs(const char *restrict s, FILE *restrict stream);
int puts(const char *s);

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,須要feofferror來判斷區分。

定位流

和Unix系統提供的無緩衝IO同樣,標準C庫也提供了流定位函數

  1. ftell和fseek函數。很是古老的函數,最好少用

  2. ftello和fseeko函數。只是把穩健偏移量類型從long換成了off_t

  3. fgetpos和fsetpos函數。被ISO C引入的,使用抽象文件位置記錄位置,跨平臺推薦使用

long ftell(FILE *stream);
int fseek(FILE *stream, long offset, int whence);
void rewind(FILE *stream);

這些函數單位是都是字節,其中whence和Unix系統的lseek函數是同樣的,rewind就是把流設置到頭位置。

off_t ftello(FILE *stream);
int fseeko(FILE *stream, off_t offset, int whence);

除了單位不一樣,和ftellfseek沒有區別

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函數族的變體

int vprintf(const char * restrict format, va_list ap);
int vfprintf(FILE * restrict stream, const char * restrict format, va_list ap);
int vsprintf(char * restrict str, const char * restrict format, va_list ap);
int vsnprintf(char * restrict str, size_t size, const char * restrict format, va_list ap);
int vdprintf(int fd, const char * restrict format, va_list ap);

這些函數被放在<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系統開發的時候,多查系統手冊,有了疑問多看看源代碼,是永遠不會錯的,並且有一些特殊的小技巧能夠幫助咱們開發找到錯誤

> cc -E xxx.c

就好比上面,讓C編譯器只進行預處理,而後就能獲得預處理後的文件,這樣看起來就更容易了,不須要咱們在多個頭文件中反覆跳轉。

臨時文件

char *tmpnam(char *s);
FILE *tmpfile(void);

tmpnam函數產生一個有效路徑名字符串,若是s參數爲null,則所產生的路徑存放在靜態區,而後將指針返回,當繼續調用時,將會重寫整個靜態區,若是ptr不是null,則將其存放在ptr中,ptr也做爲函數值返回。
tmpfile建立一個臨時文件,而且在文件關閉時刪除文件,咱們知道,進程結束時會自動關閉全部文件,因此當進程結束時,也會刪除文件。

#include "include/apue.h"

int main(int argc, char *argv[])
{
    char name[L_tmpnam], line[MAXLINE];
    FILE *fp;
    
    printf("%s\n", tmpnam(NULL));
    
    tmpnam(name);
    printf("%s\n", name);
    
    if ((fp = tmpfile()) == NULL)
        err_sys("tmpfile error");
    fputs("one line of output\n", fp);
    rewind(fp);
    if (fgets(line, sizeof(line), fp) == NULL)
        err_sys("fgets error");
    fputs(line, stdout);
    
    exit(0);
}

一般開發者是先調用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的替代

在文章最前面也提到了,標準函數庫已經有好久沒有作出修改了,並且從一路的學習下來,咱們也看到了標準函數庫實際上存在着許多的缺陷。甚至不少東西在不一樣的系統上有着不一樣的表現,咱們甚至還沒法肯定這個標準的存在。因此如今也有許多的替代品被提出來方便開發者使用。可是要記得,無論庫怎麼變化,實際上的系統調用仍是那個。

相關文章
相關標籤/搜索