C語言-標(biāo)準(zhǔn)I/O與系統(tǒng)i/o

幾個基本概念

什么是IO:
????I/O輸入/輸出(Input/Output),分為IO設(shè)備和IO接口兩個部分。
在C語言中的I/O,個人理解就是把數(shù)據(jù)傳輸?shù)匠绦蚓褪荌,從程序中把數(shù)據(jù)傳輸出去就是O。
很多時候,數(shù)據(jù)來源就是文件,所有很多時候我們把對文件的操作,也叫做I/O操作了。
????關(guān)于這樣的I/O操作,在Linux操作系統(tǒng)上分兩種,其中一個是標(biāo)準(zhǔn)IO,另一個是系統(tǒng)調(diào)用IO。
系統(tǒng)調(diào)用IO與標(biāo)準(zhǔn)IO的區(qū)別:
????????系統(tǒng)調(diào)用IO:由操作系統(tǒng)直接提供的操作文件的接口函數(shù),和操作系統(tǒng)綁定,沒有緩存,更接近硬件,可以操作普通文件與驅(qū)動文件。
????????標(biāo)準(zhǔn)IO:由標(biāo)準(zhǔn)C庫(第三方庫)提供的操作文件的接口函數(shù)(對操作系統(tǒng)提供的系統(tǒng)IO進(jìn)行二次封裝),有緩存,只可以操作普通文件。

C語言中的文件
????????我們對文件的概念已經(jīng)非常熟悉了,比如常見的 word 文檔、txt 文件、c源文件等。
文件是數(shù)據(jù)源的一種,最主要的作用是保存數(shù)據(jù)。
在C語言程序中,文件有著更廣泛的定義:文件通常是在磁盤或硬盤上的已命名的儲存區(qū)。
要看到是通常,那么就說明會有不尋常的。對于C程序來說,設(shè)備也可以看成是文件!
例如顯示器(標(biāo)準(zhǔn)輸出文件 stdout,標(biāo)準(zhǔn)錯誤輸出stderr)和鍵盤(標(biāo)準(zhǔn)輸入文件 stdin)。

我們不去探討硬件設(shè)備是如何被映射成文件的,大家只需要記住,在C語言中硬件設(shè)備可以看成文件!

另外,在C程序看來,文件只有兩種形式:
(1)文本文件
(2)二進(jìn)制文件
????????根據(jù)我們以往的經(jīng)驗,文本文件通常用來保存肉眼能認(rèn)識的字符,比如.txt文件、.c文件、.dat文件等,用文本編輯器打開這些文件,我們能夠順利看懂文件的內(nèi)容。
????????二進(jìn)制文件通常用來保存視頻、圖片、程序等不可閱讀的內(nèi)容,用文本編輯器打開這些文件,會看到一堆亂碼,根本看不懂。
????????但是從物理上講,二進(jìn)制文件和字符文件并沒有什么區(qū)別,它們都是以二進(jìn)制的形式保存在磁盤上的數(shù)據(jù)。

????????我們之所以能看懂文本文件的內(nèi)容,是因為文本文件中采用的是 ASCII、UTF-8、GBK 等字符編碼,文本編輯器可以識別出這些編碼格式,并將編碼值轉(zhuǎn)換成字符展示出來。

????????而二進(jìn)制文件使用的是 mp4、gif、exe 等特殊編碼格式,文本編輯器并不認(rèn)識這些編碼格式,只能按照字符編碼格式胡亂解析,所以就成了一堆亂七八糟的字符,有的甚至都沒見過。

????????總起來說,不同類型的文件有不同的編碼格式,必須使用對應(yīng)的程序(軟件)才能正確解析,否則就是一堆亂碼,無法使用。

文件流

????????文件是數(shù)據(jù)源的一種,除了文件,還有數(shù)據(jù)庫、網(wǎng)絡(luò)、鍵盤等;
數(shù)據(jù)傳遞到內(nèi)存也就是保存到C語言的變量(例如整數(shù)、字符串、數(shù)組、緩沖區(qū)等)。

????????我們把數(shù)據(jù)在數(shù)據(jù)源和程序(內(nèi)存)之間傳遞的過程叫做數(shù)據(jù)流(Data Stream)。相應(yīng)的,數(shù)據(jù)從數(shù)據(jù)源到程序(內(nèi)存)的過程叫做輸入流(Input Stream),從程序(內(nèi)存)到數(shù)據(jù)源的過程叫做輸出流(Output Stream)。

????????輸入輸出(Input output,IO)是指程序(內(nèi)存)與外部設(shè)備(鍵盤、顯示器、磁盤、其他計算機(jī)等)進(jìn)行交互的操作。幾乎所有的程序都有輸入與輸出操作,如從鍵盤上讀取數(shù)據(jù),從本地或網(wǎng)絡(luò)上的文件讀取數(shù)據(jù)或?qū)懭霐?shù)據(jù)等。通過輸入和輸出操作可以從外界接收信息,或者是把信息傳遞給外界。
????????一般情況下,我們可以把打開文件說成打開了一個流。

C語言fopen和fclose函數(shù)

????????C語言中操作文件的正確流程為:打開文件 --> 讀寫文件 --> 關(guān)閉文件。
所謂打開文件,就是獲取文件的有關(guān)信息,例如文件名、文件狀態(tài)、當(dāng)前讀寫位置等,
這些信息會被保存到一個 FILE 類型的結(jié)構(gòu)體變量中。
關(guān)閉文件,就是斷開與文件之間的聯(lián)系,釋放結(jié)構(gòu)體變量,同時禁止再對該文件進(jìn)行操作。

使用 <stdio.h> 頭文件中的 fopen() 函數(shù)即可打開文件,它的用法為:

FILE *fopen(char *filename, char *mode);

filename為文件名(包括文件路徑)
mode為打開模式,它們都是字符串。


c91d15f4af634c509c63c5bcdcbdb6d7.png

fopen() 函數(shù)的返回值類型是(FILE *)
FILE 是 <stdio.h> 頭文件中的一個結(jié)構(gòu)體,它專門用來保存文件信息。
我們不用關(guān)心 FILE 的具體結(jié)構(gòu),只需要知道它的用法就行。

例如:fopen.c

#include <stdio.h>
int main(){
    FILE * fp=fopen("./demo.txt","w");
    return 0;
}
//編譯執(zhí)行可執(zhí)行文件后,若當(dāng)前文件夾中沒有demo.txt,則創(chuàng)建之,
gcc -g fopen.c -o fopen.exe
./fopen

表示以“只讀”方式打開當(dāng)前目錄下的 demo.txt 文件,并使 fp 指向該文件,
這樣就可以通過 fp 來操作 demo.txt 了。fp 通常被稱為文件指針。若在文件中加了內(nèi)容,我再次執(zhí)行可執(zhí)行文件./fopen,會清空demo.txt了。
再來看一個例子:

FILE *fp = fopen("D:\\demo.txt","rb+");

表示以二進(jìn)制方式打開 D 盤下的 demo.txt 文件,允許讀和寫。

--------文本方式和二進(jìn)制方式并沒有本質(zhì)上的區(qū)別,只是對于換行符的處理不同。
C語言程序?qū)n作為換行符,類 UNIX/Linux 系統(tǒng)在處理文本文件時也將\n作為換行符,所以程序中的數(shù)據(jù)會原封不動地寫入文本文件中,反之亦然。

2.png

但是 Windows 系統(tǒng)卻不同,它將\r\n作為文本文件的換行符。

--------在 Windows 系統(tǒng)中,如果以文本方式打開文件,當(dāng)讀取文件時,程序會將文件中所有的\r\n轉(zhuǎn)換成一個字符\n。也就是說,如果文本文件中有連續(xù)的兩個字符是\r\n,則程序會丟棄前面的\r,只讀入\n。

當(dāng)寫入文件時,程序會將\n轉(zhuǎn)換成\r\n寫入。也就是說,如果要寫入的內(nèi)容中有字符\n,則在寫入該字符前,程序會自動先寫入一個\r。

?????????總起來說,對于 Windows 平臺,為了保險起見,我們最好用"t"來打開文本文件,用"b"來打開二進(jìn)制文件。對于 Linux 平臺,使用"t"還是"b"都無所謂,既然默認(rèn)是"t",那我們什么都不寫就行了。

判斷文件是否打開成功
??????打開文件出錯時,fopen() 將返回一個空指針,也就是 NULL,我們可以利用這一點來判斷文件是否打開成功,請看下面的代碼:

FILE *fp;
if( (fp=fopen("D:\\demo.txt","rb") == NULL ){
    printf("Fail to open file!\n");
    exit(0);  //退出程序(結(jié)束程序)
}

??????以上代碼是文件操作的規(guī)范寫法,在打開文件時一定要判斷文件是否打開成功,因為一旦打開失敗,后續(xù)操作就都沒法進(jìn)行了,往往以“結(jié)束程序”告終。
fclose函數(shù)
??????當(dāng)進(jìn)行打開文件操作時, 操作系統(tǒng)會建立文件登記表中記錄該文件處于打開狀態(tài),除了申請進(jìn)行打開文件操作的進(jìn)程,其他程序不能對文件進(jìn)行訪問和寫入。
??????文件一旦使用完畢,應(yīng)該用 fclose() 函數(shù)把文件關(guān)閉,以釋放相關(guān)資源,避免數(shù)據(jù)丟失。fclose() 的用法為:

int fclose(FILE *fp);

fp 為文件指針。例如:

int fclose(fp);

文件正常關(guān)閉時,fclose() 的返回值為0,如果返回非零值則表示有錯誤發(fā)生。

實例演示
??????最后,我們通過一段完整的代碼來演示 fopen 函數(shù)的用法,這個例子會一行一行地讀取文本文件的所有內(nèi)容:

#include <stdio.h>
#include <stdlib.h>
int main(){
    FILE * fp =NULL;
    //判斷一個,有沒有成功打開文件,如果打開出錯,直接退出程序
    if((fp = fopen("./demo.txt","rt")) == NULL){
        puts("fail to open file!!!");
        exit(0);
    }
    //打開文件后,就對文件進(jìn)行讀寫操作。。。。

    //用完了文件后,一定要關(guān)閉
    if(fclose(fp) == 0){
        puts("關(guān)閉文件成功?。?!");
    }else{
        puts("文件關(guān)閉失敗");
        exit(0);
    }
    return 0;
}


//編譯與執(zhí)行
gcc -g .\fopen_close.c -o fopenclose.exe     
./fopenclose
關(guān)閉文件成功?。?!

C語言fgetc和fputc函數(shù)用法

??????在C語言中,讀寫文件比較靈活,既可以每次讀寫一個字符,也可以讀寫一個字符串,甚至是任意字節(jié)的數(shù)據(jù)(數(shù)據(jù)塊)。每次可以從文件中讀取一個字符,或者向文件中寫入一個字符。
主要使用兩個函數(shù),分別是 fgetc() 和 fputc()。
字符讀取函數(shù) fgetc
??????fgetc 是 file get char 的縮寫,意思是從指定的文件中讀取一個字符。fgetc() 的用法為:

int fgetc (FILE *fp);

?????????fp 為文件指針。
??????fgetc() 讀取成功時返回讀取到的字符,讀取到文件末尾或讀取失敗時返回EOF。
EOF 是 end of file 的縮寫,表示文件末尾,是在 stdio.h 中定義的宏,它的值是一個負(fù)數(shù),往往是 -1。
??????fgetc() 的返回值類型之所以為 int,就是為了容納這個負(fù)數(shù)(char不能是負(fù)數(shù))。
注意:不能武斷地認(rèn)為EOF 就是 -1,也可以是其他負(fù)數(shù),這要看編譯器的實現(xiàn)。

fgetc() 的用法舉例:

char ch;
FILE *fp = fopen("./demo.txt", "r+");
ch = fgetc(fp);

???表示從當(dāng)前目錄中的demo.txt文件中讀取一個字符,并保存到變量 ch 中。在文件內(nèi)部有一個位置指針,用來指向當(dāng)前讀寫到的位置,也就是讀寫到第幾個字節(jié)。
??????在文件打開時,該指針總是指向文件的第一個字節(jié)。使用 fgetc() 函數(shù)后,該指針會向后移動一個字節(jié),所以可以連續(xù)多次使用 fgetc() 讀取多個字符。
注意:這個文件內(nèi)部的位置指針與C語言中的指針不是一回事。
???位置指針僅僅是一個標(biāo)志,表示文件讀寫到的位置,也就是讀寫到第幾個字節(jié),它不表示地址。
???文件每讀寫一次,位置指針就會移動一次,它不需要你在程序中定義和賦值,而是由系統(tǒng)自動設(shè)置,對用戶是隱藏的。

【示例】在屏幕上顯示 當(dāng)前目錄中的demo.txt 文件的內(nèi)容。

#include <stdio.h>
#include <stdlib.h>
int main(){
    FILE * fp =NULL;
    char ch;
    //判斷一個,有沒有成功打開文件,如果打開出錯,直接退出程序
    if((fp = fopen("./demo.txt","rt")) == NULL){
        puts("fail to open file!!!");
        exit(0);
    }
    //打開文件后,就對文件進(jìn)行讀寫操作。。。。
    while((ch=fgetc(fp))!=EOF){
        printf("%c",ch);
    }

    //用完了文件后,一定要關(guān)閉
    if(fclose(fp) == 0){
        puts("\n關(guān)閉文件成功?。?!");
    }else{
        puts("文件關(guān)閉失敗");
        exit(0);
    }
    return 0;
}
//編譯執(zhí)行
>gcc -g .\fopen_close.c -o fopenclose.exe     
>./fopenclose
xiongshaowen
關(guān)閉文件成功!?。?

對 EOF 的說明
??????EOF 本來表示文件末尾,意味著讀取結(jié)束,但是很多函數(shù)在讀取出錯時也返回 EOF,那么當(dāng)返回 EOF 時,到底是文件讀取完畢了還是讀取出錯了?
我們可以借助 stdio.h 中的兩個函數(shù)來判斷,分別是 feof() 和 ferror()。
??????feof() 函數(shù)用來判斷文件內(nèi)部指針是否指向了文件末尾,它的原型是:

int feof ( FILE * fp );

當(dāng)指向文件末尾時返回非零值,否則返回零值。

??????ferror() 函數(shù)用來判斷文件操作是否出錯,它的原型是:

int ferror ( FILE *fp );

出錯時返回非零值,否則返回零值。

??????需要說明的是,文件出錯是非常少見的情況,上面的示例基本能夠保證將文件內(nèi)的數(shù)據(jù)讀取完畢。
如果追求完美,也可以加上判斷并給出提示:

while((ch=fgetc(fp))!=EOF){
    printf("%c",ch);
}
if((ferror(fp))==0){
    printf("\n正常讀完文件!");
}else{
    printf("\n文件讀取出了錯誤 !");
}

字符寫入函數(shù) fputc
??????fputc 是 file output char 的所以,意思是向指定的文件中寫入一個字符。fputc() 的用法為:

int fputc ( int ch, FILE *fp );

??????ch 為要寫入的字符,fp 為文件指針。
??????fputc() 寫入成功時返回寫入的字符,失敗時返回 EOF,返回值類型為 int 也是為了容納這個負(fù)數(shù)。
例如:

fputc('a', fp);

或者:

char ch = 'a';
fputc(ch, fp);

表示把字符 'a' 寫入fp所指向的文件中。

注意:寫和讀一樣,每寫入一個字符,文件內(nèi)部位置指針向后移動一個字節(jié)。

【示例】從鍵盤輸入一行字符,寫入文件。

#include <stdio.h>
#include <stdlib.h>
int main(){
    FILE * fp =NULL;
    char ch;
    //判斷一個,有沒有成功打開文件,如果打開出錯,直接退出程序
    if((fp = fopen("./demo.txt","r+")) == NULL){
        puts("fail to open file!!!");
        exit(0);
    }

    //打開文件后,就對文件進(jìn)行寫操作。。。。
    printf("\n請輸入要寫入文件的字符!以換行結(jié)束:  ");
    while((ch = getchar())!='\n'){
        fputc(ch,fp);
    }
     printf("\n寫入成功!");
   
     //用完了文件后,一定要關(guān)閉
    if(fclose(fp) == 0){
        puts("\n關(guān)閉文件成功!?。?);
    }else{
        puts("文件關(guān)閉失敗");
        exit(0);
    }
    return 0;
}

C語言fgets和fputs函數(shù)

fgetc() 和 fputc() 函數(shù)每次只能讀寫一個字符,速度較慢;
??????實際開發(fā)中往往是每次讀寫一個字符串或者一個數(shù)據(jù)塊,這樣能明顯提高效率。

讀字符串函數(shù) fgets
??????fgets() 函數(shù)用來從指定的文件中讀取一個字符串,并保存到字符數(shù)組中,它的用法為:

char *fgets ( char *str, int n, FILE *fp );

str 為字符數(shù)組,n 為要讀取的字符數(shù)目,fp 為文件指針。

返回值:讀取成功時返回字符數(shù)組首地址,也即 str;
??? 讀取失敗時返回 NULL;
??? 如果開始讀取時文件內(nèi)部指針已經(jīng)指向了文件末尾,那么將讀取不到任何字符,也返回 NULL。

注意,讀取到的字符串會在末尾自動添加 '\0',n 個字符也包括 '\0'。
???也就是說,實際只讀取到了 n-1 個字符,如果希望讀取 100 個字符,n 的值應(yīng)該為 101。
例如:

#define N 101
char str[N];
FILE *fp = fopen("D:\\demo.txt", "r");
fgets(str, N, fp);

需要重點說明的是,在讀取到 n-1 個字符之前如果出現(xiàn)了換行,或者讀到了文件末尾,則讀取結(jié)束。
???這就意味著,不管 n 的值多大,fgets() 最多只能讀取一行數(shù)據(jù),不能跨行。

在C語言中,沒有按行讀取文件的函數(shù),我們借助 fgets(),將 n 的值設(shè)置很大,每次就可以讀取到一行行的數(shù)據(jù)。

【示例】一行一行地讀取文件。

#include <stdio.h>
#include <stdlib.h>
int main(){
    FILE * fp = NULL;
    char ch;
    char str[101] = {0};  //假設(shè)讀100個字符,我們要額外留下一個位置裝'\0'
    //判斷一個,有沒有成功打開文件,如果打開出錯,直接退出程序
    if((fp = fopen("./demo.txt","r+")) == NULL){
        puts("fail to open file!!!");
        exit(0);
    }
    //讀當(dāng)前文件demo.txt,已經(jīng)有幾行內(nèi)容了
    while(fgets(str,101,fp) != NULL){
        printf("%s",str);
    }

     //用完了文件后,一定要關(guān)閉
    if(fclose(fp) == 0){
        puts("\n關(guān)閉文件成功?。?!");
    }else{
        puts("\n文件關(guān)閉失敗");
        exit(0);
    }

}
//編譯執(zhí)行
>gcc -g frwstring.c -o frwstring
> ./frwstring
 
熊少文
xuhuifeng
xionglingzhou
sbsbsb
aaaaaa
bbbbbb
cccccc                             

寫字符串函數(shù) fputs
??????fputs() 函數(shù)用來向指定的文件寫入一個字符串,它的用法為:

int fputs( char *str, FILE *fp );

??????str 為要寫入的字符串,fp 為文件指針。寫入成功返回非負(fù)數(shù),失敗返回 EOF。例如:

char *str = "hello world";
FILE *fp = fopen("./demo.txt", "at+");
fputs(str, fp);

表示把把字符串 str 寫入到 D:\demo.txt 文件中。

【示例】向上例中建立的 d:\demo.txt 文件中追加一個字符串。

#include <stdio.h>
#include <stdlib.h>
int main(){
    FILE * fp = NULL;
    char ch;
    char str[101] = {0};  //假設(shè)讀100個字符,我們要額外留下一個位置裝'\0'
    //判斷一個,有沒有成功打開文件,如果打開出錯,直接退出程序
    if((fp = fopen("./demo.txt","r+")) == NULL){
        puts("fail to open file!!!");
        exit(0);
    }

    //寫入字符串到當(dāng)前demo.txt中
    printf("\n請輸入字符串!");
    gets(str);    //gets函數(shù),請注意不要越界101
    fputs(str,fp);

    //讀當(dāng)前文件demo.txt,已經(jīng)有幾行內(nèi)容了,fp指針要賦初始值,即我們要重新打開文件,初始到最開始的位置
    printf("讀取的內(nèi)容如下:\n");
    //判斷一個
    if((fp = fopen("./demo.txt","r")) == NULL){
        puts("fail to open file!!!");
        exit(0);
    }
    while(fgets(str,101,fp) != NULL){
        printf("\n%s",str);
    }

     //用完了文件后,一定要關(guān)閉
    if(fclose(fp) == 0){
        puts("\n關(guān)閉文件成功!??!");
    }else{
        puts("\n文件關(guān)閉失敗");
        exit(0);
    }

}

注意,上面的代碼,讀出的是上一次寫進(jìn)文件后的內(nèi)容不是本次的內(nèi)容,這種問題以后再講,這里只是講一下寫字符串的功能

C語言fread和fwrite的用法

??????fgets() 有局限性,每次最多只能從文件中讀取一行內(nèi)容,因為 fgets() 遇到換行符就結(jié)束讀取,上面代碼我們用了while循環(huán)讀了多行。
??????如果希望讀取多行內(nèi)容,需要使用 fread() 函數(shù);fputs一次只能寫入一行,想一次寫入多行數(shù)據(jù),相應(yīng)地寫入函數(shù)為 fwrite()。
??????對于 Windows 系統(tǒng),使用 fread() 和 fwrite() 時應(yīng)該以二進(jìn)制的形式打開文件!因為換行時有兩個字符‘'\r\n'

??????fread() 函數(shù)用來從指定文件中讀取塊數(shù)據(jù)塊。所謂塊數(shù)據(jù),也就是若干個字節(jié)的數(shù)據(jù),可以是一個字符,可以是一個字符串,可以是多行數(shù)據(jù),并沒有什么限制。

fread() 的原型為:

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

fwrite() 函數(shù)用來向文件中寫入塊數(shù)據(jù),它的原型為:

size_t fwrite ( void * ptr, size_t size, size_t count, FILE *fp );

對參數(shù)的說明:
(1)ptr 為內(nèi)存區(qū)塊的指針,它可以是數(shù)組、變量、結(jié)構(gòu)體等。
fread() 中的 ptr 用來存放讀取到的數(shù)據(jù),fwrite() 中的 ptr 用來存放要寫入的數(shù)據(jù)。
(2)size:表示每個數(shù)據(jù)塊的字節(jié)數(shù)。
(3)count:表示要讀寫的數(shù)據(jù)塊的塊數(shù)。
(4)fp:表示文件指針。
(5)理論上,每次讀寫 size*count個字節(jié)的數(shù)據(jù)。
(6)size_t 是在 stdio.h 和 stdlib.h 頭文件中使用 typedef 定義的數(shù)據(jù)類型,無符號整數(shù),常用來表示數(shù)量。

返回值:返回成功讀寫的塊數(shù),也即 count。如果返回值小于 count:
(1)對于 fwrite() 來說,肯定發(fā)生了寫入錯誤,可以用 ferror() 函數(shù)檢測。
(2)對于 fread() 來說,可能讀到了文件末尾,可能發(fā)生了錯誤,可以用 ferror() 或 feof() 檢測。

【示例】從鍵盤輸入一個數(shù)組,將數(shù)組寫入文件再讀取出來。

#include <stdio.h>
#include <stdlib.h>
int main(){
    FILE * fp = NULL;
    char ch;
    //判斷一個,有沒有成功打開文件,如果打開出錯,直接退出程序 ,'wb+'二進(jìn)制讀寫文件,重寫時先清清除原內(nèi)容
    if((fp = fopen("./demo.txt","wb+")) == NULL){
        puts("fail to open file!!!");
        exit(0);
    }
    //練習(xí)用fread和fwrite讀寫文件的數(shù)據(jù)
    int a[5]={0},b[5]={0};
    int i=0;
    int size = sizeof(int);   //數(shù)據(jù)塊數(shù)
    for(i=0;i<5;i++){
        scanf("%d",&a[i]);
    }
    fwrite(a,size,5,fp);
    //讀取數(shù)據(jù)到b數(shù)組中
    rewind(fp);    //將文件內(nèi)部的內(nèi)容指針重新指向文件的頭位置處
    fread(b,size,5,fp);
    printf("\n從文件中讀到數(shù)組b的數(shù)據(jù)為:   \n");
    for(i=0;i<5;i++){
        printf("%d",b[i]);

    }
   //用完了文件后,一定要關(guān)閉
    if(fclose(fp) == 0){
        puts("\n關(guān)閉文件成功?。?!");
    }else{
        puts("\n文件關(guān)閉失敗");
        exit(0);
    }

}

數(shù)據(jù)寫入完畢后,位置指針在文件的末尾,要想讀取數(shù)據(jù),必須將文件指針移動到文件開頭rewind(fp);

【示例】從鍵盤輸入兩個學(xué)生數(shù)據(jù),寫入一個文件中,再讀出這兩個學(xué)生的數(shù)據(jù)顯示在屏幕上。

#include<stdio.h>
#define N 2
struct stu{
    char name[10]; //姓名
    int num;  //學(xué)號
    int age;  //年齡
    float score;  //成績
}boya[N], boyb[N], *pa, *pb;
int main(){
    FILE *fp;
    int i;
    pa = boya;
    pb = boyb;
    if( (fp=fopen("./demo.txt", "wb+")) == NULL ){  //w+b,表示打開文件時,清除原文件的內(nèi)容,若沒有文件,則創(chuàng)建demo.txt文件
        puts("Fail to open file!");
        exit(0);
    }
    //從鍵盤輸入數(shù)據(jù)
    printf("Input data:\n");
    for(i=0; i<N; i++,pa++){
        scanf("%s %d %d %f",pa->name, &pa->num,&pa->age, &pa->score);
    }
    //將數(shù)組 boya 的數(shù)據(jù)寫入文件
    fwrite(boya, sizeof(struct stu), N, fp);
    //將文件指針重置到文件開頭
    rewind(fp);
    //從文件讀取數(shù)據(jù)并保存到數(shù)據(jù) boyb
    fread(boyb, sizeof(struct stu), N, fp);
    //輸出數(shù)組 boyb 中的數(shù)據(jù)
    for(i=0; i<N; i++,pb++){
        printf("%s  %d  %d  %f\n", pb->name, pb->num, pb->age, pb->score);
    }
    fclose(fp);
    return 0;
}
//編譯與執(zhí)行
>gcc -g freadwrite.c -o freadwritestu.exe
>./freadwritestu
Input data:
xiong 1 50 59.6
xu 2 37 98.9
xiong  1  50  59.599998
xu  2  37  98.900002

C語言fscanf和fprintf函數(shù)

????????fscanf() 和 fprintf() 函數(shù)與前面使用的 scanf() 和 printf() 功能相似,都是格式化讀寫函數(shù),兩者的區(qū)別在于 fscanf() 和 fprintf() 的讀寫對象不是鍵盤和顯示器,而是磁盤文件。

這兩個函數(shù)的原型為:

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

fp 為文件指針,format 為格式控制字符串,... 表示參數(shù)列表。
與 scanf() 和 printf() 相比,它們僅僅多了一個 fp 參數(shù)。

例如:

#include <stdio.h>
#include <stdlib.h>
int main(){
    FILE *fp;
    int i;
    if( (fp=fopen("./demo.txt", "r+")) == NULL ){  //r+,表示打開文件后可讀寫,但不會清空原文件內(nèi)容
        puts("Fail to open file!");
        exit(0);
    }
     //讀文件中的數(shù)據(jù)到數(shù)組v中,再打印v數(shù)組內(nèi)容
     char v[100] = {0};
     fscanf(fp,"%s",v);
     printf("%s \n",v);
     
    //用完了文件后,一定要關(guān)閉
    if(fclose(fp) == 0){
        puts("\n關(guān)閉文件成功!??!");
    }else{
        puts("文件關(guān)閉失敗");
        exit(0);
    }
    return 0;
}

image.png

寫數(shù)據(jù)到文件中fprintf函數(shù)測試

#include <stdio.h>
#include <stdlib.h>
int main(){
    FILE *fp;
    int i;
    if( (fp=fopen("./demo.txt", "r+")) == NULL ){  //r+,表示打開文件后可讀寫
        puts("Fail to open file!");
        exit(0);
    }
    char v[100] = {0};
    
     //讀文件到數(shù)組中
     //fscanf(fp,"%s",v);
     //printf("%s \n",v);

     //寫數(shù)據(jù)到文件中,       這里代碼功能,只是測試用,讀和寫我們要注釋一個,不然有異常結(jié)果
     scanf("%100[^\n]",v);
     fprintf(fp,"%s",v);
   
    //用完了文件后,一定要關(guān)閉
    if(fclose(fp) == 0){
        puts("\n關(guān)閉文件成功?。。?);
    }else{
        puts("文件關(guān)閉失敗");
        exit(0);
    }
    return 0;
}

image.png

【示例】用 fscanf 和 fprintf 函數(shù)來完成對學(xué)生信息的讀寫。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//定義學(xué)生信息的結(jié)構(gòu)體,兩個結(jié)構(gòu)體數(shù)組變量和指針
struct stu
{
    char name[10];
    int num;
    int age;
    float score;
} boya[2],boyb[2],*pa,*pb;

int main(){
    FILE *fp;
    int i;
    if( (fp=fopen("./demo.txt", "r+")) == NULL ){  //r+,表示打開文件后可讀寫,沒有文件不創(chuàng)建,不清除原內(nèi)容
        puts("Fail to open file!");
        exit(0);
    }
    
    //從健盤中獲取數(shù)據(jù),保存到boya,后面再寫入到文件中,
    pa = boya;            //數(shù)組名代表當(dāng)前數(shù)組的地址(頭地址)
    pb = boyb;
    printf("請輸入學(xué)生信息:  ");
    for(i=0;i<2;i++,pa++){
        scanf("%s %d %d %f",pa->name,&pa->num,&pa->age,&pa->score);  //字符數(shù)組名就是地址了,不用&取地址符
    }
    //寫入文件,重新把pa指向首址,因為上面pa已指向了尾地址了
    pa = boya;
    for(i=0;i<2;i++,pa++){
        fprintf(fp,"%s %d %d %f\n",pa->name,pa->num,pa->age,pa->score);
    }

    //讀文件到boyb中
    rewind(fp);            //文件內(nèi)容指針重指到頭位置
    for(i=0;i<2;i++,pb++){
        fscanf(fp,"%s %d %d %f",pb->name,&pb->num,&pb->age,&pb->score);
    }
    pb = boyb;
    for(i=0;i<2;i++,pb++){
        printf("%s %d %d %f\n",pb->name,pb->num,pb->age,pb->score);
    }


    //用完了文件后,一定要關(guān)閉
    if(fclose(fp) == 0){
        puts("\n關(guān)閉文件成功?。?!");
    }else{
        puts("文件關(guān)閉失敗");
        exit(0);
    }
    return 0;
}
//編譯執(zhí)行
>gcc -g fscanffprintfstu.c -o fscanfprintfstu.exe
>./fscanfprintfstu
輸入學(xué)生信息:  
xiongwen 20240430 41 92.5
xuhuifeng 20240429 37 98.5
                               //顯示的是boyb中的數(shù)據(jù)
xiongwen 20240430 41 92.500000
xuhuifeng 20240429 37 98.500000

關(guān)閉文件成功!?。?

??????打開 當(dāng)前./demo.txt,發(fā)現(xiàn)文件的內(nèi)容是可以閱讀的,格式非常清晰。
??????用 fprintf() 和 fscanf() 函數(shù)讀寫配置文件、日志文件會非常方便,不但程序能夠識別,用戶也可以看懂,可以手動修改,因為它是生產(chǎn)了文本文件,不像其它一樣要用專門軟件格式打開之。

???如果將 fp 設(shè)置為 stdin,那么 fscanf() 函數(shù)將會從鍵盤讀取數(shù)據(jù),與 scanf 的作用相同;
???設(shè)置為 stdout,那么 fprintf() 函數(shù)將會向顯示器輸出內(nèi)容,與 printf 的作用相同。
例如:

#include<stdio.h>
int main(){
    int a, b, sum;
    fprintf(stdout, "Input two numbers: ");
    fscanf(stdin, "%d %d", &a, &b);
    sum = a + b;
    fprintf(stdout, "sum=%d\n", sum);
    return 0;
}

rewind和fseek函數(shù)-移動文件指針

??????前面介紹的文件讀寫函數(shù)都是順序讀寫,即讀寫文件只能從頭開始,依次讀寫各個數(shù)據(jù)。但在實際開發(fā)中經(jīng)常需要讀寫文件的中間部分,要解決這問題,就得先移動文件內(nèi)部的位置指針,再進(jìn)行讀寫。
???這種讀寫方式稱為隨機(jī)讀寫,也就是說從文件的任意位置開始讀寫。

移動文件內(nèi)部位置指針的函數(shù)主要有兩個,即 rewind() 和 fseek()。
??????rewind() 用來將位置指針移動到文件開頭,前面已經(jīng)多次使用過,它的原型為:

void rewind ( FILE *fp );

??????fseek() 用來將位置指針移動到任意位置,它的原型為:

int fseek ( FILE *fp, long offset, int origin );

參數(shù)說明:

  1. fp 為文件指針,也就是被移動的文件。

  2. offset 為偏移量,也就是要移動的字節(jié)數(shù)。
    之所以為 long 類型,是希望移動的范圍更大,能處理的文件更大。
    offset 為正時,向后移動;offset 為負(fù)時,向前移動。

  3. origin 為起始位置,也就是從何處開始計算偏移量。
    C語言規(guī)定的起始位置有三種,分別為文件開頭、當(dāng)前位置和文件末尾,每個位置都用對應(yīng)的常量來表示:


    image.png

    例如,把位置指針移動到離文件開頭100個字節(jié)處:

fseek(fp, 100, 0);

-----值得說明的是,fseek() 一般用于二進(jìn)制文件(打開文件模式為b),在文本文件中由于要進(jìn)行轉(zhuǎn)換,計算的位置有時會出錯。

文件的隨機(jī)讀寫
??????在移動位置指針之后,就可以用前面介紹的任何一種讀寫函數(shù)進(jìn)行讀寫了。
由于是二進(jìn)制文件,因此常用 fread() 和 fwrite() 讀寫。

【示例】從鍵盤輸入三組學(xué)生信息,保存到文件中,然后讀取第二個學(xué)生的信息。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//定義學(xué)生信息的結(jié)構(gòu)體,兩個結(jié)構(gòu)體數(shù)組變量和指針
struct stu
{
    char name[10];
    int num;
    int age;
    float score;
} boys[3],boy,*pboys;

int main(){
    FILE *fp;
    int i;
    if( (fp=fopen("./demo.txt", "w+b")) == NULL ){  //w+b,表示打開文件后可讀寫,沒有文件創(chuàng)建,清除原內(nèi)容
        puts("Fail to open file!");
        exit(0);
    }
    
    //從健盤中獲取數(shù)據(jù),保存到boys,后面再寫入到文件中,
    pboys = boys;            //數(shù)組名代表當(dāng)前數(shù)組的地址(頭地址)
    printf("請輸入學(xué)生信息:  ");
    for(i=0;i<3;i++,pboys++){
        scanf("%s %d %d %f",pboys->name,&pboys->num,&pboys->age,&pboys->score);  //字符數(shù)組名就是地址了,不用&取地址符
    }
    //寫入文件,寫本個數(shù)據(jù)塊,一個塊大小為結(jié)構(gòu)體所占用的數(shù)據(jù)空間
    fwrite(boys,sizeof(struct stu),3,fp);
    

    //讀文件到boy中,寫入三個學(xué)生信息,我們只讀第二學(xué)生信息
    fseek(fp,sizeof(struct stu),SEEK_SET);  
    fread(&boy,sizeof(struct stu),1,fp);
    printf("\n第二個學(xué)生為:\n");
    printf("%s %d %d %f",boy.name,boy.num,boy.age,boy.score);

    //用完了文件后,一定要關(guān)閉
    if(fclose(fp) == 0){
        puts("\n關(guān)閉文件成功?。?!");
    }else{
        puts("文件關(guān)閉失敗");
        exit(0);
    }
    return 0;
}
//編譯執(zhí)行結(jié)果
> gcc -g freadwritestufseek.c -o fseek.exe        
> ./fseek                                         
請輸入學(xué)生信息:  
xiongshaowen 1 41 96.5
xuhuifeng 2 37 98.5
huangren 3 36 90.1

第二個學(xué)生為:
xuhuifeng 2 37 98.500000
關(guān)閉文件成功?。?!

C語言實現(xiàn)文件復(fù)制功能

??????文件的復(fù)制是常用的功能,要求寫一段代碼,讓用戶輸入要復(fù)制的文件以及新建的文件,然后對文件進(jìn)行復(fù)制。能夠復(fù)制的文件包括文本文件和二進(jìn)制文件,你可以復(fù)制1G的電影,也可以復(fù)制1Byte的txt文檔。

??????實現(xiàn)文件復(fù)制的主要思路是:開辟一個緩沖區(qū),不斷從原文件中讀取內(nèi)容到緩沖區(qū),每讀取完一次就將緩沖區(qū)中的內(nèi)容寫入到新建的文件,直到把原文件的內(nèi)容讀取完。

這里有兩個關(guān)鍵的問題需要解決:

  1. 開辟多大的緩沖區(qū)合適?緩沖區(qū)過小會造成讀寫次數(shù)的增加,過大也不能明顯提高效率。目前大部分磁盤的扇區(qū)都是4K對齊的,如果讀寫的數(shù)據(jù)不是4K的整數(shù)倍,就會跨扇區(qū)讀取,降低效率,所以我們開辟4K的緩沖區(qū)。

  2. 緩沖區(qū)中的數(shù)據(jù)是沒有結(jié)束標(biāo)志的,如果緩沖區(qū)填充不滿,如何確定寫入的字節(jié)數(shù)?最好的辦法就是每次讀取都能返回讀取到的字節(jié)數(shù)。

fread() 的原型為:

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

???它返回成功讀寫的塊數(shù),該值小于等于 count。
???如果我們讓參數(shù) size 等于1,那么返回的就是讀取的字節(jié)數(shù)。

代碼實現(xiàn):

#include <stdio.h>
#include <stdlib.h>
//實現(xiàn)復(fù)制文件函數(shù)
int copyFile(char * fileRead,char * fileWrite){
    FILE * fpRead;     //指向要復(fù)制的文件
    FILE * fpWrite;    //指向復(fù)制后的文件
    //設(shè)置緩沖區(qū)
    int bufferLen = 4*1024;   //4k
    char * buffer = (char *)malloc(bufferLen);   //動態(tài)分配緩沖區(qū)
    if((fpRead = fopen(fileRead,"rb")) == NULL || (fpWrite = fopen(fileWrite,"wb")) == NULL){
        printf("打開文件失??!\n");
        exit(1);     //打開文件失敗,異常退出,exit(0),沒問題時退出
    }
    //不斷讀取源文件的數(shù)據(jù),放到目標(biāo)文件中,下面代碼是很多語言通用形式
    int readCount = 0;
    while((readCount = fread(buffer,1,bufferLen,fpRead)) >0){
        fwrite(buffer,readCount,1,fpWrite);
    }

    free(buffer);     //功能寮現(xiàn)完成后,釋放緩沖區(qū)
    fclose(fpRead);
    fclose(fpWrite);
    return 1;
}
int main(){
    char fileRead[100];    //源文件名字
    char fileWrite[100];   //目標(biāo)文件名字
    //用戶輸入要復(fù)制的文件
    printf("請輸入要復(fù)制的文件的名字: \n");
    scanf("%s",fileRead);
    printf("請輸入將文件復(fù)制的文件名字: \n");
    scanf("%s",fileWrite);
    //調(diào)用函數(shù)復(fù)制文件
    if(copyFile(fileRead,fileWrite)){
        printf("復(fù)制成功! \n");
    }else{
        printf("復(fù)制失??! \n");
    }
    return 0;
}
//編譯執(zhí)行效果
 >gcc -g freadwritefilecopy.c -o copyfile.exe
 >./copyfile
 請輸入要復(fù)制的文件的名字,不輸入絕對路徑,為當(dāng)前目錄中:
c:\\a.txt 
請輸入將文件復(fù)制的文件名字:
c:\\b.txt
復(fù)制成功!

C語言獲取文件大小

??????實際開發(fā)中,有時候需要先獲取文件大小再進(jìn)行下一步操作。
C語言沒有提供獲取文件大小的函數(shù),要想實現(xiàn)該功能,必須自己編寫函數(shù)。

ftell()函數(shù)
???ftell() 函數(shù)用來獲取文件內(nèi)部指針(位置指針)距離文件開頭的字節(jié)數(shù),它的原型為:

long int ftell ( FILE * fp );

注意:fp 要以二進(jìn)制方式打開,如果以文本方式打開,函數(shù)的返回值可能沒有意義。

??????先使用 fseek() 將文件內(nèi)部指針定位到文件末尾,再使用 ftell() 返回內(nèi)部指針距離文件開頭的字節(jié)數(shù),這個返回值就等于文件的大小。

long fsize(FILE *fp){
    fseek(fp, 0, SEEK_END);
    return ftell(fp);
}

??????這段代碼并不健壯,它移動了文件內(nèi)部指針,可能會導(dǎo)致接下來的文件操作錯誤。
??????所以,獲取到文件大小后還需要恢復(fù)文件內(nèi)部指針,不然其它地方會出問題,請看下面的代碼:

long fsize(FILE *fp){
    long n;
    fpos_t fpos;  //當(dāng)前位置
    fgetpos(fp, &fpos);  //獲取當(dāng)前位置
    fseek(fp, 0, SEEK_END);
    n = ftell(fp);
    fsetpos(fp,&fpos);  //恢復(fù)之前的位置
    return n;
}

??????fpos_t 是在 stdio.h 中定義的結(jié)構(gòu)體,用來保存文件的內(nèi)部指針。
??????fgetpos() 用來獲取文件內(nèi)部指針,fsetpos() 用來設(shè)置文件內(nèi)部指針。

完整的示例:

#include<stdio.h>
#include<stdlib.h>
#include<conio.h>
long fsize(FILE *fp);
int main(){
    long size = 0;
    FILE *fp = NULL;
    char filename[30] = "c:\\2.png";
    if( (fp = fopen(filename, "rb")) == NULL ){  //以二進(jìn)制方式打開文件
        printf("Failed to open %s...", filename);
        getch();
        exit(EXIT_SUCCESS);
    }
   
    printf("%ld\n", fsize(fp));
    return 0;
}

long fsize(FILE *fp){
    long n;
    fpos_t fpos;  //當(dāng)前位置
    fgetpos(fp, &fpos);  //獲取當(dāng)前位置,并保存在fpos中
    fseek(fp, 0, SEEK_END);
    n = ftell(fp);
    fsetpos(fp,&fpos);  //恢復(fù)之前的位置
    return n;
}
image.png

C語言插入、刪除、更改文件內(nèi)容

??????C語言沒有提供插入、刪除、修改文件內(nèi)容的函數(shù),要想實現(xiàn)這些功能,只能自己編寫函數(shù)。
???我們平時所見的文件,例如 txt、doc、mp4 等,文件內(nèi)容是按照從頭到尾的順序依次存儲在磁盤上的,就像排起一條長長的隊伍,稱為順序文件。
??????除了順序文件,還有索引文件、散列文件等,一般用于特殊領(lǐng)域,例如數(shù)據(jù)庫、高效文件系統(tǒng)等。
??????以插入數(shù)據(jù)為例,假設(shè)原來文件的大小為 1000 字節(jié),現(xiàn)在要求在500字節(jié)處插入用戶輸入的字符串,那么可以這樣來實現(xiàn):

1) 創(chuàng)建一個臨時文件,將后面500字節(jié)的內(nèi)容復(fù)制到臨時文件;
2) 將原來文件的內(nèi)部指針調(diào)整到500字節(jié)處,寫入字符串;
3) 再將臨時文件中的內(nèi)容寫入到原來的文件。

