01——線程標準IO

一、文件與文件類型

1、文件定義

定義:文件(File)是一個具有符號名字的一組相關聯元素的有序序列。文件可以包含的內容十分廣泛,操作系統和用戶都可以將具有一定獨立功能的一個程序模塊、一組數據或一組文字命名爲一個文件。

文件名:這個數據有序序列集合(文件)的名稱。

2、文件的分類

文件由許多種,運行的方式也各有不同。在Windows中,我們是通過文件的後綴名來對文件分類的。例如.txt、.doc、.exe等。而在Linux系統中則不是,它不以文件後綴來區分文件的類型。

在Linux中,我們可以使用ls -l指令來查看文件的類型。在Linux系統中,文件主要有7種類型:

  1. - 普通文件 指ASCII文本文件、二進制文件以及硬鏈接文件
  2. d 目錄文件 包含若干文件或子目錄
  3. l 符號鏈接 只保留所指向文件的地址而非文件本身
  4. p 管道文件 用於進程間通信
  5. c 字符設備 原始的I/O設備文件,每次操作僅操作1個字符(例如鍵盤)
  6. b 塊設備 按塊I/O設備文件(例如硬盤)
  7. s 套接字 套接字是方便進程間通信的特殊文件,與管道不同的是套接字能通過網絡連接使不同的計算機的進程進行通信

3、Linux的文件目錄結構

Linux系統中文件採取樹形結構,即一個根目錄(/),包含下級目錄或文件的信息;子目錄又包含更下級的目錄或文件的信息。依次類推層層延伸,最終構成一棵樹。

//見附圖

 

Linux系統的每個目錄都有其特定的功能,這裏只簡單介紹一些主要目錄及其功能:

  • 目錄 功能說明
  • /etc 存放系統配置文件
  • /bin 存放常用指令
  • /sbin (root用戶的)存放指令目錄
  • /home 用戶主目錄,所有用戶p的文件默認建立在此目錄下(用戶工作目錄)
  • /boot 包含內核啓動文件
  • /dev 存放設備文件(與底層驅動交互)
  • /usr 存放應用程序
  • /mnt 掛載目錄
  • /root root用戶主目錄
  • /proc process的所寫,存放描述系統進程的詳細信息
  • /lib 存放常見庫文件
  • /lost+found 可以找到一些誤刪除或丟失的文件並恢復它們
  •  

二、系統調用與用戶編程接口(API)

系統調用(System Call)是由操作系統實現提供的所有系統調用所構成的程序接口的集合。是應用程序與操作系統間的接口與紐帶。

操作系統的主要功能是爲管理硬件資源和爲應用程序開發人員提供良好的環境來使應用程序具有良好的兼容性。爲了達到這個目的,內核提供一系列具備預定功能的函數,通過系統調用的接口呈現給用戶。當用戶訪問系統調用,系統調用把應用程序的請求傳遞給內核,調用相應的內核函數完成所需處理,將處理結果返回給應用程序。

應用程序編程接口(API,Application Programming Interface)是一些預定義的函數,目的是提供應用程序與開發人員基於軟件/硬件得以訪問一組例程的能力,而又無需訪問源碼或理解內部工作原理機制。

在實際開發應用程序的過程中,我們並不直接使用系統調用接口,而是使用用戶編程接口(API)。爲什麼呢?

1.系統調用功能非常簡單,有時無法滿足程序的需求。

2.不同操作系統的系統調用接口不兼容,若使用系統調用接口則程序移植工作量非常大。

用戶編程接口使用各種庫(在C語言中最主要的是C庫)中的函數。爲了提高編程效率,C庫中實現了很多函數。這些函數實現了許多常用功能供程序開發者調用。這樣一來,程序開發者無需自己編寫這些代碼,直接可以調用函數就能實現功能,提高了編碼效率和代碼複用率。

使用用戶編程接口還有一個好處:一定程度上解決了程序的可移植性(雖然C語言的可移植性仍沒有Java好)。幾乎所有的操作系統上都實現了C庫,因此使用C語言編寫的程序只需在不同的系統下重新編譯即可運行。

通常情況下,用戶編程接口API在實現時需要依賴系統調用接口。例如創建進程API函數fork()需要調用內核空間的sys_fork()系統調用。但是還有一些API無需調用任何系統調用。

在Linux中用戶編程接口遵循在Unix系統中最流行的應用編程編程標準POSIX標準。

/***************POSIX簡介*************************/

POSIX表示可移植操作系統接口(Portable Operating System Interface ,縮寫爲 POSIX ),POSIX標準定義了操作系統應該爲應用程序提供的接口標準,是IEEE爲要在各種UNIX操作系統上運行的軟件而定義的一系列API標準的總稱,其正式稱呼爲IEEE 1003,而國際標準名稱爲ISO/IEC 9945。

POSIX標準意在期望獲得源代碼級別的軟件可移植性。換句話說,爲一個POSIX兼容的操作系統編寫的程序,應該可以在任何其它的POSIX操作系統(即使是來自另一個廠商)上編譯執行。

/***************POSIX簡介end**********************/

標準I/O與文件I/O的區別:

