UNIX 環境高級編程|標準 I/O 庫
GitHub: https://github.com/storagezhang
Emai: debugzhang@163.com
本文為《UNIX 環境高級編程》第 5 章學習筆記
大多數 UNIX 應用程序都使用標準 I/O 庫。
本章說明了該庫提供的很多函數以及某些實現細節和效率方面的考慮。
標準 I/O 庫使用了緩沖技術,而它正是產生很多問題、引起許多混淆的部分。
5.2 流和 FILE 對象
對于標準 I/O 庫,它們的操作是圍繞流(stream)進行的。當用標準 I/O 庫打開或創建一個文件時,我們已使一個流與一個文件相關聯。
對于 ASCII 字符集,一個字符用一個字節表示。對于國際字符集,一個字符可用多個字節表示。標準 I/O 文件流可用于單字節或多字節(“寬”)字符集。
流的定向(stream’s orientation)決定了所讀、寫的字符是單字節還是多字節的。
當一個流最初被創建時,它并沒有定向。
如若在未定向的流上使用一個單字節 I/O 函數(
若在未定向的流上使用一個單字節 I/O 函數,則將該流的定向設為字節定向的。
只有兩個函數可改變流的定向。
freopen 函數清除一個流的定向。
fwide 函數可用于設置流的定向。
#include
參數:
mode:
若 mode 值為負,fwide 將試圖使指定的流是字節定向的。
若 mode 值為正,fwide 將試圖使指定的流是寬定向的。
若 mode 值為 0,fwide 將不試圖設置流的定向,但返回標識該流定向的值。
返回值:
若流是寬定向的,返回正值。
若流是字節定向的,返回負值。
若流是未定向的,返回 0。
注意:
fwide 并不改變已定向流的定向。
fwide 無出錯返回。
在調用前先清除 errno,從返回時檢查 errno 的值。
當打開一個流時,標準 I/O 函數 fopen 返回一個指向 FILE 對象的指針。該對象通常是一個結構,它包含了標準 I/O 庫為管理該流需要的所有信息,包括用于實際 I/O 的文件描述符、指向用于該流緩沖區的指針、緩沖區的長度、當前在緩沖區中的字符數以及出錯標志等。
5.3 標準輸入、標準輸出和標準錯誤
對一個進程預定義了 3 個流,并且這 3 個流可以自動地被進程使用(
標準輸入:文件指針 stdin ——文件描述符 STDIN_FILENO
標準輸出:文件指針 stdout ——文件描述符 STDOUT_FILENO
標準錯誤:文件指針 stderr ——文件描述符 STDERR_FILENO
5.4 緩沖
標準 I/O 庫提供緩沖的目的是盡可能減少使用 read 和 write 調用的次數。它也對每個 I/O 流自動地進行緩沖管理,從而避免了應用程序需要考慮這一點所帶來的麻煩。
標準 I/O 流提供 3 種類型的緩沖:
全緩沖
在填滿標準 I/O 緩沖區后才進行實際 I/O 操作。對于駐留在磁盤上的文件通常是由標準 I/O 庫實施全緩沖的。在一個流上執行第一次 I/O 操作時,相關標準 I/O 函數通常調用 malloc 獲得需使用的緩沖區。
術語沖洗說明標準 I/O 緩沖區的寫操作。緩沖區可由標準 I/O 例程自動地沖洗,或者可以調用函數 fflush 沖洗一個流。
行緩沖
當在輸入和輸出中遇到換行符時,標準 I/O 庫執行 I/O 操作。這允許我們一次輸出一個字符(用標準 I/O 函數 fputc),但只有在寫了一行之后才進行實際 I/O 操作。當流涉及一個終端時(如標注輸入和標準輸出),通常使用行緩沖。
行緩沖的限制:
因為標準 I/O 庫用來收集每一行的緩沖區的長度是固定的,所以只要填滿了緩沖區,那么即使還沒有寫一個換行符,也進行 I/O 操作。
任何時候只要通過標準 I/O 庫要求從(a)一個不帶緩沖的流,或者(b)一個行緩沖的流得到輸入數據,那么就會沖洗所有行緩沖輸出流。
不帶緩沖
標準 I/O 庫不對字符進行緩沖存儲。
標準錯誤流 stderr 通常是不帶緩沖的,這就使得出錯信息可以盡快顯示除了,而不管它們是否含有一個換行符。
ISO C 要求下列緩沖特征:
當且僅當標準輸入和標準輸出并不指向交互式設備時,它們才是全緩沖的。
標準錯誤絕不會是全緩沖的。
很多系統默認使用下列類型的緩沖:
標準錯誤是不帶緩沖的。
若是指向終端設備的流,則是行緩沖的;否則是全緩沖的。
對任何一個給定的流,如果我們并不喜歡這些系統默認,則可調用下列兩個函數中的一個更改緩沖類型:
#include
參數:
fp:被打開的文件對象的指針
buf:為了帶緩沖進行 I/O,buf 必須指向一個長度為 BUFSIZ 的緩沖區(該常量定義在
如果 buf 為 NULL,關閉緩沖。
如果 buf 非 NULL,通常設定該流為全緩沖的。但是如果該流與一個設備終端相關,那么某些系統也可將其設置為行緩沖的。
setvbuf 中,如果 buf 為 NULL 而該流是帶緩沖的,標準 I/O 庫將自動的為該流分配適當長度的緩沖區。
mode:緩沖類型
_IOFBF:全緩沖
_IOLBF:行緩沖
_IONBF:不帶緩沖
size:緩沖區的長度
返回值:
若成功,返回 0。
若出錯,返回非 0。
任何時候,我們都可以強制沖洗一個流:
#include
返回值:
若成功,返回 0。
若出錯,返回 EOF。
此函數使該流所有未寫的數據都被傳送至內核。
作為一種特殊情形,如若 fp 是 NULL,則此函數將導致所有輸出流被沖洗。
沖洗是雙向的:
輸入流 —> 用戶緩沖區。
輸出流 —> 內核 —> 磁盤或者終端。
沖洗并不是立即寫到磁盤文件中,沖洗只是負責數據傳到內核。
5.5 打開流
下列 3 個函數打開一個標準 I/O 流。
#include
區別:
fopen 函數打開路徑名為 pathname 的一個指定的文件。
freopen 函數在一個指定的流上打開一個指定的文件,如若該流已經打開,則先關閉該流。若流已經定向,則使用 freopen 清楚該定向。此函數一般用于將一個指定的文件打開為一個預定義的流:標準輸入、標準輸出或標準錯誤。
fdopen 函數取一個已有的文件描述符,并使一個標準的 I/O 流與該描述符相結合。此函數常用于由創建管道和網絡通信信道函數返回的描述符。因為這些特殊類型的文件不能用標準 I/O 函數 fopen 打開,所以我們必須先調用設備專用函數以獲得一個文件描述符,然后用 fdopen 使一個標準 I/O 流與該描述符相結合。
參數:
type:指定對該 I/O 流的、讀寫方式,ISO C 規定該參數可以有 15 種不同的值
使用字符 b 作為 type 的一部分,使得標準 I/O 系統可以區分文本和二進制文件。因為 UNIX 內核并不對著兩種文件進行區分,所以在 UNIX 系統環境下指定字符 b 作為 type 的一部分實際上并無作用。
對于 fdopen,因為該描述符已被打開,所以 fdopen 為寫而打開并不截斷該文件,標準 I/O 追加寫方式也不能用于創建該文件。
當用追加寫類型打開一個文件后,每次寫都將數據寫到文件的當前尾端處。如果有多個進程用標準 I/O 追加寫方式打開同一文件,那么來自每個進程的數據都將正確地寫到文件中。
打開一個流的 6 中不同的的方式:
返回值:
若成功,返回文件指針。
若出錯,返回 NULL。
調用 flose 關閉一個打開的流:
#include
返回值:
若成功,返回 0。
若出錯,返回 EOF。
在該文件被關閉之前,沖洗緩沖中的輸出數據,緩沖區中的任何輸入數據被丟棄,如果標準 I/O 庫已經為該流自動分配了一個緩沖區,則釋放此緩沖區。
當一個進程正常終止時(直接調用 exit 函數,或從 main 函數返回),所有帶未寫緩沖數據的標準 I/O 流都被沖洗,所有打開的標準 I/O 流都被關閉。
5.6 讀和寫流
一旦打開了流,可在 3 中不同類型的非格式化 I/O 中進行選擇,對其進行讀、寫操作:
每次一個字符的 I/O。
一次讀或寫一個字符,如果流是帶緩沖的,則標準 I/O 函數處理所有緩沖。
每次一行的 I/O。
fgets 和 fputs
直接 I/O。
fread 和 fwrite
每次 I/O 操作讀或寫某種數量的對象,而每個對象具有指定的長度。
輸入函數
以下 3 個函數可用于一次讀一個字符:
#include
區別:
函數 getchar 等同于 getc(stdin)。
getc 可被實現為宏,而 fgetc 不能實現為宏。
返回值:
若成功,返回下一個字符。
若已到達文件尾端或出錯,返回 EOF。
為了區分出錯還是到到文件尾端,必須調用 ferror 或 feof:
#include
返回值:
若條件為真,返回非 0(真);
否則,返回 0(假)。
在大多數實現中,為每個流在 FILE 對象中維護了兩個標志:
出錯標志;
文件結束標志。
調用 clearerr 可以清楚這兩個標志。
從流中讀取數據以后,可以調用 ungetc 將字符再押送回流中:
#include
返回值:
若成功,返回 c。
若出錯,返回 EOF。
回送的字符,不一定是上次讀到的字符。
不能回送 EOF,但是當文件已經到達尾端時,仍可以回送一個字符,下次讀將返回該字符,再讀則返回 EOF。能這樣做的原因是,一次成功的 ungetc 調用會清除該流的文件結束標志。
用 ungetc 壓送回字符時,并沒有將它們寫到底層文件中或設備上,只是將它們寫回標準 I/O 庫的流緩沖區中。
輸出函數
對應于上述的每個輸入函數都有一個輸出函數:
#include
區別:
putchar(c) 等同于 putc(c, stdout。
putc 可被實現為宏,而 fputc 不能實現為宏。
返回值:
若成功,返回 c。
若出錯,返回 EOF。
5.7 每次一行 I/O
下面 2 個函數提供每次輸入一行的功能:
#include
區別:
gets 從標準輸入讀,而 fgets 從指定的流讀。
gets 不將換行符存入緩沖區中,而 fgets 存入。
gets 不推薦使用,原因是調用者在使用 gets 時不能指定緩沖區的長度,可能造成緩沖區溢出。
返回值:
若成功,返回 buf。
若已到達文件尾端或出錯,返回 EOF。
這兩個函數都指定了緩沖區的地址,讀入的行將送入其中。
fputs 和 puts 提供每次輸出一行的功能:
#include
區別:
fputs 將一個以 null 字節終止的字符串寫到指定的流,尾端的終止符 null 不寫出。
這并不一定是每次輸出一行,因為字符串不需要換行符作為最后一個非 null 字節。
puts 將一個以 null 字節終止的字符串寫到標準輸出,終止符不寫出。但是 puts 隨后又將一個換行符寫到標準輸出。
同樣避免使用,以免需要記住它在最后是否添加了一個換行符。
返回值:
若成功,返回非負值。
若出錯,返回 EOF。
5.8 標準 I/O 的效率
使用標準 I/O 例程的一個優點是無需考慮緩沖及最佳 I/O 長度的選擇。
使用每次一行的 I/O 版本的速度大約是每次一個字符版本速度的兩倍。
標準 I/O 庫與直接調用 read 和 write 函數相比并不慢很多。對于大多數比較復雜的應用程序,最主要的用戶 CPU 時間是由應用本身的各種處理消耗的,而不是由標準 I/O 例程消耗的。
5.9 二進制 I/O
下列兩個函數執行二進制 I/O 操作:
#include
參數:
ptr:存放二進制數據對象的緩沖區地址
size:單個二進制數據對象的字節數(比如一個 struct 的大小)
nobj:二進制數據對象的數量
fp:打開的文件對象指針
返回值:讀或寫的對象數
對于讀,如果出錯或到達文件尾端,則此數字可以少于 nobj。在這種情況下,應該調用 ferror 或 feof 判斷究竟是哪一種情況。
對于寫,如果返回值少于所要求的 nobj,則出錯。
使用二進制 I/O 的基本問題是,它只能用于讀在同一系統上已寫的數據。
現在,很多異構系統通過網絡相互連接起來,在一個系統上寫的數據,要在另一個系統上進行處理。在這種環境下,這兩個函數可能就不能正常工作,其原因是:
在一個結構中,統一成員的偏移量可能隨編譯程序和系統的不同而不同(由于不同的對齊要求)。
用來存儲多字節整數和浮點值的二進制格式在不同的系統結構間也可能不同。
5.10 定位流
有 3 種方法定位標準 I/O 流:
ftell 和 fseek 函數。
V7
假定文件的位置可以存放在一個長整型中。
ftello 和 fseeko 函數。
Single UNIX Specification
使用 off_t 數據類型代替了長整型。
fgetpos 和 fsetpos 函數。
ISO C
使用一個抽象數據類型 fpos_t 記錄文件的位置,這種數據類型可以根據需要定義為一個足夠大的數,用以記錄文件位置。
需要移植到非 UNIX 系統上運行的應用程序應當使用 fgetpos 和 fsetpos:
#include
參數:
offset:偏移量。
whence:偏移量的解釋方式
SEEK_SET:從文件的起始位置開始
SEEK_CUR:從文件的當前位置開始
SEEK_END:從文件的尾端開始
除了偏移量的類型是 off_t 而非 long 以外,ftello 函數與 ftell 相同,fseeko 函數與 fseek 相同:
#include
fgetpos 將文件位置指示器的當前值存入由 pos 指向的對象中。在以后調用 fsetpos 時,可以使用此值將流重新定位至該位置:
#include
5.11 格式化 I/O
格式化輸出
格式化輸出是由 5 個 printf 函數來處理的:
#include
區別:
printf 將格式化數據寫到標準輸出
fprintf 寫至指定的流
dprintf 寫至指定的文件描述符
不需要調用fopen 將文件描述符轉換為文件指針
sprintf 將格式化的字符送入數組 buf 中,并在該數組的尾端自動加一個 null 字節,但該字符不包括在返回值中
sprintf 函數可能會造成由 buf 指向的緩沖區的溢出,調用者有責任確保該緩沖區足夠大
snprintf 中緩沖區長度是一個顯示參數,超過緩沖區尾端寫的所有字符都被丟棄。與 sprintf 相同,該返回值不包括結尾的 null 字節。
參數:
format, ...:輸出的格式化字符串,一個轉換說明有 4 個可選擇的部分:
%[flags][fldwidth][precision][lenmodifier]convtype
flags
fldwidth:說明最小字段寬度,轉換后參數字符如果小于寬度,則多余字符位置用空格填充。
字段寬度是一個非負十進制數,或者是一個星號 *
precision:說明整型轉換后最少輸出數字位數、浮點數轉換后小數點后的最少位數、字符串轉換后最大字節數。
精度是一個點 .,后跟隨一個可選的非負十進制數或者一個星號 *
lenmodifier:說明參數長度:
convtype 不是可選的,它控制如何解釋參數:
buf:一個緩沖區的指針,格式化輸出到該緩沖區中
下列 5 種 printf 族的變體類似于上面的 5 種,但是可變參數表(…)替換成了 arg:
#include
格式化輸入
執行格式化輸入處理的是 3 個 scanf 函數:
#include
scanf 族用于分析輸入字符串,并將字符序列轉換成指定類型的變量。在格式之后的各參數包含了變量的地址,用轉換結構對這些變量賦值。
一個轉換說明有 3 個可選擇的部分:
%[*][fldwidth][m][lenmodifier]convtype:
*:用于抑制轉換,按照轉換說明的其余部分對輸入進行轉換,但轉換結果并不存放在參數中,而是拋棄。
fldwidth:說明最大寬度,即最大字符數
m:賦值分配符
可以用于 %c、%s 以及 %[轉換符],迫使內存緩沖區分配空間以接納轉換字符串
相關的參數必須是指針地址,分配的緩沖區地址必須復制給該指針
如果調用成功,該緩沖區不再使用時,由調用者負責通過調用 free 函數來釋放該緩沖區
lenmodifier:說明要轉換結果賦值的參數大小
convtype:與 printf 中相比,輸入中帶符號的可賦予無符號類型
與 printf 族相同,scanf 族也使用由
#include
5.12 實現細節
每個標準 I/O 流都有一個與其相關聯的文件描述符,可以對一個流調用 fileno 函數以獲得其描述符:
#include
fileno 不是 ISO C 標準部分,而是 POSIX.1 支持的擴展。
如果要調用 dup 或 fcntl 等函數,需要此函數。
5.13 臨時文件
ISO C 標準 I/O 庫提供了兩個函數以幫助創建臨時文件:
#include
參數:
ptr:指向存放臨時文件名的緩沖區的指針
若 ptr 為 NULL,則所產生的路徑名存放在一個靜態區中,指向該靜態區的指針作為函數值返回。后續調用 tmpnam 時,會重寫該靜態區。
若 ptr 不是 NULL,則認為它應該是指向長度至少是 L_tmpnam(
tmpnam 函數產生一個與現有文件名不同的一個有效路徑名字符串。每次調用它時,都產生一個不同的路徑名,最多調用次數是 TMP_MAX(
tmpfile 創建一個臨時二進制文件(類型 wb+),在關閉該文件或程序結束時將自動刪除這種文件。
tmpfile 函數經常使用的標準 UNIX 技術是先調用 tmpnam 產生一個唯一的路徑名,然后用該路徑名創建一個文件,并立即 unlink 它。
對一個文件解出鏈接并不刪除其內容,關閉該文件時才刪除其內容。而關閉文件可以是顯式的,也可以在程序終止時自動進行。
注意:UNIX 對二進制文件不進行特殊區分。
執行這兩個函數時,會出現:
warning: the use of `tmpnam' is dangerous, better use `mkstemp'
原因是 tmpnam 和 tmpfile 有一個缺點:
在返回唯一的路徑名和用該名字創建文件之間存在一個時間窗口,在這個時間窗口中,另一進程可以用相同的名字創建文件。
Single UNIX Specification 為處理臨時文件定義了另外兩個函數,即 mkdtemp 和 mkstemp,它們是 XSI 的擴展部分:
#include
區別:
mkdtemp 函數創建了一個目錄,該目錄有一個唯一的名字。
創建的目錄使用訪問權限位集:S_IRUSR | S_IWUSR | S_IXUSR。
調用進程的文件模式創建屏蔽字可以進一步限制這些權限。
mkstemp 函數創建了一個文件,該文件有一個唯一的名字。
返回的文件描述符以讀寫方式打開。
使用訪問權限位:S_IRUSR | S_IWUSR。
參數:
template:字符串
這個字符串是后 6 位設置為 XXXXXX 的路徑名。
函數將這些占位符替換成不同的字符來構建一個唯一的路徑名。
如果成功的話,這兩個函數將修改 template 字符串反應臨時文件的名字。
5.14 內存流
標準 I/O 庫把數據緩存在內存中,因此每次一字符和每次一行的 I/O 更有效。我們也可以通過調用 setbuf 或 setvbuf 函數讓 I/O 庫使用我們自己的緩沖區。
內存流:
SUSv4 中支持。
一種標準 I/O 流,雖然仍使用 FILE 指針進行訪問,但其實并沒有底層文件。
所有的 I/O 都是通過在緩沖區與主存直接來回傳送字節來完成的。
即便這些流看起來像文件流,它們的某些特征使其更適用于字符串操作。
有 3 個函數可用于內存流的創建,第一個是 fmemopen 函數:
#include
參數:
buf:指向緩沖區的開始位置
size:指定了緩沖區大小的字節數
如果 buf 參數為 NULL,函數分配 size 字節數的緩沖區,并當流關閉時釋放緩沖區。
type:控制如何使用流
以追加方式打開內存流時,如果緩沖區中不存在 null 字節,則當前位置設為緩沖區結尾的后一個字節。
如果 buf 是 NULL,則打開流進行讀或者寫都沒有任何意義。
因為此時緩沖區是通過 fmemopen 分配的,沒有辦法找到緩沖區的地址。
追加 null 需要滿足兩個條件:
增加流緩沖區中數據量。
調用 fclose、fflush、fseek、fseeko、fsetpos 。
用于創建內存流的其他兩個函數分別是 open_memstream 和 open_wmemstream:
#include
區別:
open_memstream 函數創建的流是面向字節的
open_wmemstream 函數創建的流是面向寬字節的
與 fmemopen 相比:
創建的流只能寫打開;
不能指定自己的緩沖區,但可以分別通過 bufp 和 sizep 參數訪問緩沖區地址和大小;
緩沖區地址和長度只有在調用 fclose 或 fflush 才有效;
這些值只有在下一次流寫入或調用 fclose 前才有效,因為緩沖區可以增長,可能需要重新分配,如果出現這種情況,緩沖區的內存地址在下一次調用 fclose 或 fflush 會改變;
關閉流后需要自行釋放緩沖區;
對流添加字節會增加緩沖區大小。
參數:
bufp:指向緩沖區地址的指針(用于返回緩沖區地址)
sizep:指向緩沖區大小的指針(用于返回緩沖區大小)
因為避免了緩沖區溢出,內存流非常適用于創建字符串。因為內存流只訪問主存,不訪問磁盤上的文件,所以對于把標準 I/O 流作為參數用于臨時文件的函數來說,會有很大的性能提升。
5.15 標準 I/O 的替代軟件
標準 I/O 庫的一個不足之處是效率不高,這與它需要復制的數據量有關:
當使用每次一行的函數 fgets 和 fputs 時,通常需要復制兩次數據:
在內核和標準 I/O 緩沖區之間(當調用 read 和 write 時)
在標準 I/O 緩沖區和用戶程序中的行緩沖區之間
Linux Unix
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。