??????刪除數(shù)據(jù)時,也是類似的思路。假設(shè)原來文件大小為1000字節(jié),名稱為 demo.mp4,現(xiàn)在要求在500字節(jié)處往后刪除100字節(jié)的數(shù)據(jù),那么可以這樣來實現(xiàn):

1) 創(chuàng)建一個臨時文件,先將前500字節(jié)的數(shù)據(jù)復(fù)制到臨時文件,再將600字節(jié)之后的所有內(nèi)容復(fù)制到臨時文件;
2) 刪除原來的文件,并創(chuàng)建一個新文件,命名為 demo.mp4;
3) 將臨時文件中的所有數(shù)據(jù)復(fù)制到 demo.mp4。

??????修改數(shù)據(jù)時,如果新數(shù)據(jù)和舊數(shù)據(jù)長度相同,那么設(shè)置好內(nèi)部指針,直接寫入即可;
??????如果新數(shù)據(jù)比舊數(shù)據(jù)長,相當(dāng)于增加新內(nèi)容,思路和插入數(shù)據(jù)類似;
??????如果新數(shù)據(jù)比舊數(shù)據(jù)短,相當(dāng)于減少內(nèi)容,思路和刪除數(shù)據(jù)類似。

???實際開發(fā)中,我們往往會保持新舊數(shù)據(jù)長度一致,以減少編程的工作量,所以我們不再討論新舊數(shù)據(jù)長度不同的情況。

