1. 程式人生 > >UNIX環境高階程式設計(第三版) 第五章筆記

UNIX環境高階程式設計(第三版) 第五章筆記

5.2 流和物件

只有兩個函式可以改變流的定向: freopen函式清楚一個流的定向,fwide函式可用於設定一個流的定向。

#include <stdio.h>
#include <wchar.h>
int fwide(FILE *fp, int mode);
    return: 若流是寬定向的,返回正值,若是位元組定向的,放回負值;若是未定向的,返回0.
注意:它沒有出錯返回,所以呼叫前先清楚errno,從fwide返回後在檢查errno值。

#ifndef _FILE_DEFINED
struct _iobuf {
    char *_ptr; //檔案輸入的下一個位置
    int _cnt; //當前緩衝區的相對位置     char *_base; //指基礎位置(即是檔案的起始位置)     int _flag; //檔案標誌     int _file; //檔案描述符id     int _charbuf; //檢查緩衝區狀況,如果無緩衝區則不讀取     int _bufsiz; //檔案緩衝區大小     char *_tmpfname; //臨時檔名 }; typedef struct _iobuf FILE; #define _FILE_DEFINED #endif

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

為一個程序定義了三個檔案流: stdin,stdout,stderr(包含在<stdio.h>

),而檔案描述符有對應的三個: STDIN_FIENO,STDOUT_FILENO,STDERR_FILE(包含在<unistd.h>中)

5.4 緩衝

1、標準I/O庫提供緩衝的目的是儘可能減少使用read和write呼叫的次數。

2、標準I/O提供了三種類型緩衝: - 全緩衝: 在填滿標準I/O緩衝區後才進行實際I/O操作。 * 行緩衝: 在輸入和輸出中遇到換行符時才進行I/O操作。 - 不帶緩衝: 標準I/O庫不對字元進行換從儲存。

3、術語沖洗(flush)說明標準I/O緩衝區的寫操作 在UNIX環境中,flush由兩種意思 - (1)在標準I/O庫方面,flush(沖洗)意味著將緩衝區中的內容寫到磁碟上(該緩衝區可能只是部分填滿的)。 - (2)在終端驅動程式方面,flush(刷清)表示丟棄已儲存在緩衝區中的資料。

4、對於行緩衝有兩個限制: - (1)因為標準I/O庫用來收集每一行的緩衝區的長度是固定的,所以只要填滿了緩衝區,那麼即使還沒有寫一個換行符,也進行I/O操作。 - (2)任何時候只要通過標準I/O庫要求從(a),(b)得到輸入資料,那麼就會沖洗所有行緩衝輸入流。

(a)一個不帶緩衝的流 (b)一個行緩衝的流(它從核心請求需要資料)

5、標準錯誤流stderr通常是不帶緩衝的

6、ISO C要求緩衝的特徵:

  • 當且僅當標準輸入和標準輸出並不指向互動式裝置時,他們才是全緩衝的。
  • 標註錯誤標準錯誤絕不會時全緩衝的

7、很多系統預設使用下列型別的緩衝:

  • 標準錯誤是不帶緩衝的
  • 若是指向終端裝置的流,則是行緩衝的;否則時全緩衝的。

8、對任何一個給定的流,如果自己想定自己的緩衝型別,可用

#include <stdio.h>
void setbuf(FILE *restrict fp, char *restrict buf);
int setvbuf(FILE *restrict fp, char *restrict buf, int mode, size_t size);
    return: 0; error: !0

(1) 使用setbuf函式開啟或關閉緩衝機制。為了帶緩衝進行I/O,引數buf必須指向一個長度為BUFSIZ(定義在

#include <stdio.h>
int fflush(FILE *fp);
    return: 0; error: EOF。

此函式使該流所有未寫的資料都被傳送至核心,作為一種特殊情形,如若fp是NULL,則此函式將導致所有輸出流被沖洗。

5.5 開啟流

1、下列3個函式開啟一個標準I/O流

#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 *restrict type);
     3個函式的返回值:若成功,返回檔案指標;若出錯,返回NULL。

3個函式的區別如下:

  • (1)fopen函式開啟路徑名為pathname的一個指定的檔案
  • (2)freopen函式在一個指定的流上開啟一個指定的檔案,如若流已經開啟,則先關閉該流。 若該流已經定向,則使用freopen清楚該定向。
  • (3)fdopen函式取一個已有的檔案描述符,並使一個標準的I/O流與該描述符相結合。

2、type引數指定對該I/O流的讀、寫方式。 3、對於fdopen,type引數的已有稍有區別。

  • (1)因為該描述符已被開啟,所以fdopen為寫而開啟並不截斷該檔案。fdopen函式不能截斷它為寫開啟的任一檔案。
  • (2)另外,標準I/O追加寫方式也不能用於建立檔案(因為一個描述符引用一個檔案,則該檔案一定存在)。

4、當讀和寫型別開啟一個檔案時,(type中+號),具有下列限制

  • (1)如果中間沒有 fflush、fseek、fsetpos或rewind,則在輸出的後面不能直接跟隨輸入。
  • (2)如果中間沒有fseek、fsetpos或rewind,或者一個輸入操作沒有到達檔案尾端,則在輸入操作之後不能直接跟隨輸出。

5、開啟一個流的6種不同的方式: 6、注意,在指定w或a型別建立一個新檔案時,我們無法說明該檔案的訪問許可權位。可以通過調整umask值來限制這些許可權。

7、否則按系統預設,流被開啟時是全緩衝的。若流引用終端裝置,則該流是行緩衝的。

5.6 讀和寫流

1、一旦打開了流,則可在3種不同型別的格式化I/O中進行選擇,對其進行讀、寫操作:

  • (1)每次一個字元的I/O。一次讀或寫一個字元,如果流是帶緩衝的,則標準I/O函式處理所有緩衝。
  • (2)每次一行的I/O。如果想要一次讀或寫一行,則使用fgets和fputs。每行都以一個換行符終止。
  • (3)直接I/O。fread和fwrite函式支援這種型別的I/O。每次I/O操作讀或寫某種數量的物件,而每個物件具有指定的長度。

2、輸入函式:

(1)以下3個函式可用於一次讀一個字元。

#include <stdio.h>
int getc(FILE *fp);
int fgetc(FILE *fp);
int getchar(void);
    3個函式的返回值:若成功,返回下一個字元;若已達到檔案尾端或出錯,返回EOF

(2)函式getchar等同於getc(stdin)。前兩個函式的區別是,getc可被實現為巨集,而fgetc不能實現為巨集。這意味著幾點:

1. getc的引數不應當是具有副作用的表示式,因為它可能會被計算多次。
2. 因為fgetc一定是一個函式,所以得到其地址。可以作為函式指標傳遞給另一個函式。
3. 呼叫fgetc所需的時間很可能比呼叫getc更長,因為呼叫函式所需的時間長於呼叫巨集。

(3)這3個函式在返回下一個字元時,將其unsigned char型別轉換為int型別 1. 說明為無符號的理由是,如果最高位是1也不會使返回值為負。 2. 要求整型返回值的理由是,這樣就可以返回所有可能的字元值再加上一個已出錯或已到達檔案尾端的指示值。在

#include <stdio.h>
int ferror(FILE *fp);
int feof(FILE *fp);
   兩個函式返回值:若條件為真,返回非0(真);否則,返回0(假)
void clearerr(FILE *fp);        

1)大多數實現中,為每個流在FILE物件中維護了兩個標誌:

1, 出錯標誌
2, 檔案結束標誌

2)呼叫clearerr可以清楚這兩個標誌。

4、從流中讀取資料以後,可以呼叫ungetc將字元再壓送回流中

#include <stdio.h>
int ungetc(int c, FILE *fp);
   返回值:若成功,返回c;若出錯,返回EOF。
  • (1)壓送回到流中的字元以後又可從流中讀出,但讀出字元的順序與壓送回去的順序相反。
  • (2)回送的字元,不一定必須是上一次讀到的字元。不能回送EOF。但是當已經到達檔案尾端時,仍可以回送一個字元。下次讀將返回該字元,再讀則返回EOF。之所以能這樣做的原因是,一次成功的ungetc呼叫會清除該流的檔案結束標誌。
  • (3)用ungetc壓送回字元時,並沒有將它們寫到底層檔案中或裝置上,只是將它們寫回標準I/O庫的流緩衝區中。

5、輸出函式 對應於上面所述的每個輸入函式都有一個輸出函式

#include <stdio.h>
int putc(int c, FILE *fp);
int fputc(int c, FILE *fp);
int putchar(int c);
   3個函式返回值:若成功,返回c;若出錯,返回EOF。

putc可被實現為巨集,而fputc不能實現為巨集。

5.7 每一次I/O

下面兩個函式提供每次輸入一行的功能。 雖然ISO C提供了gets,但已被棄用,推薦使用fgets。

#include <stdio.h>
char *fgets(char *restrict buf, int n, FILE *restrict fp);
char *gets(char *buf);
    return: buf; error:NULL

fputs和puts提供每次輸出一行的功能:

#include <stdio.h>
int fputs(const char *restrict str, FILE *restrict fp);
int puts(const char *str);
    return: 非負值; error: EOF.