1.文件I/O又稱爲低級磁盤I/O,遵循POSIX標準。任何兼容POSIX標準的操作系統都支持文件I/O。標準I/O又稱爲高級磁盤I/O,遵循ANSI C相關標準。只要開發環境有標準C庫,標準I/O就可以使用。

在Linux系統中使用GLIBC標準,它是標準C庫的超集,既支持ANSI C中定義的函數又支持POSIX中定義的函數。因此Linux下既可以使用標準I/O,也可以使用文件I/O。

2.通過文件I/O讀寫文件時,每次操作都會執行相關係統調用。這樣的好處是直接讀寫實際文件,壞處是頻繁的系統調用會增加系統開銷。標準I/O在文件I/O的基礎上封裝了緩衝機制,每次先操作緩衝區,必要時再訪問文件,從而減少了系統調用的次數。

3.文件I/O使用文件描述符打開操作一個文件,可以訪問不同類型的文件(例如普通文件、設備文件和管道文件等)而標準I/O使用FILE指針來表示一個打開的文件,通常只能訪問普通文件。

 

三、Linux標準I/O

1、標準I/O定義

標準I/O指的是ANSI C中定義的用於I/O操作的一系列函數。只要操作系統安裝了C庫,就可以調用標準I/O。換句話說,若程序使用標準I/O函數,那麼源代碼無需進行任何修改就可以在其他操作系統上編譯,具有更好的可移植性。

除此之外,由於標準I/O封裝了緩衝區,使得在讀寫文件的時候減少了系統調用的次數,提高了效率。在執行系統調用的時候,Linux必須從用戶態切換到內核態,在內核中處理相應的請求,然後再返回用戶態。如果頻繁地執行系統調用則會增加這種開銷。標準I/O爲了減少這種開銷,採取緩衝機制,爲用戶空間創建緩衝區,讀寫時優先操作緩衝區,在必須訪問文件時(例如緩衝區滿、強制刷新、文件讀寫結束等情況)再通過系統調用將緩衝區的數據讀寫實際文件中,從而避免了系統調用的次數。

2、流的定義

標準I/O的核心對象是流。當用標準I/O打開一個文件時,就會創建一個FILE結構體描述該文件。我們把這個FILE結構體稱爲“流”。標準I/O函數都是基於流進行各種操作的。

/**********************流的分類***********************/

流的分類分爲文本流二進制流兩種:

文本流:文本流是由字符文件組成的序列,每一行包含0個或多個字符並以'\n'結尾。在流處理過程中所有數據以字符形式出現,'\n'被當做回車符CR和換行符LF兩個字符處理,即'\n'ASCII碼存儲形式是0x0D和0x0A。當輸出時,0x0D和0x0A轉換成'\n'

二進制流:二進制流是未經處理的字節組成的序列,在流處理過程中把數據當做二進制序列,若流中有字符則把字符當做ASCII碼的二進制數表示。'\n'不進行變換。

例如:2016在文本流中和二進制流中的數據類型不同:

文本流:2016---->'2''0''1''6'---->50 48 49 54

二進制流:2016-->數字2016--->0000011111010001

在Linux/Unix系統中,文本流與二進制流沒有差異,但是在Windows中稍有差異,所以標準C庫定義了兩種流。

/**********************流的分類end********************/

 

在使用標準I/O操作文件的時候,每個被程序使用的文件都會在內存中開闢一塊區域,用來存放與文件相關的屬性信息,這些信息存放在一個FILE類型的結構體中,FILE類型的結構體是由系統定義的一個結構體:

typedef struct

{

short level;//緩衝區滿/空的狀態

unsigned flags;//文件狀態標誌

char fd;//文件描述符

unsigned char hold;//如緩衝區無內容則不讀取字符

short bsize;//緩衝區的大小

unsigned char *buffer;//數據緩衝區的位置

unsigned char *curp;//指針當前的指向

unsigned istemp;//臨時文件指示器

short token;//用於有效性檢查

}FILE;

在標準I/O中,常用FILE類型的結構體指針FILE*來操作文件。

/***************對“流”與“文件”的關係的討論************/

在初學C語言文件I/O相關知識點時,經常會陷入“什麼是流?”“什麼是文件?”“流和文件有什麼關係(區別)?”等問題。在這裏對“流”與“文件”進行簡單的討論,基礎好的同學可跳過該部分

《C Primer Plus》(有興趣的推薦看一下)上說,C程序處理一個流而不是直接處理文件。但是後面的解釋十分抽象:『流(stream)是一個理想化的數據流,實際輸入或輸出映射到這個數據流』。

本質上來說,文件本身就是數據的有序序列,因此我們操作文件時是按順序依次操作該文件的數據。我們可以想象一個傳送帶,傳送帶上的產品就是待操作數據。當我們對文件內的數據進行操作時,已操作的數據從當前位置離開,待操作的數據不斷流向當前位置,這樣文件內的數據就產生了流動的感覺,這個“傳送帶”就是C語言內“流”的原型。

我們打開一個流,就意味着將該流與該文件進行了連接(即讓文件內的“產品”放上“傳送帶”),關閉流將斷開流與該文件的連接。此時我們對流進行操作就是對文件進行操作。通常在不產生歧義的情況下,“文件”與“流”可以不予區分。