總起來說,本節(jié)重點討論數(shù)據(jù)的插入和刪除。
文件復(fù)制函數(shù)
??????在數(shù)據(jù)的插入刪除過程中,需要多次復(fù)制文件內(nèi)容,我們有必要將該功能實現(xiàn)為一個函數(shù),如下所示:

/**
 * fsource :要復(fù)制的源文件
 * offsetSource:開發(fā)復(fù)制的位置
 * len:復(fù)制數(shù)據(jù)的長度
 * fTarget:數(shù)據(jù)復(fù)制到的目標(biāo)文件
 * offsetTarget:放數(shù)據(jù)到目標(biāo)文件的這個位置
 **/
long fcopy(FILE *fsource, long offsetSource, long len, FILE *fTarget,
           long offsetTarget) {
    int bufferLen = 4 * 1024;
    char *buffer = (char *)malloc(bufferLen);
    int readCount = 0;  //每次讀取的數(shù)據(jù)長度
    long nBtytes = 0;   //記錄已經(jīng)讀了的數(shù)據(jù)長度
    int n = 0;  //需要調(diào)用多少次fread,源文件要被讀取多少次,能讀完數(shù)據(jù)

    //重置源文件和目標(biāo)文件的位置指針
    fseek(fsource, offsetSource, SEEK_SET);
    fseek(fTarget, offsetTarget, SEEK_SET);

    //開始復(fù)制內(nèi)容
    if (len < 0) {  //從開始位置開始,后面的所有內(nèi)容全部復(fù)制
        while ((readCount = fread(buffer, 1, bufferLen, fsource)) > 0) {
            nBtytes += readCount;
            fwrite(buffer, readCount, 1, fTarget);
        }
    } else {  //從開始位置復(fù)制len長度的數(shù)據(jù)
        n = (int)ceil(
            (double)((double)len / bufferLen));  // ceil函數(shù)向上取整,1.1===2 要include <math.h>
        for (int i = 1; i <= n; i++) {
            if ((len - nBtytes) < bufferLen) {
                bufferLen = len - nBtytes;
            }
            readCount = fread(buffer, 1, bufferLen, fsource);
            fwrite(buffer, readCount, 1, fTarget);
            nBtytes += readCount;
        }
    }
    fflush(fTarget);  //刷新系統(tǒng)緩沖區(qū),保證數(shù)據(jù)都進(jìn)入到目標(biāo)文件的磁盤上去
    free(buffer);
    return nBtytes;
}