函式fputs將一個以null位元組終止的字串寫到指定的流,尾端的終止符null不寫出。puts雖不像gets那樣不安全,但也應避免使用,並且他在輸出時最後添加了一個換行符。如果經常使用fgets和fputs,那麼就會熟知每行終止處我們必須自己處理換行符。

5.8 標準I/O的效率

  • 1、用getc和putc將標準輸入複製到標準輸出
  • 2、讀、寫行版本:
  • 3、以fgetc和read相比,系統呼叫與普通函式呼叫相比花費更多的時間

5.9 二進位制I/O

1、下列兩個函式以執行二進位制I/O操作

#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);
    兩個函式的返回值:讀或寫的物件數。

size為結構長度,nobj為需要物件的個數。

2、對於讀,如果出錯或到達檔案尾端,則此數字可以少於nobj。在這種情況下,應該呼叫ferror或feof以判斷究竟是哪一種情況。

3、對於寫,如果返回值少於所要求的nobj,則出錯。

4、二進位制I/O的基本問題是,它只能用於讀在同一系統上已寫的資料。原因是: - (1)在一個結構中,統一成員的偏移量可能隨編譯程式和變異系統的不同而不同(由於不同的對齊要求)。 - (2)用來儲存多位元組數的腹地安置的二進位制格式在不同的系統結構間也可能不同。

5.10 定位流

1、有3種定位標準I/O流: - (1)ftell和seek函式,它們假定檔案的位置可以存放在一個長整型中。 - (2)ftello和fseeko函式。它們使用off_t作為檔案偏移量的資料型別。 - (3)fgetpos和fsetpos函式,它們使用fpos_t記錄檔案的位置。這種資料足夠大以記錄檔案位置。 需要移植到非UNIX系統上執行的應用程式應當使用fgetpos和fsetpos。

#include <stdio.h>
long ftell(FILE *fp);
   返回值:若成功,返回當前檔案指示;若出錯,返回-1Lint fseek(FILE *fp, long offset, int whence);
   返回值:若陳宮,返回0;若出錯,返回-1void rewind(FILE *fp);

2、對於一個二進位制檔案,其檔案指示器是從檔案起始位置開始度量,並以位元組為度量單位的。

3、為了用fseek定位一個二進位制檔案,必須指定一個位元組offset。whence值與lseek函式的相同: - (1)SEEK_SET表示從檔案起始位置開始 - (2)SEEK_CUR表示當前檔案位置開始 - (3)SEEK_END表示從檔案尾端開始

4、為了定位一個文字檔案,whence一定要是SEEK_SET,而且offset只有兩種可能值:0(後退到檔案的起始位置),或是對該檔案的ftell所返回的值。

5、使用rewind函式也可將一個流設定到檔案的起始位置。

#include <stdio.h>
    void rewind(FILE * stream);

rewind()函式用於將檔案指標重新指向檔案的開頭,同時清除和檔案流相關的錯誤和eof標記,相當於呼叫fseek(stream, 0, SEEK_SET).

6、除了偏移量型別是off_t而非long以外,ftello函式與ftell相同,fseeko函式與fseek相同。

#include <stdio.h>
off_t   ftello(FILE *fp);
int fseeko(FILE *fp, off_t offset, int whence);
   返回值:若成功,返回0。若出錯,返回-1

7、fgetpos和fsetpos函式:

#include <stdio.h>
int fgetpos(FILE *restrict fp, fpos_t *restrict pos);
int fsetpos(FILE *restrict fp, const fpos_t *pos);
   兩個函式返回值:若成功,返回0;若出錯,返回非0.

5.11 格式化I/O

1、格式化輸出 格式化輸出是由5個printf函式來處理的。

#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, ...);
   3個函式返回值:若成功,返回輸出字元數;若出錯,返回負值。
int sprintf(char *restrict buf, const char *restrict format, ...);
   返回值:若成功,返回存入陣列的字元數;若編碼出錯,返回負值。
int snprintf(char *restrict buf, size_t n, const char *restrict format, ...);
   返回值:若緩衝區足夠大,返回將要存入陣列的字元數;若編碼出錯,返回負值。
  • (1)printf將格式化資料寫到標準輸出。
  • (2)fprintf寫至指定的流。
  • (3)dprintf寫至指定的檔案描述符
  • (4)sprintf將格式化字元送入陣列buf中。
  • (5)sprintf在該陣列尾端加上一個null位元組,但該字元不包括在返回值中。

2、sprintf函式可能會造成由buf指向的緩衝區的溢位。為了解決這種緩衝區溢位問題,引入了snprintf函式。

