APUE學習筆記——標準I/O

  今天我們圍繞標準I/O做一些詳細的討論。
  首先,我們先來看一些重要的概念。

流和文件指針

  文件I/O操作都是針對文件描述符進行的,相對的,標準I/O的操作都是圍繞一種叫做流(stream)的東西進行的,當使用標準 I/O 庫打開或創建一個文件時,我們就已使一個流與這個文件相關聯,通過流的讀入和輸出完成所需要的 I/O操作。
  用fopen打開一個流會返回一個指向FILE對象的指針,即文件指針,FILE對象通常是一個結構,包含了標準I/O庫爲管理該流需要的所有信息,如用於實際I/O的文件描述符、指向用於該流緩衝區的指針、緩衝區長度、出錯標誌等。
  系統爲每個進程預定義了3個可以自動被使用的流:標準輸入、標準輸出和標準錯誤,這3個標準I/O流通過預定義的文件指針stdin、stdout、stderr加以引用。

緩衝

  標準I/O庫提供緩衝的目的是儘可能減少使用read和write系統調用的次數,緩衝類型有三種:
  (1)全緩衝:填滿標準I/O緩衝區後才進行實際I/O操作,磁盤中的文件通常是全緩衝,術語沖洗(flush)說明標準I/O緩衝區的寫操作。
  (2)行緩衝:在輸入和輸出中遇到換行符或行緩衝區被填滿時執行實際I/O操作,當流涉及一個終端(如標準輸入和標準輸出)時,通常使用行緩衝。
  (3)不帶緩衝:標準I/O不對字符進行緩衝存儲,標準錯誤stderr通常默認爲不帶緩衝的。
ISO C規定
  當且僅當stdin和stdout不指向交互式設備時,他們纔是全緩衝。
  stderr決不會是全緩衝。

Linux系統默認
  stderr是不帶緩衝的。
  若流指向終端設備,則是行緩衝,否則爲全緩衝。
  
  對於一個給定的已經打開且尚未執行任何操作的流,我們可以調用setbuf和setvbuf來更改系統默認的緩衝類型。

#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只提供兩種功能——打開或關閉由第一個參數fp指定的流的緩衝機制。若爲打開緩衝,第二個參數buf必須指向一個長度爲BUFSIZ的緩衝區,BUFSIZ定義在stdio.h中,至於調用函數之後究竟是全緩衝還是行緩衝,那就取決於該流是與終端相關還是與文件相關了;若想關閉該流的緩衝,只需把buf設爲NULL即可。
  setvbuf功能就要強大一些,它多了兩個參數mode和size,不僅可以實現setbuf的功能,還可以在打開緩衝時由mode指定具體的指定緩衝類型,由size指定緩衝區的長度。

mode參數 緩衝類型
_IOFBF 全緩衝
_IOLBF 行緩衝
_IONBF 不帶緩衝

  關於這兩個函數更詳細的動作,可以看下下面這張表,這裏就不再多費口舌了。
  這裏寫圖片描述
關鍵字restrict:
  大家可能都注意到了,這兩個函數的參數中都帶有restrict這個關鍵字,查了下發現是C99新增的,它只用於限定指針,作用是告訴編譯器,所有修改該指針所指向內容的操作都必須通過該指針進行,而不能通過其它途徑(其它變量或指針)來修改。這樣做的好處是能幫助編譯器進行更好的代碼優化,生成更有效率的彙編代碼。
  


  接下來,我們圍繞對流的操作介紹一些函數,包括打開和關閉流、讀和寫流。

打開和關閉流

1、打開流
  以下三個函數可以打開一個標準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*/ 

  三個函數的區別:
  (1)fopen:打開路徑名爲pathname的一個指定文件
  (2)freopen:在一個指定的流上打開一個指定的文件,該函數一般用於將一個指定的文件打開爲一個預定義的流,也就是前面說的標準輸入、標準輸出和標準錯誤。
  (3)fdopen:讀取一個現有的文件描述符,並使一個標準I/O流與其結合,常用於由創建管道和網絡通信通道函數返回的描述符。

  其中,type參數可以用來指定對該流的讀寫方式,如下圖所示:
這裏寫圖片描述

2、關閉流

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

  當我們使用完一個文件之後,需要調用 fclose函數關閉該文件、釋放相關的資源,否則會造成內存泄漏,但在文件關閉前,系統還會進行一些操作,如沖洗緩衝區中輸出數據、丟棄緩衝區中的所有輸入數據、釋放爲流分配的緩衝區等。

讀和寫流

  讀寫操作分成兩大類:非格式化I/O、格式化I/O
  非格式化I/O又包括三種:每次一個字符I/O、每次一行I/O、每次一個結構I/O。 
(一)非格式化I/O
1、每次一個字符的I/O

#include <stdio.h>

/*輸入函數*/
int getc(FILE *fp);
int fgetc(FILE *fp);
int getchar(void);   /*等同於getc(stdin)*/

     /*三個函數返回值:若成功,返回下一個字符;若已到達文件尾或出錯,返回EOF*/

/*對應的輸出函數*/
int putc(int c, FILE *fp);
int fputc(int c, FILE *fp);
int putchar(int c);

     /*三個函數返回值:若成功,返回c;若出錯,返回EOF*/

  關於三個輸入函數的返回值,注意這樣一句話:若已到達文件尾或出錯,返回EOF,所以如果想知道是哪一種情況,可以調用ferror或feof進行判斷。

#include <stdio.h>

int ferror(FILE *fp);
int feof(FILE *fp);

     /*兩個函數返回值:若條件爲真,返回真;否則,返回假*/ 

  那麼,ferror和feof是怎麼知道條件是否爲真呢?原來系統爲每個流在FILE對象中維護了兩個標誌:出錯標誌和文件結束標誌,所以當把FILE指針作爲參數傳遞給這兩個函數時,就可以通過這兩個標誌來判斷條件是真還是假了。

2、每次一行I/O

#include <stdio.h>

/*buf:緩衝區地址; n:緩衝區長度; fp:指定的流*/
char *fgets(char *restrict buf, int n, FILE *restrict fp);
char *gets(char *buf);

         /*兩個函數返回值:若成功,返回buf;若已到達文件尾或出錯,返回NULL*/ 

int fputs(const char *restrict str, FILE *restrict fp);
int puts(const char *str);

         /*兩個函數返回值:若成功,返回非負值;若出錯,返回EOF*/ 

  我們先來分析兩個輸入函數:
  fgets和gets都指定了緩衝區的地址,讀入的行送入其中,只不過gets從標準輸入讀,而fgets從指定的流讀而已。
  gets函數並不檢查輸入行的長度是否超過緩衝區長度,因此有緩衝區溢出的危險,歷史上的蠕蟲病毒就是利用這個漏洞做的,所以gets一般不推薦使用。fgets彌補了gets的缺點,我們必須給它指定緩衝區長度n,當輸入行長度超過緩衝區長度時就會出錯。
  關於fgets、緩衝區長度、輸入行長度要注意一個小問題:
  fgets讀取輸入行直到遇到換行符,注意fgets也讀入換行符,而緩衝區總以null字節結尾,所以輸入行包括換行符在內的字符數不能超過n-1,也就是說除換行符外的實際字符數最多爲n-2,否則fgets返回一個不完整的行,該行的剩餘部分會在下一次調用fgets時接着讀取。
  測試代碼
  

#include <stdio.h>
#include <stdlib.h>

#define MAXLINE 20

int main()
{
    char buf[MAXLINE];

    if (fgets(buf, MAXLINE, stdin) != NULL)   
        if (fputs(buf, stdout) == EOF)
            printf("output error\n");

    if (ferror(stdin))
        printf("input error\n");

    exit(0);
}

