1. 程式人生 > >APUE第5章 標準I/O庫

APUE第5章 標準I/O庫

1、概述

標準I/O庫處理很多細節,如緩衝區分配、以優化的塊長度執行I/O等。這些處理使使用者不必擔心如何選擇使用正確的長度。本章深入瞭解I/O庫函式的操作。

2、流和FILE物件

對於所有I/O函式(見第3章)都圍繞檔案描述符的。當開啟一個檔案時,即返回一個檔案描述符,然後該檔案描述符就用於後續的I/O操作。

對於標準I/O庫,其操作是圍繞流進行的。當用標準I/O庫開啟或建立一個檔案時,已使一個流與一個檔案相關聯。

流的定向決定了所讀、寫的字元是單位元組還是多位元組的。當一個流最初建立時,它並沒有定向。如若在未定向的流上使用一個多位元組I/O函式(見<wchar.h>),則將該流的定向設定為寬定向的。若在未定向的流上使用一個單位元組I/O函式,則將該流的定向設為位元組定向的。

兩個函式,freopen清除一個流的定向;fwide函式可用於設定流的定向。

#include<stdio.h>
#include<wchar.h>
int fwide(FILE* fp, int mode);
		//返回值:若流是寬定向的,返回正值;若流是位元組定向的,返回負值;若流是未定向的,返回0

根據mode引數的不同值,fwide函式執行不同的工作。

若mode引數值為負,fwide將試圖使指定的流是位元組定向的。

若mode引數值為正,fwide將試圖使定的流是寬定向的。

若mode引數值為0,fwide將不試圖設定流的定向,但返回標識該流定向的值。

注意:fwide並不改變已定向流的定向。同時,fwide無出錯返回,所以需要利用全域性變數errno來檢測錯誤。

當開啟一個流時,標準I/O函式fopen返回一個指向FILE物件的指標。該物件通常是一個結構,它包含了標準I/O庫為管理該流需要的所有資訊,包括用於實際I/O的檔案描述符、指向用於流緩衝區的指標、緩衝區的長度、當前在緩衝區中的字元數以及出錯標誌等。型別FILE*稱為檔案指標。

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

3個流可以自動被程序使用,分別是標準輸入(stdin)、標準輸出(stdout)、標準錯誤(stderr),這三個文個把指標定義在標頭檔案<stdio.h>中。其分別與檔案描述符STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO相對應。

4、緩衝

i、緩衝型別

全緩衝:在填滿標準I/O緩衝區後才進行實際I/O操作。(駐留在磁碟上的檔案通常是由標準I/O庫實施全緩衝的)

        沖洗意味著將緩衝區中的內容寫至磁碟上(該緩衝區可能只是部分填滿)。刷清表示丟棄已儲存在緩衝區中的資料。

行緩衝:當在輸入和輸出中遇到換行符時,標準I/O庫執行I/O操作。(當流涉及一個終端時(如標準輸入和標準輸出),通常使用行緩衝)

        使用行緩衝的兩個限制:一、只要填滿緩衝區,即使還沒有寫一個換行符,也進行I/O操作。二、任何時候只要通過標準I/O庫要求從一個不帶緩衝的流,或一個行緩衝的流得到輸入資料,那麼就會沖洗所有行緩衝輸出流。

不帶緩衝:標準I/O庫不對字元進行緩衝儲存。(標準錯誤流stderr通常是不帶緩訓的)

ii、對任何一個給定的流,可通過以下兩個函式更改緩衝型別。

#include<stdio.h>
void setbuf(FILE* restrict fp, char* restrict buf);//設定緩衝
int setvbuf(FILE* restrict fp, char * restrict buf, int mode, size_t size);//設定緩衝,並指定緩衝型別
		//返回值:若成功,返回0;若出錯,返回非0

上述函式一定要在流已被開啟後呼叫(因為每個函式都要求一個有效的檔案指標作為它們的第一個引數),而且也應在對該流執行任何一個其他操作之前呼叫。

setbuf所設定的緩衝型別通常由系統決定,而使用setvbuf,可以精確說明所需的緩衝型別。通過mode引數實現,具體說明見下。

_IOFBF  全緩衝

_IOLBF  行緩衝

_IONBF  不帶緩衝

iii、使用fflush函式可認強制沖洗一個流。

#include<stdio.h>
int fflush(FILE* fp);
		//返回值:若成功,返回0;若出錯,返回EOF

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

5、開啟流

以下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* type);
		//返回值:若成功,返回檔案指標;若出錯,返回NULL

3個函式的區別如下:

a、fopen函式開啟路徑名為pathname的一個指定的檔案。

b、freopen函式在一個指定的流上開啟一個指定的檔案,若該流已經開啟,則先關閉訪該流。若該流已經定向,則使用freopen清除該定向。此函式一般用於將一個指定的檔案開啟為一個預定義的流:標準輸入、標準輸出或標準錯誤。

c、fdopen函式取一個已有的檔案描述符,並使一個標準的I/O流與該描述符相結合。

type引數指定對該I/O流的讀、寫方式,ISO C規定type引數可以有15種不同的值,如下表所示。