-------------該函數(shù)可以將原文件任意位置的任意長度的內(nèi)容復(fù)制到目標(biāo)文件的任意位置,非常靈活。
那么可以像這面這樣調(diào)用:

fcopy(fSource, 0, -1, fTarget, 0);

文件內(nèi)容插入函數(shù)
先看代碼:

/**
 * filename:要插入內(nèi)容的文件名字,包含路徑
 * offset:要插入內(nèi)容的位置
 * buffer:要插入的內(nèi)容
 * len:要插入的內(nèi)容的長度
 * */
int finsert(char *filename, long offset, void *buffer, int len) {
    //打開文件
    FILE *fp = NULL;
    if ((fp = fopen(filename, "r+b")) == NULL) {
        puts("fail to open file!");
        return -1;  //操作失敗
    }

    //獲取目標(biāo)文件的長度
    long fileSize = fsize(fp);
    if (offset > fileSize || offset < 0 || len < 0) {
        return -1;
    }

    if (offset == fileSize) {         //插入的數(shù)據(jù)是直接在末尾
        fseek(fp, offset, SEEK_SET);  //位置指針移動到了文件的末尾
        if (!fwrite(buffer, len, 1, fp)) {
            return -1;
        }
    }

    if (offset < fileSize) {
        FILE *fpTemp =
            tmpfile();  //自動創(chuàng)建臨時文件,可讀寫,fclose后,臨時文件會自動刪除
        fcopy(fp, 0, offset, fpTemp, 0);  //靈活的文件復(fù)制函數(shù)就被使用了
        fwrite(buffer, len, 1, fpTemp);  //把插入的內(nèi)容寫到臨時文件的尾巴上
        fcopy(fp, offset, -1, fpTemp,
              offset + len);  //把源文件的后半部分復(fù)制到臨時文件的尾巴上
        freopen(filename, "wb+", fp);
        fcopy(fpTemp, 0, -1, fp, 0);
        fclose(fpTemp);
    }
    fclose(fp);
    return 0;  //操作成功
}