在程序開始執行的時候,操作系統會默認開啓stdin、stdout、stderr三個文件,這三個文件作爲輸入、輸出、輸出錯誤的流,這樣我們使用諸如scanf()、printf()等就無需手動加載流,方便使用。

當我們使用fopen()打開一個文件的時候,該文件會返回一個FILE*類型的指針,例如:

FILE *fp = fopen("hello.txt","w")

此時文件hello.txt與流指針fp就關聯了起來,對fp的操作就相當於對文件hello.txt進行操作。

/***************對“流”與“文件”的關係的討論end*********/

 

在標準I/O中預定義了三塊緩衝區:stdin、stdout、stderr,分別代表標準輸入流、標準輸出流、標準輸出錯誤流

見下:流的名稱 程序中使用

  1. 標準輸入 stdin 
  2. 標準輸出 stdout 
  3. 標準錯誤輸出 stderr 

標準I/O中的流的緩衝類型有三種:

1.全緩衝:這種情況下,當緩衝區被填滿後才進行實際的I/O操作。對於存放在磁盤上的普通文件用標準I/O打開時默認是全緩衝的。當緩衝區滿或者執行刷新緩衝區(fflush)操作纔會進行磁盤操作。(多看幾遍)

2.行緩衝:這種情況下,當在輸入/輸出中遇到換行符時執行I/O操作。標準輸入/輸出流(stdin/stdout)就是使用行緩衝。

3.無緩衝:不使用緩衝區進行I/O操作,即對流的讀寫操作會立即操作實際文件。標準出錯流(stderr)是不帶緩衝的,這就使得當發生錯誤時,錯誤信息能第一時間顯示在終端上,而不管錯誤信息是否包含換行符。

示例1.1:stdout使用行緩衝形式

效果:不會立即打印內容,而是等待'\n'或者緩衝區滿才輸出。

fflush(stdin):刷新緩衝區把緩衝裏面的東西丟掉

fflush(stdout):刷新緩衝區把緩衝裏面的東西輸出到設備上去

#include<stdio.h>

int main()

{

while(1)

{

printf("Hello World");

sleep(1);//延時1秒

}

return 0;

}

示例1.2:stdout使用行緩衝形式

效果:當添加了'\n'之後,會正確地輸出

#include<stdio.h>

int main()

{

while(1)

{

printf("Hello World\n");

sleep(1);//延時1秒

}

return 0;

}

示例1.3:stderr使用無緩衝形式

效果:stderr使用無緩衝,即使不使用'\n'仍能立即輸出

#include<stdio.h>

int main()

{

while(1)

{

perror("Hello World");

sleep(1);//延時1秒

}

return 0;

}

示例2:編寫程序實現以下功能:

⒈向標準輸出流輸出HelloWorld

⒉向標準錯誤流輸出HelloWorld

⒊控制輸出重定向,使程序僅能輸出標準輸出流的字符

⒋控制輸出重定向,使程序僅能輸出標準錯誤流的字符

#include<stdio.h>

int main()

{

fprintf(stdout,"%s","This is stdout:HelloWorld!\n");

fprintf(stderr,"%s","This is stderr:HelloWorld!\n");

//fprintf的作用是向某個流(文件)中按格式輸出指定內容

return 0;

}

實現第三步的功能:在執行時:./a.out 2> /dev/null

實現第四步的功能:在執行時:./a.out 1> /dev/null

 

四、標準I/O編程

1、打開文件(流)

使用fopen()/fdopen()/freopen()函數可以打開一個文件。其中fopen()是最常用的函數,fdopen()可以指定打開文件的文件描述符和模式,freopen()除可以指定打開的文件與模式外,還可以指定特定的I/O流。

函數fopen()

需要頭文件:#include<stdio.h>

函數原型:FILE *fopen(const char *path,const char *mode)

函數參數:path:要打開的文件的路徑及文件名

  mode:文件打開方式,見下

函數返回值:成功:指向文件的FILE類型指針

失敗:NULL

以下是mode參數允許使用的取值及說明:

  • r或rb 以只讀的方式打開文件,該文件必須存在
  • r+或r+b 以可讀可寫的方式打開文件,該文件必須存在
  • w或wb 以只寫的方式打開文件,若文件不存在則創建該文件;若文件存在則擦除文件原始內容,從文件開頭開始操作文件
  • w+或w+b 以可讀可寫的方式打開文件,若文件不存在則創建該文件;若文件存在則擦除文件原始內容,從文件開頭開始操作文件
  • a或ab 以附加的方式打開只寫文件,若文件不存在則創建該文件;若文件存在,寫入的數據追加在文件尾,即文件的原始內容會被保留
  • a+或a+b 以附加的方式打開可讀可寫文件,若文件不存在則創建該文件;若文件存在,寫入的數據追加在文件尾,即文件的原始內容會被保留

注意:

⒈+的作用代表操作文件的方式是隻讀/寫/附加(無+)還是同時讀寫(有+)

⒉b的作用代表操作的文件是ASCII文本文件(無b)還是二進制文件(有b)