運行結果可看出,緩衝區長度20,第一次輸入18個字符,fgets將這些字符與換行符一起讀入,fputs輸出有換行;第二次輸入19字符,fgets沒有讀入換行符,fpus輸出無換行。

  分析完了輸入,咱們再來看輸出。
  puts將一個字符串寫到標準輸出,尾部終止符’\0’不寫出,隨後puts會補一個換行符到標準輸出,也就是說puts會自動換行。
  fputs將一個字符串寫到指定的流,尾部終止符’\0’不寫出,但並不一定是每次輸出一行,因爲字符串不需要換行符作爲最後一個非NULL字節

3、二進制I/O(每次讀寫一個結構)

  該方式可一次讀寫一個完整的結構,通過fread和fwrite實現。

#include <stdio.h>

size_t fread(void *restrict ptr, size_t size, size_t nobj, FILE *restrict fp);

sizt_t fwrite(const void *restrict ptr, size_t size, size_t nobj, FILE *restrict fp);

 /*兩個函數返回值:讀或寫的對象數*/
 /*對於讀,若出錯或到達文件尾,返回值可少於nobj,需調用ferror或feof判斷是哪一種*/
 /*對於寫,若返回值少於要求的nobj,則爲出錯*/

  這兩個函數有以下常見用法:
  (1)讀或寫一個二進制數組:例如將一個數組的第2~5個元素寫到一文件上,可以使用如下代碼。其中,size爲每個數組元素的長度,nobj爲欲寫的元素個數

int data[10];

if (fwrite(&data[1], sizeof(int), 4, fp) != 4)
    printf("fwrite error\n");

  (2)讀或寫一個結構:如下代碼,其中,size爲結構的長度,nobj爲1(要寫的結構體個數)

struct
{
    short count;
    long total;
    char name[NAMESIZE];
} item;

if (fwrite(&item, sizeof(item), 1, fp) != 1)
     printf("fwrite error\n");

  (3)將(1)(2)結合起來就可以讀或寫一個結構數組:代碼如下,其中size是結構的sizeof,nobj是數組的元素個數。

/*定義一個結構數組*/
struct student
{
    char name[20];
    char sex[2];
    int age;
    char address[100];
} student[40];

if (fwrite(student, sizeof(struct student), 40, fp) != 40)
     printf("fwrite error\n");

(二)格式化I/O
1、格式化輸出

#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, ...);

int sprintf(char *restrict buf, const char *restrict format, ...);
int snprintf(char *restrict buf, size_t n,
       const char *restrict format, ...);

  我們來比較一下這5個函數:
  printf將格式化數據寫到標準輸出,注意它的返回值是成功打印的字符數(不包括\0字符)
  fdprintf將格式化數據寫到指定的流,dprintf寫到指定的文件描述符,這兩個函數的返回值也是成功打印的字符數(不包括\0字符);
  sprintf將格式化數據寫到數組buf中,並在數組尾端自動加一個\0,若成功,函數返回寫入數組的字符數(不包括爲數組自動添加的\0),否則返回負值。
  與gets類似,sprintf也有可能造成緩衝區溢出,所以有了snprintf,需要顯示指定緩衝區長度n,超過緩衝區尾端的所有字符都被丟棄。如果n足夠大,返回寫入buf的字符數,否則返回負值。
  
2、格式化輸入

#include <stdio.h>

int scanf(const char *restrict format, ...);
int fscasnf(FILE *restrict fp, const char *restrict format, ...);
int sscanf(char *restrict buf, const char *restrict format, ...);

  這三個函數的區別和上面的printf函數族類似,大家比較着看下就行了,這裏不再詳細闡述了。


  最後再來看下面這個函數:

#include <stdio.h>
int fileno(FILE *fp);

  每個標準I/O流都有一個與其相關聯的文件描述符,可以調用fileno獲得其描述符並返回。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章