代碼說明:

  1. fsize() 是前面自定義的函數(shù),用來獲取文件大?。ㄒ宰止?jié)計)。
  2. 第17行判斷數(shù)據(jù)的插入位置,如果是在文件末尾,就非常簡單了,直接用 fwrite() 寫入即可。
  3. 如果從文件開頭或中間插入,就得創(chuàng)建臨時文件。

tmpfile() 函數(shù)用來創(chuàng)建一個臨時的二進(jìn)制文件,可以讀取和寫入數(shù)據(jù),相當(dāng)于 fopen() 函數(shù)以"wb+"方式打開文件。該臨時文件不會和當(dāng)前已存在的任何文件重名,并且會在調(diào)用 fclose() 后或程序結(jié)束后自動刪除。

文件內(nèi)容刪除函數(shù)
看下面的代碼:

int fdelete(char *filename, long offset, int len) {
    //打開文件
    FILE *fp = NULL;
    if ((fp = fopen(filename, "r+b")) == NULL) {
        puts("fail to open file!");
        return -1;  //操作失敗
    }

    //獲取目標(biāo)文件的長度
    long fileSize = fsize(fp);
    if (offset > fileSize || offset < 0 || len < 0) {
        return -1;
    }

    FILE *fpTemp =
        tmpfile();  //自動創(chuàng)建臨時文件,可讀寫,fclose后,臨時文件會自動刪除
    fcopy(fp, 0, offset, fpTemp, 0);  //靈活的文件復(fù)制函數(shù)就被使用了
    fcopy(fp, offset + len, -1, fpTemp,
          offset);  //把源文件的后半部分復(fù)制到臨時文件的尾巴上
    freopen(filename, "wb+", fp);
    fcopy(fpTemp, 0, -1, fp, 0);
    fclose(fpTemp);

    fclose(fp);
    return 0;  //操作成功
 }