開啟標準I/O流的type引數
type 說明 open標誌
r或rb 為讀而開啟 O_RDONLY
w或wb 把檔案截斷至0長,或為寫而建立 O_WRONLY|O_CREAT|O_TRUNC
a或ab 追加;為在檔案尾寫而開啟,或為寫而建立 O_WRONLY|O_CREAT|O_APPEND
r+或r+b或rb+ 為讀和寫而開啟 O_RDWR
w+或w+b或wb+ 把檔案截斷至0長,或為讀和寫而開啟 O_RDWR|O_CREAT|O_TRUNC
a+或a+b或ab+ 為在檔案尾讀和寫而開啟或建立 O_RDWR|O_CREAT|O_APPEND

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

i、如果中間沒有fflush、fseek、fsetpos或rewind,則在輸出的後面不能直接跟隨輸入。

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

此處這點,理解的不是很透徹,難道是說,fseek->write->fseek->read這種情況下才能迴圈交替寫、讀操作????

呼叫fclose關閉一個開啟的流。

#include<stdio.h>
int fclose(FILE* fp);
		//返回值:若成功,返回0;若出錯,返回EOF

在該檔案被關閉之前,沖洗緩衝中的輸出資料。緩衝區中的任何輸入資料被丟棄。如果I/O庫已經為該流自動分配了一個緩衝區,則釋放此緩衝區。(自動分配的緩衝區,自動回收。)

6、讀和寫流、每次一行I/O

a、讀和寫流

對於開啟的流,可在3種不同型別的非格式化I/O中進行選擇,對其進行讀、寫操作。

i、每次一個字元的I/O。一次讀或寫一個字元,如果流是帶緩衝的,則標準I/O函式處理所有緩衝。(以字元為單位)

ii、每次一行的I/O。如果想發一次讀或寫一行,則使用fgets和fputs。每行都以一個換行符終止。當呼叫fgets時,應說明能處理的最大行長。(以行為單位)

iii、直接I/O。fread和fwrite函式支援此型別的I/O。每次I/O操作讀或寫某種數量的物件,而每個物件具有指定的長度。此兩函式常用於從二進位制檔案中每次讀或寫一個結構。

針對輸入函式

以下3個函式可用於一次讀或寫一個字元。

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

號外號外,返回值是下一個字元的asci碼的整型值哈!這是有原因的。(整型返回值,這樣就可以返回所有可能的字元值再加上一個已出錯或已到達檔案尾端的指示值。在<stdio.h>中的常量EOF被要求是一個負值,其值經常是-1)。

前兩個函式的主要區別:getc可被實現為巨集,而fgetc不能實現為巨集。以下幾點需要注意。

i、getc的引數不應當是具有副作用的表示式,因為它可能會被計算多次。(也要防止腦慘替換帶來的影響)

ii、因為fgetc一定是個函式,所以可以等到其地址。於是允許fgetc的地址作為一個引數傳送給另一個函式。

iii、呼叫fgetc所需時間很可能比呼叫getc要長,因為呼叫函式所需的時間通常長於呼叫巨集。

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

#include<stdio.h>
int ungetc(int c, FILE* fp);
		////返回值:若成功,返回c;若出錯,返回EOF

壓送回到流中的字元以後又可從流中讀出。但讀出字元的順序與壓送回的順序相反。同時要求實現提供一次只回送一個字元。不能期望一次能回送多個字元。

用ungetc壓送回字元時,並沒有將它們寫到底層檔案中或裝置上,只是將它們寫回標準I/O庫的流緩衝區中。

針對輸出函式

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

與輸入函式一樣,putchar(c)等同於putc(c,stdout),putc可被實現為巨集,而fputc不能實現為巨集。(需要注意點和getc/fgetc/getchar一樣)

以下程式碼是getc和putc函式應用的舉例。

#include "apue.h"

int
main(void)
{
	int		c;

	while ((c = getc(stdin)) != EOF)
		if (putc(c, stdout) == EOF)
			err_sys("output error");

	if (ferror(stdin))
		err_sys("input error");

	exit(0);
}

b、每次一行I/O

針對輸入函式(每次輸入一行)

#include<stdio.h>
char* fgets(char* restrict buf, int n, FILE* restrict fp);//將讀入的行,存放在buf中,並自動新增null位元組作為結尾符
		//返回值:若成功,返回buf;若已到達檔案尾端或出錯,返回NULL

fgets,必須指定緩衝的長度n。此函式一直讀到下一個換行符為止,但是不超過n-1個字元,讀入的字元被送入緩衝區。該緩衝區以null位元組結尾。若該行包括最後一個換行符的字元數超過n-1,則fgets只返回一個不完整的行,但是,緩衝區總是以null位元組結尾。對fgets的下一次呼叫會繼續該行。

針對輸出函式(每次輸出一行)

#include<stdio.h>
int fputs(const char* restrict str, FILE *restrict fp);//輸出以null位元組為終止標誌而不是以換行符
		//返回值:若成功,返回非負值;若出錯,返回EOF