2、關閉文件(流)

使用fclose()函數可以關閉一個文件,該函數將緩衝區內的所有內容寫入相關文件中並回收相應的系統資源

函數fclose()

需要頭文件:#include<stdio.h>

函數原型:int fclose(FILE *stream)

函數參數:stream:已打開的流指針

函數返回值:成功:0

失敗:EOF

示例:打開一個文件然後關閉

寫一個程序,打開一個文件,然後關閉該文件。

#include<stdio.h>

#include<stdlib.h>

int main()

{

FILE *fp;

if((fp = fopen("hello.txt","w"))==NULL)//打開文件,之後判斷是否打開成功

{

perror("cannot open file");

exit(0);

}

//對文件的操作

fclose(fp);//關閉文件

return 0;

}

注意:由於打開文件fopen()函數可能會失敗,所以我們打開文件後通常需要判斷fopen()函數是否打開文件成功。判斷的方法是將fopen的結果放入if()表達式中並判斷該表達式得到的結果是否爲NULL(空指針)。

練習:若示例程序的hello.txt文件不存在,以"r"的模式打開該文件會出現什麼效果?

答案:

//對文件的操作

fclose(fp);//關閉文件

}

若hello.txt文件不存在,則在執行程序時會報錯:

cannot open file: No such file or directory

示例:打開文件hello.txt,使用fprintf()向hello.txt中寫入"HelloWorld"。其中打開文件部分使用命令行傳參。

#include<stdio.h>

#include<stdlib.h>

int main(int argc,char *argv[])

{

FILE *fp;

if((fp = fopen(argv[1],"w"))==NULL)//打開文件,之後判斷是否打開成功

{

perror("cannot open file");

exit(0);

}

fprintf(fp,"%s","HelloWorld\n");

fclose(fp);//關閉文件

}

練習1:打開文件hello.txt,使用fprintf()在剛纔的"HelloWorld"之後添加一行"NiHao, Farsight"

答案:

#include<stdio.h>

#include<stdlib.h>

int main()

{

FILE *fp;

if((fp = fopen("hello.txt","a"))==NULL)//打開文件,之後判斷是否打開成功

{

perror("cannot open file");

exit(0);

}

fprintf(fp,"%s","NiHao, Farsight\n");

fclose(fp);//關閉文件

}

練習2:打開文件hello.txt,將該文件原始內容清空並添加"LiLaoShi zhen shuai!"

#include<stdio.h>

#include<stdlib.h>

int main()

{

FILE *fp;

if((fp = fopen("hello.txt","w"))==NULL)//打開文件,之後判斷是否打開成功

{

perror("cannot open file");

exit(0);

}

fprintf(fp,"%s","LiLaoShi zhen shuai!\n");

fclose(fp);//關閉文件

}

3、錯誤輸出

在剛纔的示例程序與練習程序中使用了perror這個函數。perror函數可以在程序出錯的時候將錯誤信息輸出到標準錯誤流stderr中。由於標準錯誤流不使用緩衝所以可以及時顯示錯誤信息。

函數perror()

需要頭文件:#include<stdio.h>

函數原型:void perror(const char *s)

函數參數:s:在標準錯誤流上輸出的錯誤信息

函數返回值:無

在標準I/O函數執行的時候,若發生了錯誤(例如以r或r+打開一個不存在的文件),會將錯誤碼保存在系統變量errno中。使用perror函數可以檢測errno的錯誤碼信息並輸出對應的錯誤信息。

注意:errno的變量聲明不在stdio.h中,而是在頭文件errno.h中

除了使用perror來輸出錯誤信息,也可以使用strerror函數手動獲得錯誤碼對應的錯誤信息

函數strerror()

需要頭文件:#include<string.h>、#include<errno.h>

函數原型:char *strerror(int errno)

函數參數:errno:返回的錯誤碼

函數返回值:錯誤碼對應的信息

示例:不使用perror,使用strerror輸出錯誤信息

#include<stdio.h>

#include<stdlib.h>

#include<string.h>

#include<errno.h>

int main()

{

FILE *fp;

if((fp = fopen("hello1.txt","r"))==NULL)//故意打開一個不存在的文件

{

printf("打開了一個不存在的文件:%s\n",strerror(errno));

exit(0);

}

fclose(fp);//關閉文件

}

4、按字符輸入/輸出

函數getc()、fgetc()、getchar()

需要頭文件:#include<stdio.h>

函數原型:int getc(FILE *stream)

  int fgetc(FILE *stream)

  int getchar(void)

函數參數:stream:輸入文件流

函數返回值:成功:讀到的字符

失敗:EOF

/*********************有關EOF************************/

在C語言(或者更精確的說是在C標準庫中)EOF表示一個文本文件的結束符(end of file),這個宏定義在頭文件stdio.h中,其值爲-1,因爲在ASCII表的編碼(0~255)中沒有-1編碼。EOF通常被當做文件結束的標誌,還有很多的文件處理相關的函數使用EOF作爲函數出錯的返回值。

