其他的庫文件看起來沒有什麼實現層面的知識可以探究的,所以,直接來看stdio.h,stdlib.h,string.h。
1.茶余飯後的雜談,有趣的歷史
在過去的幾十年中,獨立於設備的輸入輸出模型得到了飛速的發展,標准C從這個改善的模型中獲益頗豐。
輸入輸出模塊
在20世紀60年代早期,FORTRAN IV被認為是獨立於機器的語言。但是如果不作任何改動,根本不可能在各種計算機體系結構中移動FORTRAN IV程序。可移植性的主要障礙是輸入輸出領域。在FORTRAN IV中,可以對FORTRAN IV代碼中間的I/O語句中對正在通信的設備進行命名。CARD 和 INPUT TAPE就不一樣。
之後,逐漸發展到使用邏輯單元號(LUN)來代替具體的設備名,從而在可執行的二進制卡片之前加入控制卡片,從而指定某個特殊的運行過程那些設備與特定的LUN相對應。這時候,獨立於設備的I/O時代來臨了。
設備獨立的進一步改善得益於標准外圍交換程序(peripheral interchange program,PIP)的進步。該程序允許指定源設備與目標設備的任意成,然後盡力執行兩個設備之間的拷貝操作。
進入UNIX。UNIX對所有文本流采用了標准內部形式,文本的每一行以換行符終止。這正是程序讀入文本時所期望的,也是程序輸出所產生的。假如這樣的約定不能滿足和UNIX機器相連的處理文本的外圍設備的需求,可以在系統的對外接口有些修改,不必修改任何內部代碼。UNIX提供了兩種機制來修正“對外接口”的文本流。首先的是一個通用的映射函數,它可以用任意的文本處理設備工作。可以用系統調用ioctl來設置或者測試一個具體設備的的各種參數。另一個修正文本流的機制是修改直接控制該設備的專門軟件。對於每一個UNIX可能需要控制的設備來說,用戶必須添加一個常駐UNIX的設備管理器。
當第一個C編譯器在UNIX平台上運行時,C語言就自然地繼承了它的宿主操作系統簡單的I/O模型。除了文本流的統一表示,還有其他一些優點。很久以前使用的LUNs在最近幾十年也慢慢地演變為稱為文件描述符或句柄的非常小的正整數。操作系統負責分發文件描述符。並且把所有的文件控制信息存儲在自己的專用內存中,而不是讓用戶去分配和維持文件和記錄控制塊以加重負擔。
為了簡化多數程序的運行管理,UNIX shell分配給每個運行的程序3個標准文件描述符,這些就是現在普通使用的標准輸入、標准輸出和標准錯誤流。(文本流)
UNIX不會阻止向任意打開的文件寫入任意的二進制編碼,或者從一個足夠大的地方把它們絲毫不變地讀取出來。(二進制流)
所以,UNIX消除了文本流(與人通信)和二進制流(與機器通信)之間的區別。
在相同的實現下,從一個二進制流讀入的數據應該和之前寫入到這個liu的數據相同,而文本流則不是。
關於文本流和二進制流的參考資料:http://www.embedu.org/Column/Column186.htm,有助於理解概念。
PS:流是一個操作系統層面的高度抽象的概念,它隱藏了I/O設備具體的實質,而將所有的I/O帶來的數據變化看做輸入的流入和流出,這樣,在操作系統層面為程序將各種I/O設備模擬成流的樣式,已經使這時的I/O模塊獨立而抽象了。可以看到,I/O模型發展的過程,就是其逐漸抽象統一的過程,這一點與語言的發展的歷程是相似的。
X3J11委員會在1983年開始召開會議為C起草ANSI標准。非UNIX系統的C廠商和那些UNIX用戶之間爭論了很長時間,因為UNIX用戶不能理解I/O為什麼要這麼麻煩(顯然,UNIX的文件結構和設備的管理機制保證了I/O模塊的簡潔性,這是相對於其他操作系統的優點)。這是一個很有教育意義的過程,這些爭論的一個重要的副產品就是更清楚地闡明了C支持的I/O模塊。
最終,委員會經過討論整潔的重要性和向下兼容的重要性之後,決定拋棄UNIX風格的原語。(主要平衡代碼效率和代碼簡潔性)
2.不識廬山真面露,包含的內容
類型:
FILE 它是一個對象類型,可以記錄控制流需要的所有信息,包括它的文件定位符、指向相關緩沖(如果有的話)的指針、記錄是否發生了讀/寫錯誤的錯誤提示符和記錄文件手否結束的文件結束符(用來控制流的FILE對象的地址可能很重要,不必使用FILE對象的副本來代替原始的對象進行服務。)
庫中的函數分兩類:
1.針對任意流的操作;
2.指定特定問文件流的操作;
兩者分別又有讀寫、文件定位、緩沖區控制等操作,可以完成對流的全方位操作,只要你能想到。
3.不畏浮雲遮望眼,看實現吧
有兩種設計決策對<stdio.h>的實現非常關鍵:
類型FILE:
------------------------------------------------------------------------------------------------------------------------------------------------------------
(此處純為個人理解,不為原書內容)
不管是二進制流還是文本流,C都是將文件當做連續的字節流在處理。該字節流的信息以及文件對應的文件描述符等都是需要存儲在FILE類型中的內容。
typedef *_ptr; _cnt; *_base; _flag; _file; _charbuf; _bufsiz; *_tmpfname; }FILE;
雖然不知道這些個變量是什麼意思,但看一下一些函數的實現可以勉強猜一猜。
1.FOPEN
FILE * _TSCHAR * _TSCHAR * , REG1 FILE * REG2 FILE * _ASSERTE(file != _ASSERTE(*file != _T( _ASSERTE(mode != _ASSERTE(*mode != _T( ((stream = _getstream()) == retval = /* _UNICODE */ retval = /* _UNICODE */ }
其中FILE *類型變量通過_getstream()獲得。所以,我們可以稍微看下_getstream()的實現。不過在看_getstream()的實現之前,有必要介紹C標准庫中關於IO控制塊(即FILE文件)的管理機制:
首先,在I/O控制塊中有三個特殊的控制塊,分別是標准輸入、標准輸出和標准錯誤流。其中標准輸入有其輸入大小的限制。
_INTERNAL_BUFSIZ 4096 _bufin[_INTERNAL_BUFSIZ];
可以看到我使用的這個版本的C標准庫中,標准輸入流的大小為:4096。我們可以簡單用代碼測試一下:
main( s[ ( i = ; i < ; i++ s[i] = scanf( (i = ; i < ; i++ ( s[i] == printf(,i+ }
輸入為5000個1,輸出結果為:4095。應該是有一個結束標志符的緣故。顯然,那些超出_INTERNAL_BUFSIZ的字符串部分被捨去了。
該C標准庫中,這個I/O控制塊通過一個FILE數組_iob和一個FILE **指針來進行管理。如下:
代碼塊1:
_NSTREAM_ 512 _IOB_ENTRIES 20 /* _WIN32 */ _NSTREAM_ 128 /* *MUST* match the value under ifdef _DLL! */ /* CRTDLL */ _NSTREAM_ 128 /* _DLL */ _NSTREAM_ 40 /* _MT */ _NSTREAM_ 20
代碼塊2:
FILE _iob[_IOB_ENTRIES] = , _bufin, _IOREAD | _IOYOURBUF, , , NULL, _IOWRT, , , , NULL, _IOWRT, , ,
代碼塊3:
_nstream = /* CRTDLL */ /* CRTDLL */
** __piob;
( (__piob = ( **)_calloc_crt( _nstream, ( *) )) === ( (__piob = ( **)_calloc_crt( _nstream, ( *))) == ( i = ; i < _IOB_ENTRIES ; i++= ( *)&_iob[i];
從代碼塊2中我們可以看到,FILE _iob[_IOB_ENTRIES]是一個FILE數組,其中預設了三種FILE類型,分別是stdin,stdout和stderr。因為不同平台下,I/O控制塊數量的大小至少為20(從_NSTREAM_的定義看出),所以_IOB_ENTRIES定義為20,作為前20個I/O控制塊。此處的值是不是20其實沒有什麼意義,只要這個數組能容納下3個預設的I/O控制塊,同時大小至於產生浪費空間的可能即可(大於20就有可能浪費)。
__piob是一個FILE **的二維指針,管理著一個FILE *的指針數組,用來指向陸續分配的I/O控制塊的地址,這個指針數組的大小最大為_NSTREAM_ = 512,可以測試一下這個數值。
#include <stdlib.h> #include <stdio.h> main( FILE *p[ file_name[ ( i = ; i < ; i++ itoa(i,file_name, p[i] = fopen(file_name, ( i = ; i< ; i++ }
可以看到在文件夾中建立了509個文件,加上預置的stdin、stdout、stderr正好為512個。
管理方式如圖:
現在,我們將目光移回,看下_getstream()的實現:
FILE * REG2 FILE *retval = REG1 ( i = ; i < _nstream ; i++ ( __piob[i] != ( !inuse( (FILE * ( inuse( (FILE * /* _MT */ retval = (FILE * ( (__piob[i] = _malloc_crt( (_FILEX) )) != defined (_MT) InitializeCriticalSection( &(((_FILEX *)__piob[i])-> EnterCriticalSection( &(((_FILEX *)__piob[i])-> /* defined (_MT) */ retval = (FILE * ( retval != retval->_flag = retval->_cnt = retval->_tmpfname = retval->_ptr = retval->_base = retval->_file = - /* _WIN32 */ defined (_M_MPPC) || defined (_M_M68K) REG1 FILE *stream = (; stream <= _lastiob; stream++ ( ! stream->_flag = stream->_cnt = stream->_tmpfname = stream->_ptr = stream->_base = stream->_file = - retval = /* defined (_M_MPPC) || defined (_M_M68K) */ /* _WIN32 */ }
顯然,在仍有FILE*指針可用的情況下,為第一個空閒的FILE *分配一片對應的FILE空間。即將新的stream納入到了整個I/O控制塊的管理中。
OK,我們再回到fopen函數中,在得到一個沒有使用過的I/O控制塊之後,顯然下一步要做的就是對這個I/O塊,根據設定的模式進行配置。此時,要調用到的就是_openfile函數。
在_openfile中,需要標記了stream._flag = streamflag;streamflag通過位標記了_IOREAD、_IOWRT當前所進行操作的類型。stream._file得到了一個int類型的文件標識符(通過更底層的open系列函數建立了文件標識符,同時決定了該數據流是文本流還是二進制流)。
那麼,我們可以得到FILE中兩個變量的意義了。