fputs將一個以null位元組終止的字串寫到指定的流,尾端的終止符null不寫出。值得注意,並不一定是每次輸出一行,因為字串不需要換行符作為最後一個非null位元組。通常,在null位元組之前是一個換行符,但並不要求總是如此。

以下程式碼是fgets和fputs函式應用的舉例。

#include "apue.h"

int
main(void)
{
	char	buf[MAXLINE];

	while (fgets(buf, MAXLINE, stdin) != NULL)
		if (fputs(buf, stdout) == EOF)
			err_sys("output error");

	if (ferror(stdin))
		err_sys("input error");

	exit(0);
}

7、二進位制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);
		//兩個函式的返回值:讀或寫的物件數

常見用法如下舉例。

i、讀或寫一個二進位制陣列。例如,為了將一個浮點陣列的第2-5個元素寫至一檔案上,可如下實現:

float data[10];
if (fwrite(&data[2], sizeof(float), 4, fp) != 4)
	err_sys("fwrite error");

其中,指定size為每個陣列元素的長度,nobj為欲寫的元素個數。

ii、讀或寫一個結構。例如,可以編寫如下程式:

struct {
	short	count;
	long	total;
	char	name[NAMESIZE];
}item;
if (fwrite(&item, sizeof(item), 1, fp) != 1)
	err_sys("fwrite error");

其中,指定size為結構的長度,nobj為1(要寫的物件個數)。

將以上兩例子結合起來就可讀或寫一個結構陣列。為此,size應當是該結構的sizeof,nobj應是該陣列中的元素個數。

8、格式化I/O

a、格式化輸出函式(老生常談,此處只列舉,不再作進一步解釋)

#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, ...);//格式化的字串輸出到指定buffer
		//返回值:若成功,返回存入陣列的字元數;若編碼出錯,返回負值
int snprintf(char* restrict buf, size_t n, const char* restrict format, ...);
		//返回值:若緩衝區足夠大,返回將要存入陣列的字元數;若編碼出錯,返回負值

b、格式化輸入函式(同樣,不再作進一步解釋)

#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

9、臨時檔案

ISO C標準I/O庫提供了兩函式建立臨時檔案。

#include<stdio.h>
char* tmpnam(char* ptr);
		//返回值:指向唯一路徑名的指標
FILE* tmpfile(void);//產生的臨時檔案在關閉檔案或程式結束時,自動刪除該檔案
		//返回值:若成功,返回檔案指標;若出錯,返回NULL

tmpnam函式產一個與現有檔名不同的一個有效路徑名字串。每次呼叫它時,都產生一個不同的路徑名。最多呼叫次數是TMP_MAX。TMP_MAX定義在<stdio.h>中。

tmpfile建立一個臨時二進位制檔案(型別wb+),在關閉該檔案或程式結束時將自動刪除這種檔案。

以下程式碼是上述兩函式應用的舉例。

#include "apue.h"

int
main(void)
{
	char	name[L_tmpnam], line[MAXLINE];
	FILE	*fp;

	printf("%s\n", tmpnam(NULL));		/* first temp name */

	tmpnam(name);						/* second temp name */
	printf("%s\n", name);//輸出與前一個輸出不一樣,name也作為函式值返回

	if ((fp = tmpfile()) == NULL)		/* create temp file */
		err_sys("tmpfile error");
	fputs("one line of output\n", fp);	/* write to temp file *///將內容寫入fp所代表的檔案中
	rewind(fp);//目的是將流設定到檔案起始位置							/* then read it back */
	if (fgets(line, sizeof(line), fp) == NULL)//從fp所代表的檔案中獲取內容
		err_sys("fgets error");
	fputs(line, stdout);//將內容寫出到標準輸出上				/* print the line we wrote */

	exit(0);
}

以下是另外兩個處理臨時檔案的函式。

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

mkdtemp函式建立一個目錄,該目錄有一個唯一的名字;(訪問許可權為S_IRUSR|S_IWUSR|S_IXUSR)

mkstemp函式建立一個檔案,該檔案有一個唯一的名字。與tempfile不同,mkstemp建立的臨時檔案並不會自動刪除。如果希望從檔案系統名稱空間中刪除該檔案,需要主動呼叫unlink函式解除連線。(訪問許可權為S_IRUSR|S_IWUSR)

以下程式碼是mkstemp函式應用的舉例。

#include "apue.h"
#include <errno.h>

void make_temp(char *template);

int
main()
{
	char	good_template[] = "/tmp/dirXXXXXX";	/* right way */
	char	*bad_template = "/tmp/dirXXXXXX";	/* wrong way*/

	printf("trying to create first temp file...\n");
	make_temp(good_template);
	printf("trying to create second temp file...\n");
	make_temp(bad_template);
	exit(0);
}

void
make_temp(char *template)
{
	int			fd;
	struct stat	sbuf;

	if ((fd = mkstemp(template)) < 0)
		err_sys("can't create temp file");
	printf("temp name = %s\n", template);
	close(fd);
	if (stat(template, &sbuf) < 0) {
		if (errno == ENOENT)
			printf("file doesn't exist\n");
		else
			err_sys("stat failed");
	} else {
		printf("file exists\n");
		unlink(template);
	}
}