C語言的本質(24)——C標準庫之輸入與輸出(下)

尹成發表於2014-07-17

 

4、讀寫二進位制檔案

C語言還提供了用於整塊資料的讀寫函式。可用來讀寫一組資料,如一個陣列元素,一個結構變數的值等。

讀資料塊函式呼叫的一般形式為:

   fread(buffer,size,count,fp);

寫資料塊函式呼叫的一般形式為:

   fwrite(buffer,size,count,fp);

其中:

buffer:是一個指標,在fread函式中,它表示存放輸入資料的首地址。在fwrite函式中,它表示存放輸出資料的首地址。

size:表示資料塊的位元組數。

count:表示要讀寫的資料塊塊數。

fp:表示檔案指標。

 

例如:

   fread(fa,4,5,fp);

其意義是從fp所指的檔案中,每次讀4個位元組(一個實數)送入實陣列fa中,連續讀5次,即讀5個實數到fa中。


從鍵盤輸入兩個學生資料,寫入一個檔案中,再讀出這兩個學生的資料顯示在螢幕上。

#include<stdio.h>
struct stu{
   char name[10];
   int num;
   int age;
   char addr[15];
}boya[2],boyb[2],*pp,*qq;
int main(void)
{
   FILE *fp;
   char ch;
   int i;
   pp=boya;
   qq=boyb;
   if((fp=fopen("~/stu_list","wb+"))==NULL){
       printf("Cannot open file strike any key exit!");
       getch();
       exit(1);
    }
   printf("input data\n");
   for(i=0;i<2;i++,pp++)
       scanf("%s%d%d%s",pp->name,&pp->num,&pp->age,pp->addr);
   pp=boya;
   fwrite(pp,sizeof(struct stu),2,fp);
   rewind(fp);
   fread(qq,sizeof(struct stu),2,fp);
   printf("\n\nname\tnumber     age      addr\n");
   for(i=0;i<2;i++,qq++)
       printf("%s\t%5d%7d    %s\n",qq->name,qq->num,qq->age,qq->addr);
   fclose(fp);
}


本例程式定義了一個結構stu,說明了兩個結構陣列boya和boyb以及兩個結構指標變數pp和qq。pp指向boya,qq指向boyb。程式第14行以讀寫方式開啟二進位制檔案“stu_list”,輸入二個學生資料之後,寫入該檔案中,然後把檔案內部位置指標移到檔案首,讀出兩塊學生資料後,在螢幕上顯示。

  

5、檔案讀寫位置控制

前面介紹的對檔案的讀寫方式都是順序讀寫,即讀寫檔案只能從頭開始,順序讀寫各個資料。但在實際問題中常要求只讀寫檔案中某一指定的部分。為了解決這個問題可移動檔案內部的位置指標到需要讀寫的位置,再進行讀寫,這種讀寫稱為隨機讀寫。

實現隨機讀寫的關鍵是要按要求移動位置指標,這稱為檔案的定位。

移動檔案內部位置指標的函式主要有兩個,即rewind()和fseek()。


rewind函式的呼叫形式為:

   rewind(檔案指標);

它的功能是把檔案內部的位置指標移到檔案首。

下面主要介紹fseek函式。fseek函式用來移動檔案內部位置指標,其呼叫形式為:

   fseek(檔案指標,位移量,起始點);

其中:

“檔案指標”指向被移動的檔案。

“位移量”表示移動的位元組數,要求位移量是long型資料,以便在檔案長度大於64KB 時不會出錯。當用常量表示位移量時,要求加字尾“L”。

“起始點”表示從何處開始計算位移量,規定的起始點有三種:檔案首,當前位置和檔案尾。

其表示方法如下表:

起始點             表示符號       數字表示

檔案首             SEEK_SET        0

當前位置         SEEK_CUR      1

檔案末尾         SEEK_END      2

 

例如:

   fseek(fp,100L,0);

其意義是把位置指標移到離檔案首100個位元組處。

 

還要說明的是fseek函式一般用於二進位制檔案。在文字檔案中由於要進行轉換,故往往計算的位置會出現錯誤。

檔案的隨機讀寫

 

在移動位置指標之後,即可用前面介紹的任一種讀寫函式進行讀寫。由於一般是讀寫一個資料據塊,因此常用fread和fwrite函式。下面用例題來說明檔案的隨機讀寫。

 

#include<stdio.h>
struct stu{
   char name[10];
   int num;
   int age;
   char addr[15];
}boy,*qq;
main(){
   FILE *fp;
   char ch;
   int i=1;
   qq=&boy;
   if((fp=fopen("~/stu_list","rb"))==NULL){
       printf("Cannot open file strike any key exit!");
       getch();
       exit(1);
    }
   rewind(fp);
   fseek(fp,i*sizeof(struct stu),0);
   fread(qq,sizeof(struct stu),1,fp);
   printf("\n\nname\tnumber age addr\n");
   printf("%s\t%5d %7d %s\n",qq->name,qq->num,qq->age,qq->addr);
}