調(diào)用,我們設(shè)置了一個字符指針,字符有123456789,分別插入,再刪除,最終等于對demo.txt文件什么也沒做

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
int main(){
    char *str ="123456789";
    //finsert("./demo.txt",3,str,9);
    fdelete("./demo.txt",3,9);
    return 0;
}
//編譯執(zhí)行,先插入,再刪除,分別注釋不同的代碼進(jìn)行測試
>gcc -g main.c -o main.exe
>./main
//demo.txt文件的變化
aaaaaaaaaa
aaa123456789aaaaaaa
aaaaaaaaaa

系統(tǒng)調(diào)用IO

什么是文件描述符
???????文件描述符:File descriptor,簡稱fd,當(dāng)應(yīng)用程序請求內(nèi)核打開/新建一個文件時(操作系統(tǒng)的核心叫內(nèi)核,是一個獨立的軟件),內(nèi)核會返回一個文件描述符用于對應(yīng)這個打開/新建的文件,其fd本質(zhì)上就是一個非負(fù)整數(shù)。
???????實際上,fd是一個索引值,指向內(nèi)核為每一個進(jìn)程所維護(hù)的該進(jìn)程打開文件的記錄表。

???????操作系統(tǒng)會為每一個進(jìn)程維護(hù)了一個文件描述符表,該表的索引值都從從0開始的,所以在不同的進(jìn)程中可以看到相同的文件描述符,這種情況下相同的文件描述符可能指向同一個文件,也可能指向不同的文件,具體情況需要具體分析,下面用一張簡圖就可以很容易的明白了:


1.png

當(dāng)一個應(yīng)用程序剛剛啟動的時候,0是標(biāo)準(zhǔn)輸入,1是標(biāo)準(zhǔn)輸出,2是標(biāo)準(zhǔn)錯誤。
如果此時去打開一個新的文件,它的文件描述符會是3。
IEEE的可移植操作系統(tǒng)(POSIX)標(biāo)準(zhǔn)要求每次打開文件時必須使用當(dāng)前進(jìn)程中最小可用的文件描述符號
什么是inode:
???????文件存儲在硬盤上,硬盤的最小存儲單位叫做“扇區(qū)”(Sector)。每個扇區(qū)儲存512字節(jié)(相當(dāng)于0.5KB)。

???????操作系統(tǒng)讀取硬盤的時候,不會一個個扇區(qū)的讀取,這樣效率太低,而是一次性連續(xù)讀取多個扇區(qū),即一次性讀取一個“塊”(block)。這種由多個扇區(qū)組成的“塊”,是文件存取的最小單位?!皦K”的大小,最常見的是4KB,即連續(xù)八個sector組成一個block。

???????文件數(shù)據(jù)都儲存在“塊”中,那么很顯然,我們還必須找到一個地方儲存文件的“元信息”,比如文件的創(chuàng)建者、文件的創(chuàng)建日期、文件的大小等等。這種儲存文件元信息的區(qū)域就叫做inode,中文譯名為"索引節(jié)點"。
???????每一個文件都有對應(yīng)的inode,里面包含了與該文件有關(guān)的一些信息。

???????Unix/Linux系統(tǒng)內(nèi)部不使用文件名,而使用inode號碼來識別文件。對于系統(tǒng)來說,文件名只是inode號碼便于識別的別稱。

open()函數(shù)---對應(yīng)標(biāo)準(zhǔn)IO的fopen

需要引入頭文件:

#include <fcntl.h>
#include <sys/types.h>
#incldue <sys/stat.h>

功能:打開和創(chuàng)建文件(建立一個文件描述符,其他的函數(shù)可以通過文
件描述符對指定文件進(jìn)行讀取與寫入的操作。)

原型

int open(const char*pathname,int flags);
int open(const char*pathname,int flags,mode_t mode);

參數(shù)說明:

  1. pathname
    要打開或創(chuàng)建的目標(biāo)文件
  2. flags,文件的狀態(tài)標(biāo)志
    打開文件時,可以傳入多個參數(shù)選項,用下面的一個或者多個常量進(jìn)行“或”運(yùn)算,構(gòu)成falgs常用參數(shù):
    O_RDONLY: 只讀打開
    O_WRONLY: 只寫打開
    O_RDWR: 讀,寫打開
    上面三個常量,必須指定一個且只能指定一個。
    O_APPEND: 追加寫,如果文件已經(jīng)有內(nèi)容,
    這次打開文件所寫的數(shù)據(jù)附加到文件的末尾而不覆蓋原來的內(nèi)容。
    O_NONBLOCK、SYNC、O_ASYNC:這些和網(wǎng)絡(luò)通信有關(guān),學(xué)到后面在了解。
    O_CREAT: 若文件不存在,則創(chuàng)建它,需要使用mode選項,來指明新文件的訪問權(quán)限。
    使用O_CREAT的時候需要配套第三個參數(shù)是設(shè)定該文件的權(quán)限:
    我們將權(quán)限用數(shù)字表示,其中 r 表示4,w表示2,x表示1,分別是2的0次方,1次方,2次方。
    那么我們可以這樣理解:
    具有 rwx 權(quán)限的數(shù)字就是 7,具有 rw- 權(quán)限的數(shù)字是 6,具有 r-- 權(quán)限的數(shù)字是 4。
    我們常用的權(quán)限的數(shù)字模式又這幾種:
    644:rw-r--r--
    755:rwxr-xr-x
    777:rwxrwxrwx
    返回值
    成功:新打開的文件描述符
    失?。?1

例子:

#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>

int main() {
    int filefd = 0;
    if ((filefd = open("./test.txt", O_CREAT | O_RDWR,0666)) == -1) {
        printf("++++++++++++++=open file fail\n");
        return -1;
    }
    return 0;
}

//在使用open()建立新文件時,該參數(shù)mode并非真正建立文件的權(quán)限,
//而是(mode&~umask)的權(quán)限值。
//例如,在建立文件時指定文件權(quán)限為0666,通常umask值默認(rèn)為022,
//則該文件的真正權(quán)限則為0666&~022=0644,也就是rw-r-r--。

close()函數(shù)---對應(yīng)標(biāo)準(zhǔn)IO的fclose

??????close用于關(guān)閉一個已打開文件。
進(jìn)程終止時,內(nèi)核會自動關(guān)閉它所有的打開文件,應(yīng)用程序經(jīng)常利用這一點而不顯式關(guān)閉文件。
需要引入頭文件:

#include <unistd.h>

原型

int close(int fd);

返回值:
成功返回0,失敗返回-1

例子:

#include<stdio.h>
#include<unistd.h>
#include<fcntl.h>

int main(){
        int fd=open("./test.txt",O_RDWR|O_CREAT,0666);
        if(fd<0){
                perror("open error");
                return -1;
        }
        printf("%d\n",fd);
        close(fd);
        return 0;
}

read()函數(shù)

???read用于從打開文件中讀數(shù)據(jù)。
需要引入頭文件:

#include <unistd.h>

原型

ssize_t read(int fd, void *buf, size_t count);

參數(shù)說明:
???fd:open返回的文件描述符-文件操作句柄,,通過fd指定要往哪個文件讀數(shù)據(jù)
???buf:從文件中讀取數(shù)據(jù)放到哪塊緩沖區(qū)的首地址
???count:想要讀取的數(shù)據(jù)長度,注意這個count不能大于緩沖區(qū)的大小
返回值:
???成功返回讀到的字節(jié)數(shù);若讀到文件尾則返回0;失敗返回-1

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

int main() {
    int filefd = 0;  //相當(dāng)于標(biāo)準(zhǔn)i/o中的 FILE *fp,用于后面的文件操作
    if ((filefd = open("./test.txt", O_CREAT | O_RDWR,0666)) == -1) {
        printf("++++++++++++++=open file fail\n");
        return -1;
    }
     char buf[1024] = {0};     //設(shè)一個緩沖區(qū),最好賦一個初值,以免出現(xiàn)不必要的異常
     //文件數(shù)據(jù)
     int ret = read(filefd,buf,1024);       //成功返回讀到的字節(jié)數(shù);若讀到文件尾則返回0;失敗返回-1
     if(ret <0){
        printf("讀取異常!\n");
        exit(1);
     }
     printf("讀取文件的數(shù)據(jù)大小和內(nèi)容為: %d-\n%s\n",ret,buf);
     printf("文件描述號: %d\n",filefd);     //顯示文件描述符
     close(filefd);                       //關(guān)閉文件
    return 0;
}