但是要注意的是,EOF只能作爲文本文件(流)的結束符,因爲若該文件(流)是二進制形式文件則會有-1的出現,因此無法使用EOF來表徵文件結束。爲解決這個問題,在C語言中提供了一個feof()函數,若遇到文件結尾,函數feof()返回1,否則返回0。這個函數既可以判斷二進制文件也可以判斷文本文件。

/*********************有關EOFend*********************/

getc()函數和fgetc()函數是從一個指定的流中讀取一個字符,getchar()函數是從stdin中讀取一個字符

函數putc()、fputc()、putchar()

需要頭文件:#include<stdio.h>

函數原型:int putc(int c,FILE *stream)

  int fputc(int c,FILE *stream)

  int putchar(int c)

函數參數:c:待輸出的字符(的ASCII碼)

  stream:輸入文件流

函數返回值:成功:輸出字符c

失敗:EOF

putc()函數和fputc()函數是從一個指定的流中輸出一個字符,putchar()函數是從stdout中輸出一個字符。

示例:從文件hello.txt中讀取字符然後輸出到顯示器上

#include<stdio.h>

#include<stdlib.h>

int main()

{

int c;

FILE *fp;

if((fp = fopen("hello.txt","r+"))==NULL)//打開文件,之後判斷是否打開成功

{

perror("cannot open file");

exit(0);

}

c = fgetc(fp);

while(c!=EOF)

{

putchar(c);

c = fgetc(fp);

}

fclose(fp);

return 0;

}

練習:文件hello.txt中存放了各種字符(大寫字母、小寫字母、數字、特殊符號等),將該文件中的字母輸出,非字母不輸出

答案:

#include<stdio.h>

#include<stdlib.h>

int main()

{

int c;

FILE *fp;

if((fp = fopen("hello.txt","r+"))==NULL)//打開文件,之後判斷是否打開成功

{

perror("cannot open file");

exit(0);

}

c = fgetc(fp);

while(c!=EOF)

{

if((c>='A'&&c<='Z')||(c>='a'&&c<='z'))

putchar(c);

c = fgetc(fp);

}

printf("\n");

fclose(fp);

return 0;

}

5、按行輸入/輸出

當然,如果我們每次都按字符一個一個字符操作的話,程序執行效率會大大降低。因此標準I/O中提供了按行輸入/輸出的操作函數。

函數gets()、fgets()

需要頭文件:#include<stdio.h>

函數原型:char *gets(char *s)

  char *fgets(char *s,int size,FILE *stream)

函數參數:s:存放輸入字符的緩衝區地址

  size:輸入的字符串長度

  stream:輸入文件流

函數返回值:成功:s

失敗或讀到文件尾:NULL

在Linux的內核man手冊中,對gets()函數的評價是:"Never use gets().  Because it is impossible to tell without knowing the data in advance how many  characters  gets()  will  read,  and  because gets() will continue to store characters past the end of the buffer, it is extremely dangerous to use.  It has  been  used  to  break  computer security.  Use fgets() instead."簡單來說gets()的執行邏輯是尋找該輸入流的'\n'並將'\n'作爲輸入結束符,但是若輸入流數據超過存儲空間大小的話會覆蓋掉超出部分的內存數據,因此gets()函數十分容易造成緩衝區的溢出,不推薦使用。而fgets()函數的第二個參數指定了一次讀取的最大字符數量。當fgets()讀取到'\n'或已經讀取了size-1個字符後就會返回,並在整個讀到的數據後面添加'\0'作爲字符串結束符。因此fgets()的讀取大小保證了不會造成緩衝區溢出,但是也意味着fgets()函數可能不會讀取到完整的一行(即可能無法讀取該行的結束符'\n')。

//學習過fgets()之後,輸入字符串儘可能多使用fgets(),而儘量避免使用gets()和scanf("%[^\n]")

示例:使用fgets()函數,依次讀取文件內的內容並輸出。

思考:由於fgets()函數不一定會讀取到'\n',那麼如何使用fgets()函數來統計一個文件有多少行呢?

練習:使用fgets()函數統計某個文本文件有多少行

答案:

#include<stdio.h>

#include<stdlib.h>

#include<string.h>

#define MAX 128

int main(int argc,char *argv[])

{

int c;

char buf[MAX]={0};

FILE *fp;

int line=0;

if(argc<2)

{

perror("argument is too few");

exit(0);

}

if((fp = fopen(argv[1],"r+"))==NULL)//打開文件,之後判斷是否打開成功

{

perror("cannot open file");

exit(0);

}

while(fgets(buf,MAX,fp)!=NULL)

{

if(buf[strlen(buf)-1]=='\n')//若這次讀取讀到了'\n',則證明該行結束

line++;//行數+1

}

printf("This file %s has %d line(s)\n",argv[1],line);

fclose(fp);

return 0;

}

函數puts()、fputs()

需要頭文件:#include<stdio.h>

函數原型:int puts(const char *s)

  int fputs(conse char *s,FILE *stream)

函數參數:s:存放輸出字符的緩衝區地址

  stream:輸出文件流

函數返回值:成功:非負數

失敗:EOF

6、使用格式化輸入/輸出操作文件