檔案stu_list已由上個例子的程式建立,本程式用隨機讀出的方法讀出第二個學生的資料。程式中定義boy為stu型別變數,qq為指向boy的指標。以讀二進位制檔案方式開啟檔案,程式第19行移動檔案位置指標。其中的i值為1,表示從檔案頭開始,移動一個stu型別的長度,然後再讀出的資料即為第二個學生的資料。

 

6、錯誤處理

 

1) 檔案結束檢測函式feof函式

呼叫格式:

   feof(檔案指標);

功能:判斷檔案是否處於檔案結束位置,如檔案結束,則返回值為1,否則為0。

#include<stdio.h>
int main(void)
{
   FILE* fp;
    fp=fopen("file.bin","r");
   fgetc(fp);
         if(feof(fp))
                   printf("Wehave reached end of file\n");
         fclose(fp);
         return0;
}


feof(fp)有兩個返回值:如果遇到檔案結束,函式feof(fp)的值為非零值,否則為0。

EOF是文字檔案結束的標誌。在文字檔案中,資料是以字元的ASCⅡ程式碼值的形式存放,普通字元的ASCⅡ程式碼的範圍是32到127(十進位制),EOF的16進位制程式碼為0x1A(十進位制為26),因此可以用EOF作為檔案結束標誌。[1]

當把資料以二進位制形式存放到檔案中時,就會有-1值的出現,因此不能採用EOF作為二進位制檔案的結束標誌。為解決這一個問題,ASCI C提供一個feof函式,用來判斷檔案是否結束。feof函式既可用以判斷二進位制檔案又可用以判斷文字檔案。

C語言的“feof()”函式和資料庫中“eof()”函式的運作是完全不同的。資料庫中“eof()”函式讀取當前指標的位置,“C”語言的“feof()”函式返回的是最後一次“讀操作的內容”。多年來把“位置和內容”相混,從而造成了對這一概念的似是而非。

那麼,位置和內容到底有何不同呢?舉個簡單的例子,比如有人說“你走到火車的最後一節車箱”這就是位置。而如果說“請你一直向後走,摸到鐵軌結束”這就是內容。也就是說用內容來判斷會“多走一節”。這就是完全依賴於“while(!feof(FP)){...}”進行檔案複製時,目標文件總會比源文件“多出一些”的原因。

 

2) 讀寫檔案出錯檢測函式

ferror函式呼叫格式:

   ferror(檔案指標);

在呼叫各種輸入輸出函式(如 putc.getc.fread.fwrite等)時,如果出現錯誤,除了函式返回值有所反映外,還可以用ferror函式檢查。它的一般呼叫形式為 ferror(fp);如果ferror返回值為0(假),表示未出錯。如果返回一個非零值,表示出錯。應該注意,對同一個檔案每一次呼叫輸入輸出函式,均產生一個新的ferror函式值,因此,應當在呼叫一個輸入輸出函式後立即檢查ferror函式的值,否則資訊會丟失。在執行fopen函式時,ferror函式的初始值自動置為0。

 

3) 檔案出錯標誌和檔案結束標誌置0函式

clearerr函式呼叫格式:

   clearerr(檔案指標);

clearerr的作用是使檔案錯誤標誌和檔案結束標誌置為0。假設在呼叫一個輸入輸出函式時出現了錯誤,ferror函式值為一個非零值。在呼叫clearerr(fp)後,ferror(fp)的值變為0。

只要出現錯誤標誌,就一直保留,直到對同一檔案呼叫clearerr函式或rewind函式,或任何一個輸入輸出函式。

#include<stdio.h>
int main(void)
{
         FILE*fp;
         charch;
         fp=fopen("file.bin","w");
         ch=fgetc(fp);
         printf("%c\n",ch);
         if(ferror(fp))
         {
                   printf("Errorreading from file.bin\n");
                   clearerr(fp);
         }
         fclose(fp);
         return0;
}

C標準庫的I/O緩衝區

使用者程式呼叫C標準I/O庫函式讀寫檔案或裝置,而這些庫函式要通過系統呼叫把讀寫請求傳給核心,最終由核心驅動磁碟或裝置完成I/O操作。C標準庫為每個開啟的檔案分配一個I/O緩衝區以加速讀寫操作,通過檔案的FILE結構體可以找到這個緩衝區,使用者呼叫讀寫函式大多數時候都在I/O緩衝區中讀寫,只有少數時候需要把讀寫請求傳給核心。以fgetc/fputc為例,當使用者程式第一次呼叫fgetc讀一個位元組時,fgetc函式可能通過系統呼叫進入核心讀1K位元組到I/O緩衝區中,然後返回I/O緩衝區中的第一個位元組給使用者,把讀寫位置指向I/O緩衝區中的第二個字元,以後使用者再調fgetc,就直接從I/O緩衝區中讀取,而不需要進核心了,當使用者把這1K位元組都讀完之後,再次呼叫fgetc時,fgetc函式會再次進入核心讀1K位元組到I/O緩衝區中。