3、格式化說明控制其餘引數如何編寫: (1)轉換說明以百分號%開始。 (2)一個轉換說明有4個可選擇的部分:

flags各標誌如下: 這裡寫圖片描述

fldwidth說明最小欄位寬度:轉換後引數字元若小於寬度,則多餘字元位置用空格填充。 欄位寬度是一個非負十進位制數,或是一個星號(*)。

precssion說明整型轉換後最少輸出數字位數、浮點數轉換後小數點後的最少位數。字串轉換後最大位元組數。精度是一個點(.),其後跟隨一個可選的非負十進位制或一個星號(*)。

lenmodifier說明引數長度,其可能的值如下: 這裡寫圖片描述

convtype是不可選的。它控制如何解釋引數: 這裡寫圖片描述

4、下列5種printf族辯題類似於上面的5種,但是可變引數表(…)替換成了arg

#include <stdarg.h>
#include <stdio.h>
int vprintf(const char *restrict format, va_list arg);
int vfprintf(FILE *restrict fp, const char *restrict format, va_list arg);
int vdprintf(int fd, const char *restrict format, va_list arg);
   所有3個函式返回值:若成功,返回輸出字元數;若輸出出錯,返回負值。
int vsprintf(char *restrict buf, const char *restrict format, va_list arg);
int vsnprintf(char *restrict buf, size_t n, const char *restrict format, va_list arg);
    函式返回值:若緩衝區足夠大,返回存入陣列的字元數;若編碼出錯,返回負值。                          

5、格式化輸入 執行格式化輸入處理的是3個scanf函式

#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, ...);
   3個函式返回值:賦值的輸入項數;若輸入出錯或在任一轉換前到達檔案尾端,返回EOF。
  • (1)scanf族用於分析輸入字串,並將字元序列轉換成指定型別的變數。
  • (2)一個轉換說明由3個可選部分:

fldwith說明最大寬度(即最大字元數)。 lenmodifier說明要轉換結果賦值的引數大小。