剛纔學習的fgetc()/fputc()/fgets()/fputs()以及相關的函數都是將數據作爲字符類型進行操作,但是如果我們想將數字相關類型(int,float類型等)讀/寫文件顯然是不可以的。例如我們想在某文件中寫入float類型3.5,則不能使用fputc()/fputs()。這時我們可以使用我們熟悉的printf()/scanf()函數以及它們的同族函數fprintf()/fscanf()實現數據的格式化讀/寫。

函數scanf()、fscanf()、sscanf()

需要頭文件:#include<stdio.h>

函數原型:int scanf(const char *format,...);

  int fscanf(FILE *fp,const char *format,...);

  int sscanf(char *buf,const char *format,...);

函數參數:format:輸入的格式

  fp:待輸入的流

  buf:待輸入的緩衝區

函數返回值:成功:讀到的數據個數

失敗:EOF

函數printf()、fprintf()、sprintf()

需要頭文件:#include<stdio.h>

函數原型:int printf(const char *format,...);

  int fprintf(FILE *fp,const char *format,...);

  int sprintf(char *buf,const char *format,...);

函數參數:format:輸出的格式

  fp:待輸出的流

  buf:待輸出的緩衝區

函數返回值:成功:輸出的字符數

失敗:EOF

示例:使用fprintf()函數向文件中寫入一些數據,然後使用fscanf()函數讀出這些數據

#include<stdio.h>

#include<stdlib.h>

int main(int argc, const char *argv[])

{

FILE *fp1,*fp2;

if((fp1=fopen("scanftest.txt","w+"))==NULL)

{

perror("cannot open file");

exit(0);

}

fprintf(fp1,"%d %d %d %d %s",1,2,3,4,"HelloWorld\n");

fclose(fp1);

/*文件寫入數據完畢*/

/*開始讀取文件數據*/

int a,b,c,d;

char str[32];

if((fp2=fopen("scanftest.txt","r+"))==NULL)

{

perror("cannot open file");

exit(0);

}

fscanf(fp2,"%d%d%d%d%s",&a,&b,&c,&d,str);

printf("a is %d\nb is %d\nc is %d\nd is %d\nstr:%s\n",a,b,c,d,str);

fclose(fp2);

return 0;

}

/****************關於sscanf()與sprintf()****************/

sscanf()與sprintf()函數的第一個參數都是字符型指針。sscanf()函數可以在字符串中讀出指定格式的數據,sprintf()函數可以將數據按指定格式寫入到某字符數組中。

示例:使用sscanf()函數在一個字符串中讀出指定的數據

#include<stdio.h>

#include<stdlib.h>

int main(int argc, const char *argv[])

{

FILE *fp;

int a,b,c,d;

if((fp=fopen("scanftest.txt","w+b"))==NULL)

{

perror("cannot open file");

exit(0);

}

sscanf(argv[1],"%d.%d.%d.%d",&a,&b,&c,&d);

printf("a is %d\nb is %d\nc is %d\nd is %d\n",a,b,c,d);

fclose(fp);

return 0;

}

編譯後執行a.out 192.168.1.20,則可將ip地址(字符串)內的數據讀出並寫入變量a,b,c,d(int型)中。

/****************關於sscanf()與sprintf()end*************/

7、指定大小讀/寫文件

格式化輸入/輸出函數fscanf()/fprintf()使用比較方便,程序也簡單易懂,但是fscanf()/fprintf()的讀/寫效率低下。一般在程序開發過程中,更多的使用fread()/fwrite()函數來一次讀/寫幾個數據。

函數fread()

需要頭文件:#include<stdio.h>

函數原型:size_t fread(void *ptr,size_t size,size_t nmemb,FILE *stream);

函數參數:ptr:存放讀入數據的緩衝區

  size:讀取的每個數據項的大小(單位字節)

  nmemb:讀取的數據個數

  stream:要讀取的流

函數返回值:成功:實際讀到的nmemb數目

失敗:0

函數fwrite()

需要頭文件:#include<stdio.h>

函數原型:size_t fwrite(void *ptr,size_t size,size_t nmemb,FILE *stream);

函數參數:ptr:存放寫入數據的緩衝區

  size:寫入的每個數據項的大小(單位字節)

  nmemb:寫入的數據個數

  stream:要寫入的流

函數返回值:成功:實際寫入的nmemb數目

失敗:0

注意:

⒈fread()函數和fwrite()函數會將流當做二進制流的形式進行讀/寫,因此使用fread()/fwrite()操作的文件使用vim打開可能會出現亂碼情況。

⒉fread()函數結束時,無法自動判斷導致fread()函數結束的原因是讀取到了文件末尾還是發生了讀寫錯誤。這時需要手動判斷髮生的情況。可以觀察最後一次fread()的返回值,或使用feof()/ferror()函數判斷。

示例1:使用fread()函數一次性讀取1000個字節的數據

#include<stdio.h>

#include<stdlib.h>

int main(int argc, const char *argv[])

{

FILE *fp;

char buf[1000];

int i;

if((fp=fopen(argv[1],"r+"))==NULL)

{

perror("cannot open file");

exit(0);

}

fread(buf,sizeof(char),1000,fp);

for(i=0;i<1000;i++)

{

putchar(buf[i]);

}

fclose(fp);

return 0;

}