C標準庫之所以會從核心預讀一些資料放在I/O緩衝區中,是希望使用者程式隨後要用到這些資料,C標準庫的I/O緩衝區也在使用者空間,直接從使用者空間讀取資料比進核心讀資料要快得多。另一方面,使用者程式呼叫fputc通常只是寫到I/O緩衝區中,這樣fputc函式可以很快地返回,如果I/O緩衝區寫滿了,fputc就通過系統呼叫把I/O緩衝區中的資料傳給核心,核心最終把資料寫回磁碟。有時候使用者程式希望把I/O緩衝區中的資料立刻傳給核心,讓核心寫回裝置,這稱為Flush操作,對應的庫函式是fflush,fclose函式在關閉檔案之前也會做Flush操作。

 

C標準庫的I/O緩衝區有三種型別:全緩衝、行緩衝和無緩衝。當使用者程式呼叫庫函式做寫操作時,不同型別的緩衝區具有不同的特性。

 

全緩衝:如果緩衝區寫滿了就寫回核心。常規檔案通常是全緩衝的。

行緩衝:如果使用者程式寫的資料中有換行符就把這一行寫回核心,或者如果緩衝區寫滿了就寫回核心。標準輸入和標準輸出對應終端裝置時通常是行緩衝的。

無緩衝:使用者程式每次調庫函式做寫操作都要通過系統呼叫寫回核心。標準錯誤輸出通常是無緩衝的,這樣使用者程式產生的錯誤資訊可以儘快輸出到裝置。

 

下面通過一個簡單的例子證明標準輸出對應終端裝置時是行緩衝的。

 

#include <stdio.h>
 
int main()
{
         printf("helloworld");
         while(1);
         return0;
}

執行這個程式,會發現hello world並沒有列印到螢幕上。用Ctrl-C終止它,去掉程式中的while(1);語句再試一次:

 

$ ./a.out
hello world$

hello world被列印到螢幕上,後面直接跟Shell提示符,中間沒有換行。

 

我們知道main函式被啟動程式碼這樣呼叫:exit(main(argc, argv));。main函式return時啟動程式碼會呼叫exit,exit函式首先關閉所有尚未關閉的FILE*指標(關閉之前要做Flush操作),然後通過_exit系統呼叫進入核心退出當前程式。

 在上面的例子中,由於標準輸出是行緩衝的,printf("hello world");列印的字串中沒有換行符,所以只把字串寫到標準輸出的I/O緩衝區中而沒有寫回核心(寫到終端裝置),如果敲Ctrl-C,程式是異常終止的,並沒有呼叫exit,也就沒有機會Flush I/O緩衝區,因此字串最終沒有列印到螢幕上。如果把列印語句改成printf("hello world\n");,有換行符,就會立刻寫到終端裝置,或者如果把while(1);去掉也可以寫到終端裝置,因為程式退出時會呼叫exitFlush所有I/O緩衝區。在本書的其它例子中,printf列印的字串末尾都有換行符,以保證字串在printf呼叫結束時就寫到終端裝置。

我們再做個實驗,在程式中直接呼叫_exit退出。

 

#include <stdio.h>
#include <unistd.h>
 
int main()
{
         printf("helloworld");
         _exit(0);
}

結果也不會把字串列印到螢幕上,如果把_exit呼叫改成exit就可以列印到螢幕上。

除了寫滿緩衝區、寫入換行符之外,行緩衝還有一種情況會自動做Flush操作。如果:使用者程式呼叫庫函式從無緩衝的檔案中讀取或者從行緩衝的檔案中讀取,並且這次讀操作會引發系統呼叫從核心讀取資料。那麼在讀取之前會自動Flush所有行緩衝。例如:

 

#include <stdio.h>
#include <unistd.h>
 
int main(void)
{
         charbuf[20];
         printf("Pleaseinput a line: ");
         fgets(buf,20, stdin);
         return0;
}

雖然呼叫printf並不會把字串寫到裝置,但緊接著呼叫fgets讀一個行緩衝的檔案(標準輸入),在讀取之前會自動Flush所有行緩衝,包括標準輸出。

如果使用者程式不想完全依賴於自動的Flush操作,可以調fflush函式手動做Flush操作。

 

int fflush(FILE *stream);

返回值:成功返回0,出錯返回EOF並設定errno對前面的例子再稍加改動:

#include <stdio.h>
 
int main()
{
         printf("helloworld");
         fflush(stdout);
         while(1);
}

雖然字串中沒有換行,但使用者程式呼叫fflush強制寫回核心,因此也能在螢幕上列印出字串。fflush函式用於確保資料寫回了核心,以免程式異常終止時丟失資料。作為一個特例,呼叫fflush(NULL)可以對所有開啟檔案的I/O緩衝區做Flush操作。

 

 

相關文章