在欄位寬度和長度修飾之間的可選項m是賦值分佩服。它可以用於%c、%s以及%[轉換符,迫使記憶體緩衝區分配空間以接納轉換字串。

scanf族函式支援的轉換型別: 這裡寫圖片描述

6、與printf族相同,scanf族也使用由

#include <stdarg.h>
#include <stdio.h>
int vscanf(const char *restrict format, va_list arg);
int vfscanf(FILE *restrict fp, const char *restrict format, va_list arg);
int vsscanf(const char *restrict buf, const char *restrict format, va_list arg);
   3個函式返回值:指定的輸入專案數;若輸入出錯或在任一轉換前檔案結束,返回EOF。

5.12 實現細節

1、每個標準I/O流都有一個與其相關的檔案描述符,可以對一個流呼叫fileno函式獲得其描述符。

#include <stdio.h>
int fileno(FILE *fp);
   返回值:與該流相關聯的檔案描述符

2、程式為3個標準流以及一個普通檔案相關聯的流列印有關緩衝的狀態資訊

5.13 臨時檔案

1、ISO C 標準I/O庫提供了兩個函式以幫助建立臨時檔案:

#include <stdio.h>
char *tmpnam(char *ptr);
    返回值:指向唯一路徑名的指標
FILE *tmpfile(void);                
    返回值:若成功,返回檔案指標;若出錯,返回NULL。 

(1)tmpnam 函式產生一個與現有檔案不同的一個有效路徑名字串。每次呼叫它時,都會產生一個不同的路徑名,最多呼叫次數是 TMP_MAX(定義在

3、Single UNIX Specification為了處理臨時檔案定義了另外兩個函式:mktemp和mksetup,它們是XSI的擴充套件部分。

#include <stdlib.h>
char *mkdtemp(char *template);
    返回值:若成功,返回指向目錄名的指標;若出錯,返回NULL
int mkstemp(char *template);            
   返回值:若成功,返回檔案描述符;若出錯,返回-1

(1)mkdtemp函式建立了一個目錄,該目錄有一個唯一的名字。 (2)mkstemp函式建立了一個檔案,該檔案有一個唯一的名字。 (3)名字是通過template字串進行選擇的,這個字串是後6位設定為XXXXXX的路徑名。函式將這些佔位符替換成不同的字元來構建一個唯一的路徑名。如果成功的話,這兩個函式將修改template字串反映臨時檔案的名字。 (4)由mkdtemp函式建立的目錄使用下列訪問許可權位集:

S_IRUSR | S_IWUSR | S_IXUSR

(5)mkstemp函式以唯一名字建立一個普通檔案病開啟該檔案,該函式的檔案描述符以讀寫方式開啟。由mkstemp建立檔案使用訪問許可權位

S_IRUSR | S_IWUSR。

(6)與tempfile不同,mkstemp建立的臨時檔案並不會自動刪除。必須自己對它解除連結。

(7)tmpnam和tempnam至少有一個缺點,在返回唯一的路徑名和使用名字建立檔案之間存在一個時間視窗。在這個時間視窗可能以相同的名字建立檔案。

5.14 記憶體流

1、有3個函式可以用於記憶體流的建立,第一個fmemopen函式:

#include <stdio.h>
FILE *fmemopen(void *restrict buf, size_t size, const char *restrict type);
                                        返回值:若成功,返回流指標;若錯誤,返回NULL。

(1)fmemopen函式允許呼叫者提供緩衝區用於記憶體流。buf引數指向緩衝區的開始位置,size引數指定了緩衝區大小的位元組數。 (2)如果buf引數為空,fmemopen函式分配size位元組數的緩衝區。在這種情況下,當流關閉時緩衝區會被釋放。

2、type引數控制如何使用流。

type 說明
r 或 rb 為讀而開啟
w 或 wb 為寫而開啟
a 或 ab 追加:為在第一個null位元組處寫而開啟
r+ 或 r+b 或 rb+ 為讀和寫而開啟
w+ 或 w+b 或 wb+ 把檔案截斷至0長,為讀和和寫而開啟
a+ 或 a+b 或 ab+ 追加:為在第一個null位元組處讀和寫而開啟

與基於檔案的標準I/O流的type引數取值,其中有些差別 - (1)無論何時以追加方式開啟記憶體流時,當前檔案位置設定為緩衝區的第一個null位元組。如果緩衝區中不存在null位元組,則當前位置就設為緩衝區結尾的後一個位元組。當流不是以追加方式開啟時,當前位置設為緩衝區的開始位置。 - (2)如果buf引數是一個null指標,開啟劉進行讀或者寫沒有任何意義。 - (3)任何時候需要增加緩衝區中資料以及呼叫fclose、fflush、fseek、fseeko以及fsetpos時都會在當前位置寫入一個null位元組。

3、程式反映了記憶體流的寫入是如何在我們提供的緩衝區上進行操作的。

4、建立記憶體流的其他兩個函式分別是open_memstream和open_wmemstream

#include <stdio.h>
FILE *open_memstream(char **bufp, size_t *sizep);
#include <wchar.h>
FILE *open_wmemstream(wchar_t **bufp, size_t *sizep);
                                    兩個函式的返回值:若成功,返回流指標;若出錯,返回NULL。

(1)open_memstream函式建立的流是面向位元組的,open_wmemstream函式建立的流是面向寬位元組的。

5、這兩個函式與fmemopen的不同在於: - (1)建立的流只能寫開啟 - (2)不能指定自己的緩衝區,但可以通過bufp和sizep引數訪問緩衝區地址和大小。 - (3)關閉流後需要自行釋放緩衝區。 - (4)對流新增位元組會增加緩衝區大小。

6、緩衝區地址和大小使用上由已寫原則: - (1)緩衝區地址和長度只有在呼叫fclose或fflush後才有效 - (2)這些值只有在下一次流寫入或呼叫fclose前才有效。因為緩衝區可以增長,可能需要重新分配。

7、因為避免了緩衝區溢位,記憶體劉非常適用於建立字串。因為記憶體流只能訪問主存,不訪問磁碟上的檔案,所以對於把標準I/O流作為引數用於臨時檔案的函式來說,會有很大的效能提升。

總結

  檔案I/O一章講了不帶緩衝的I/O,本章講的是帶緩衝的I/O。不帶緩衝針對的是核心的系統呼叫,而帶緩衝針對的是使用者空間的標準庫函式,是基於帶緩衝的I/O實現的。不帶緩衝的I/O通過檔案描述符的方式來引用一個檔案,而帶緩衝的I/O則通過檔案流(stream)的方式來引用檔案。至於為什麼要用流的方式,原因就是帶緩衝區,這樣檔案的讀寫就要經過緩衝區做緩衝,就像水流一樣。

  引入標準IO庫的目的是為了提高IO的效率,避免頻繁的進行read/write系統呼叫,而系統呼叫會消耗較多的資源。因此標準IO庫引入了IO快取,通過累積一定量的IO資料後,然後集中寫入到實際的檔案中來減少系統呼叫,從而提高IO效率。標準IO庫會自動管理內部的快取,不需要程式設計師介入。然而,也正是因為我們看不到標準IO庫的快取,有時候會給我們帶來一定的迷惑性。