示例2:將一個int型數組{0,1,2,3,4,5,6,7,8,9}寫入一個文件中

#include<stdio.h>

#include<stdlib.h>

int main(int argc, const char *argv[])

{

FILE *fp;

int a[10]={1,2,3,4,5,6,7,8,9,10};

int i;

if((fp=fopen(argv[1],"w+"))==NULL)

{

perror("cannot open file");

exit(0);

}

fwrite(a,sizeof(a[0]),sizeof(a)/sizeof(a[0]),fp);

fclose(fp);

return 0;

}

不過查看該文件會發現是亂碼

練習1:讀以下程序,猜想該程序會向文件中輸入什麼數據,運行程序證明猜想

#include<stdio.h>

#include<stdlib.h>

int main(int argc, const char *argv[])

{

if(argc<2)

{

printf("too few arguments\n");

exit(0);

}

FILE *fp;

int a[]={1632397644,1768444783,1852139610,1635084371,169943401};

if((fp=fopen(argv[1],"w+"))==NULL)

{

perror("cannot open file");

exit(0);

}

fwrite(a,sizeof(a[0]),sizeof(a)/sizeof(a[0]),fp);

fclose(fp);

return 0;

}

練習2:使用標準I/O的fread()/fwrite()函數實現文件的複製

提示:分別打開兩個文件,一個源文件一個目標文件,循環從源文件中使用fread()取出數據,然後使用fwrite()函數寫入目標文件中。注意fread()函數的循環結束條件。

#include<stdio.h>

#include<stdlib.h>

#define MAX 128

int main(int argc, const char *argv[])

{

FILE *fp1,*fp2;

char buf[MAX];

int n;

if(argc<3)

{

printf("arguments are too few, Usage:%s <src_file> <dst_file>\n",argv[0]);

exit(0);

}

if((fp1=fopen(argv[1],"r"))==NULL)

{

perror("cannot open file1");

fclose(fp1);

exit(0);

}

if((fp2=fopen(argv[2],"w"))==NULL)

{

perror("cannot open file2");

fclose(fp2);

exit(0);

}

while((n=fread(buf,sizeof(buf[0]),sizeof(buf),fp1))>0)

{

fwrite(buf,sizeof(buf[0]),n,fp2);

}

fclose(fp1);

fclose(fp2);

return 0;

}

8、流的定位

每次使用流打開文件並對文件進行操作後,都會讓操作文件數據的位置發生偏移。在打開流的時候,偏移位置爲0(即文件開頭),隨着讀寫的進行,偏移位置會不斷向後,每次偏移量自動增加實際讀寫的大小。可以使用fseek()函數和ftell()函數對當前流的偏移量進行定位操作

函數fseek()

需要頭文件:#include<stdio.h>

函數原型:int fseek(FILE *stream,long offset,int whence);

函數參數:stream:要定位的流

  offset:相對於基準點whence的偏移量,正數表示向前(向文件尾方向)移動,負數表示向後(向文件頭方向)移動,0表示不移動

  whence:基準點(取值見下)

函數返回值:成功:0,改變讀寫位置

失敗:EOF,不改變讀寫位置

其中第三個參數whence的取值如下:

SEEK_SET:代表文件起始位置,數字表示爲0

SEEK_CUR:代表文件當前的讀寫位置,數字表示爲1

SEEK_END:代表文件結束位置,數字表示爲2

使用fseek()函數可以定位流的讀寫位置,通過偏移量+基準值的計算將讀寫位置移動到指定位置,其中第二個參數offset的值爲正時表示向後移動,爲負時表示向前移動,0表示不動。

示例:讀取一個文件的最後10個字節

#include<stdio.h>

#include<stdlib.h>

#define MAX 10

int main(int argc, const char *argv[])

{

FILE *fp;

char buf[MAX];

int n;

if(argc<2)

{

printf("arguments are too few\n");

exit(0);

}

if((fp=fopen(argv[1],"r"))==NULL)

{

perror("cannot open file1");

exit(0);

}

fseek(fp,-10,SEEK_END);

fread(buf,sizeof(buf[0]),MAX,fp);

for(n=0;n<MAX;n++)

{

putchar(buf[n]);

}

fclose(fp);

return 0;

}

注意實際輸出只能輸出9個字符,因爲最後一個字符爲'\n'。

若想知道當前的讀寫位置的偏移量,則可以使用ftell()函數

函數ftell()

需要頭文件:#include<stdio.h>

函數原型:int ftell(FILE *stream);

函數參數:stream:要定位的流

函數返回值:成功:返回當前的讀寫位置

失敗:EOF

示例:在上一個示例程序中添加ftell()函數

#include<stdio.h>

#include<stdlib.h>

#define MAX 10

int main(int argc, const char *argv[])

{

FILE *fp;

char buf[MAX];

int n;

if(argc<2)

{

printf("arguments are too few\n");

exit(0);

}

if((fp=fopen(argv[1],"r"))==NULL)

{

perror("cannot open file1");

exit(0);

}

fseek(fp,-10,SEEK_END);

printf("This location is %ld\n",ftell(fp));

fread(buf,sizeof(buf[0]),MAX,fp);

for(n=0;n<MAX;n++)

{

putchar(buf[n]);

}

fclose(fp);

return 0;

}