//編譯與執(zhí)行
[root@localhost os_io]# gcc -g read.c -o read
[root@localhost os_io]# ./read
讀取文件的數(shù)據(jù)大小和內(nèi)容為: 60-
xiongshaowen熊少文
xuhuifeng
xionglinzhou
xionglinxiao
文件描述號: 3

write()函數(shù)

??????向文件寫入數(shù)據(jù)。

#include <unistd.h>

//成功返回寫入的字節(jié)數(shù),失敗返回-1

ssize_t write(int fd, const void *buf, size_t count);

參數(shù)
???fd:open返回的文件描述符-文件操作句柄,,通過fd指定要往哪個文件寫入數(shù)據(jù)
???buf:要寫入文件的數(shù)據(jù)的空間首地址
???count:要寫入的數(shù)據(jù)大小
???返回值:返回實際寫入文件的數(shù)據(jù)字節(jié)長度,錯誤返回-1

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

int main() {
    int fd = 0,fdb=0;  //相當(dāng)于標(biāo)準(zhǔn)i/o中的 FILE *fp,用于后面的文件操作
    if ((fd = open("./test.txt", O_CREAT | O_RDWR,0666)) == -1) {
        printf("++++++++++++++=open file fail\n");
        return -1;
    }
     char buf[1024] = {0};     //設(shè)一個緩沖區(qū),最好賦一個初值,以免出現(xiàn)不必要的異常
     //讀文件數(shù)據(jù)
     int ret = read(fd,buf,1024);       //成功返回讀到的字節(jié)數(shù);若讀到文件尾則返回0;失敗返回-1
     if(ret <0){
        printf("讀取異常!\n");
        exit(1);
     }
      printf("讀取文件的數(shù)據(jù)大小和內(nèi)容為: %d-\n%s\n",ret,buf);

     //把buf中讀到的數(shù)據(jù)保存到另一個文件(testb.txt)中。
     if((fdb=open("./testb.txt",O_RDWR | O_CREAT,0666)) == -1){
        printf("file testb.txt open fail!\n");
        return -1;
     }
     int retb = write(fdb,buf,strlen(buf));
     if(retb <0){
        printf("寫數(shù)據(jù)異常!\n");
        exit(1);
     }

     printf("兩文件描述號: %d %d\n",fd,fdb);     //顯示文件描述符
     close(fd);                       //關(guān)閉文件
     close(fdb);
    return 0;
}

lseek()函數(shù)---標(biāo)準(zhǔn)IO中fseek函數(shù)
???lseek用于設(shè)置打開文件的偏移量。

#include <sys/types.h>
#include <unistd.h>

成功返回新的文件偏移量,失敗返回-1

off_t lseek(int fd, off_t offset, int whence);

對offset的解釋取決于whence的值:
???若whence == SEEK_SET,則將文件偏移量設(shè)為距文件開頭offset個字節(jié),此時offset必須為非負(fù)
???若whence == SEEK_CUR,則將文件偏移量設(shè)為當(dāng)前值 + offset,此時offset可正可負(fù)
???若whence == SEEK_END,則將文件偏移量設(shè)為文件長度 + offset,此時offset可正可負(fù)
注意:
??????lseek僅將新的文件偏移量記錄在內(nèi)核中,它并不引起任何IO操作,因此它不是系統(tǒng)調(diào)用IO,但該偏移量會用于下一次read/write操作。

dup()和dup2()函數(shù)

??????兩個函數(shù)都可以來復(fù)制一個現(xiàn)有的文件描述符,使多個文件描述符指向同一個文件,也可以把標(biāo)準(zhǔn)輸入和輸出換成某個文件的輸入輸出:

#include <unistd.h>
 int  dup(int fd);
 int dup2(int fd, int fd2);

???關(guān)于dup函數(shù),當(dāng)我們調(diào)用它的時候,dup會返回一個新的描述符,這個描述一定是當(dāng)前可用文件描述符中的最小值。

??????我們知道,一般的0,1,2描述符分別被標(biāo)準(zhǔn)輸入、輸出、錯誤占用,所以在程序中如果close(1)關(guān)掉標(biāo)準(zhǔn)輸出,調(diào)用dup函數(shù),此時返回的描述符就是1。

???對于dup2,可以用fd2指定新描述符的值,如果fd2本身已經(jīng)打開了,則會先將其關(guān)閉。
返回fd2。如果fd剛好和fd2相等,就直接返回fd2,不關(guān)閉。

總之:這兩個函數(shù)返回的描述符與fd描述符所指向的文件共享同一文件表項。

dup和dup2的例子:

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int main() {
    int fd = 0;  //相當(dāng)于標(biāo)準(zhǔn)i/o中的 FILE *fp,用于后面的文件操作
    if ((fd = open("./test.txt", O_CREAT | O_RDWR,0666)) == -1) {
        printf("++++++++++++++=open file fail\n");
        return -1;
    }

    if(write(fd,"hello 111111\n",13)!=13){  //寫入之前如果文件存在,手動清除原內(nèi)容,
        printf("寫錯誤!");
        exit(1);
    }
    //int dup_fd = dup(fd); //或   
    int dup_fd = dup(fd,4);               //復(fù)制文件描述符,重新指定描述符,
    printf("fd:%d dup_fd:%d\n",fd,dup_fd);   //fd:3 dup_fd:4,此時,4和3是描述指向同一個文件
    if(write(4,"hello 222222\n",13)!=13){    //再往4描述符指向的文件中寫入數(shù)據(jù),是向test.txt寫入的
        printf("寫錯誤!");
        exit(1);
    }
   close(dup_fd); 
   close(fd);
    return 0;
}
2.png

??????我們知道,一般0,1,2描述符分別被標(biāo)準(zhǔn)輸入、輸出、錯誤占用,我在如下代碼中,我讓fd指向1標(biāo)準(zhǔn)輸出,即讓test.txt代替了屏幕文件了。

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int main() {
    int fd = 0;  //相當(dāng)于標(biāo)準(zhǔn)i/o中的 FILE *fp,用于后面的文件操作
    if ((fd = open("./test.txt", O_CREAT | O_RDWR,0666)) == -1) {
        printf("++++++++++++++=open file fail\n");
        return -1;
    }

    if(write(fd,"hello 111111\n",13)!=13){  //寫入之前如果文件存在,手動清除原內(nèi)容,
        printf("寫錯誤!");
        exit(1);
    }
   
    int dup_fd = dup2(fd,1);               //復(fù)制文件描述符,重新指定描述符,
    printf("fd:%d dup_fd:%d\n",fd,dup_fd);   //fd:3 dup_fd:1,此時,1和3是描述指向同一個文件,此時為1,即打印到文件test.txt中了,而不是屏幕上
    fflush(stdout);                   //刷新標(biāo)準(zhǔn)輸出
    close(dup_fd);
    close(fd);
    return 0;
}
3.png

fcntl()和ioctl()函數(shù)

fcntl原型

#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );

功能
??????fcntl函數(shù)其實是File Control的縮寫,通過fcntl可以設(shè)置、或者修改已打開的文件性質(zhì)。
參數(shù)
???fd:指向打開文件
???cmd:控制命令,通過指定不同的宏來修改fd所指向文件的性質(zhì)。
F_DUPFD
用來查找大于或等于參數(shù) arg 的最小且仍未使用的文件描述符,并且復(fù)制參數(shù) fd 的文件描述符。
作用就和dup和dup2一樣,雖然能實現(xiàn)一樣的工程,但項目中推薦用dup和dup2哈!
返回值:返回復(fù)制后的新文件描述

F_GETFL、F_SETFL
取得/設(shè)置文件狀態(tài)標(biāo)志,通過第三個參數(shù)設(shè)置
可以更改的幾個標(biāo)志是:O_APPEND、O_NONBLOCK、SYNC、O_ASYNC
(O_RDONLY、O_WRONLY和O_RDWR不適用)

什么時候需要fcntl來補(bǔ)設(shè)?
當(dāng)文件描述符不是你自己open得到,而是調(diào)用別人給的函數(shù),別人的函數(shù)去open某個文件,
然后再將文件描述符返回給你用,在這種情況下,我們是沒辦法去修改被人的函數(shù),
在他調(diào)用的

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容