練習:使用fseek()函數和ftell()函數求一個文件的大小。

提示:先使用fseek()定位到文件末尾,再使用ftell()得到值。

#include<stdio.h>

#include<stdlib.h>

#define MAX 10

int main(int argc, const char *argv[])

{

FILE *fp;

if(argc<2)

{

printf("arguments are too few\n");

exit(0);

}

if((fp=fopen(argv[1],"r"))==NULL)

{

perror("cannot open file1");

exit(0);

}

fseek(fp,0,SEEK_END);

printf("File is %ld\n",ftell(fp));

fclose(fp);

return 0;

}

可以使用ls -l指令與該程序的執行結果進行對比。

9、其他常用標準I/O操作函數

①刷新緩衝區fflush()

函數fflush()

需要頭文件:#include<stdio.h>

函數原型:int fflush(FILE *stream);

函數參數:stream:操作的流

函數返回值:成功:0

失敗:EOF

fflush()函數會清除(原意是“沖刷”)該流內的輸出緩衝區並立即將輸出緩衝區的數據寫回,即強迫將緩衝區內的數據寫回流stream指定的文件中。若fflush()的參數爲0(或NULL)則會刷新所有已經打開的流的輸出緩衝區。

注意:

⒈fflush()函數可能會執行失敗,當fflush()函數執行失敗時會返回EOF,這可能由於緩衝區數據意外丟失或其他未知原因。因此當某些重要文件需要設置時,若使用fflush()刷新緩衝區失敗,則應考慮使用文件I/O相關操作(open()、close()、read()、write()等)來代替標準I/O操作。

⒉!!!請不要試圖使用fflush()刷新stdin!!!

在C標準和POSIX標準中,fflush()僅定義了刷新輸出流的行爲,對於輸入流stdin的fflush()操作是“未定義”(undefined),因此不同操作系統不同編譯器對fflush(stdin)的操作都不會相同。fflush(stdin)操作只對部分編譯器(例如VC++)等有效,而現在的大多數編譯器(gcc等)是無效的。按C99標準規定的fflush()函數定義來說,fflush()函數是不允許刷新stdin的。

(原文:For input streams, fflush() discards any buffered data that has been fetched from the underlying file, but has not been consumed by the application. 大意是說如果對fflush傳入一個輸入流,會清除已經從輸入流中取出但還沒有交給程序的數據,而對輸入流中剩餘的未被程序處理的數據沒有提及,可能不受影響,也可能直接丟棄)

對於fflush(stdin)操作,該操作未被標準定義,行爲不確定,不同系統不同編譯器操作不相同(可移植性不好),因此極爲不推薦使用fflush()刷新標準輸入流stdin。

②判斷文件是否已經結束feof()

函數feof()

需要頭文件:#include<stdio.h>

函數原型:int feof(FILE *stream);

函數參數:stream:操作的流

函數返回值:文件結束:非0的值

文件未結束:0

feof()函數可以檢測流上的文件結束符,若文件結束則返回一個非0值,否則返回0。

文件結束符EOF的數值是0xFF(十進制爲-1),在文本文件中我們可以通過fgetc()是否讀取到EOF來判斷文件是否結尾(見fgetc()的示例程序)。而在二進制文件中可以有數值-1,因此就無法使用fgetc()讀取EOF的方法來判斷文件是否結尾。這時我們可以使用feof()函數來判斷,feof()函數既可以判斷文本文件又可以判斷二進制文件。

feof()的常用用法有

if(feof(stream))//判斷該流是否已結尾

或者

while(!feof(stream))//循環操作該流直至文件結尾

等。

③回到文件開頭rewind()

函數rewind()

需要頭文件:#include<stdio.h>

函數原型:void rewind(FILE *stream);

函數參數:stream:操作的流

函數返回值:無

rewind()函數會將當前讀寫位置返回至文件開頭(rewind原意爲“(磁帶等)回滾,倒帶”),其等價於

(void)fseek(stream, 0L, SEEK_SET)

 

綜合練習:循環記錄系統的時間

每過1s,讀取一次當前系統時間,之後寫入到文件中。再次操作該文件不會刪除文件內的原始數據而是繼續向後書寫數據。

提示:打開文件--->獲取系統時間--->寫入文件--->延時1s

                       ↑                        ↓

   -----------死循環---------

答案:

#include<stdio.h>

#include<stdlib.h>

#include<time.h>

#define MAX 64

int main(int argc, const char *argv[])

{

FILE *fp;

char buf[MAX];

int n;

time_t t;

if(argc<2)

{

printf("arguments are too few\n");

exit(0);

}

if((fp=fopen(argv[1],"a+"))==NULL)

{

perror("cannot open file");

exit(0);

}

while(1)

{

t = time(NULL);

fprintf(fp,"%s",asctime(localtime(&t)));

fflush(NULL);//刷新緩衝區

printf("%s",asctime(localtime(&t)));

sleep(1);

}

fclose(fp);

return 0;

}

 

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