一、為什么使用文件
如果沒有文件,我們寫的程序的數據是存儲在電腦的內存中,如果程序退出,內存回收,數據就丟失 了,等再次運行程序,是看不到上次程序的數據的,如果要將數據進行持久化的保存,我們可以使用文件。
二、什么是文件
磁盤(硬盤)上的文件是文件。 但是在程序設計中,我們?般談的文件有兩種:程序文件、數據文件(從文件功能的角度來分類的)
2.1?程序文件
程序文件包括源程序文件(后綴為.c),目標文件(windows環境后綴為.obj),可執行程序(windows 環境后綴為.exe)
2.2 數據文件
文件的內容不?定是程序,而是程序運行時讀寫的數據,比如程序運行需要從中讀取數據的文件,或者輸出內容的文件。
本章討論的是數據文件。
在以前各章所處理數據的輸?輸出都是以終端為對象的,即從終端的鍵盤輸入數據,運行結果顯示到顯示器上。 其實有時候我們會把信息輸出到磁盤上,當需要的時候再從磁盤上把數據讀取到內存中使用,這里處理的就是磁盤上文件。
2.3 文件名
?個文件要有?個唯?的文件標識,以便用戶識別和引用。
文件名包含3部分:文件路徑+文件名主干+文件后綴
例如: c:\code\test.txt
為了方便起見,文件標識常被稱為文件名
三、文件的打開和關閉
3.1 流和標準流
3.1.1 流
我們程序的數據需要輸出到各種外部設備,也需要從外部設備獲取數據,不同的外部設備的輸入輸出操作各不相同,為了方便程序員對各種設備進行方便的操作,我們抽象出了流(stream)的概念,我們可以把 流 想象成 流淌著字符的河。
C程序針對文本件、畫面、鍵盤等的數據輸入輸出操作都是通過流操作的。?般情況下,我們要想向流里寫數據,或者從流中讀取數據,都是要打開流,然后操作。
3.1.2 標準流
那為什么我們從鍵盤輸入數據,向屏幕上輸出數據,并沒有打開流呢? 那是因為C語言程序在啟動的時候,默認打開了3個流:?
? stdin - 標準輸入流(鍵盤),在大多數的環境中從鍵盤輸入,scanf函數就是從標準輸入流中讀取數據。?
? stdout - 標準輸出流(屏幕),大多數的環境中輸出至顯示器界?,printf函數就是將信息輸出到標準輸出 流中。
? stderr - 標準錯誤流(屏幕),大多數環境中輸出到顯示器界?。
這是默認打開了這三個流,我們使用scanf、printf等函數就可以直接進行輸入輸出操作的。?
stdin、stdout、stderr 三個流的類型是: FILE * ,通常稱為文件指針。
C語?中,就是通過 FILE* 的文件指針來維護流的各種操作的。
3.2 文件指針
緩沖文件系統中,關鍵的概念是“文件類型指針”,簡稱“文件指針”。
每個被使用的文件都在內存中開辟了?個相應的文件信息區,用來存放文件的相關信息(如文件的名 字,文件狀態及文件當前的位置等)。這些信息是保存在?個結構體變量中的。該結構體類型是由系統聲明的,取名FILE。
每當打開?個文件的時候,系統會根據文件的情況自動創建?個FILE結構的變量,并填充其中的信息。
?般都是通過?個FILE的指針來維護這個FILE結構的變量,這樣使用起來更加方便。
下?我們可以創建?個 FILE* 的指針變量:
FILE* pf;//文件指針變量
定義pf是?個指向FILE類型數據的指針變量。可以使pf指向某個文件的文件信息區(是?個結構體變量)。通過該文件信息區中的信息就能夠訪問該文件。也就是說,通過文件指針變量能夠間接找到與它關聯的文件。
3.3 文件的打開和關閉
文件在讀寫之前應該先打開文件,在使用結束之后應該關閉文件。
在編寫程序的時候,在打開文件的同時,都會返回?個FILE*的指針變量指向該文件,也相當于建立了指針和文件的關系。
ANSI C 規定使用?fopen 函數來打開文件, fclose 來關閉文件。
它們需要包含頭文件 #include <stdio.h>
//打開文件
FILE * fopen ( const char * filename, const char * mode );
//關閉文件
int fclose ( FILE * stream );
【fclose 如果流成功關閉,則返回零值。 若關閉失敗,則返回 EOF。】
mode表示文件的打開模式,下面都是文件的打開模式:
---------------------------------------------------------------------------------------------------------------------------------
文件使用方式????????????????含義????????????????????????????????????????????????????????????????如果指定文件不存在
“r”(只讀)? ? ? ? ? ? 為了輸入數據,打開?個已經存在的文本文件? ? ? ? 出錯
“w”(只寫)? ? ? ? ? ?為了輸出數據,打開?個文本文件? ? ? ? ? ? ? ? ? ? ? ? ? 建立?個新的文件
“a”(追加)? ? ? ? ? ? 向文本文件尾添加數據? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 建立?個新的文件
“rb”(只讀)? ? ? ? ?? 為了輸入數據,打開?個?進制文件? ? ? ? ? ? ? ? ? ? ? 出錯
“wb”(只寫)? ? ? ? ?為了輸出數據,打開?個?進制文件? ? ? ? ? ? ? ? ? ? ? 建立?個新的文件
“ab”(追加)? ? ? ? ??向?個?進制文件尾添加數據? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?建立?個新的文件
“r+”(讀寫)? ? ? ? ? ?為了讀和寫,打開?個文本文件? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 出錯
“w+”(讀寫)? ? ? ? ??為了讀和寫,建議?個新的文件? ? ? ? ? ? ? ? ? ? ? ? ? ? ?建立?個新的文件
“a+”(讀寫)? ? ? ? ? 打開?個文件,在文件尾進行讀寫? ? ? ? ? ? ? ? ? ? ? ? ? 建立?個新的文件
“rb+”(讀寫)? ? ? ? ?為了讀和寫打開?個?進制文件? ? ? ? ? ? ? ? ? ? ? ? ? ? ?出錯
“wb+”(讀寫)????????為了讀和寫,新建?個新的?進制文件? ? ? ? ? ? ? ? ? 建立?個新的文件?
“ab+”(讀寫)? ? ? ??打開?個?進制文件,在文件尾進行讀和寫? ? ? ? ? ?建立?個新的文件??
---------------------------------------------------------------------------------------------------------------------------------
例如:當我們想讀取此文件路徑?D: \VS2022 c語言 \code \ 下的?test.txt?
要想正確讀取,我們首先是要創建文件路徑,而在創建之前,我們應該先勾選一下選項,再進行創建:
#include <stdio.h>
#include <string.h>
int main()
{FILE* pf = fopen("D:\\VS2022 c語言\\code\\test.txt","r");if(pf == NULL)//需要對返回值進行判斷{perror("fopen");return 1;}else{printf("打開文件成功\n");}//讀文件//……//關閉文件fclose(pf);pf = NULL;return 0;
}
介紹一下什么是絕對路勁和相對路徑:
絕對路徑:D:\\VS2022 c語言\\code\\test.txt
相對路勁:test.txt(指的是當前目錄下的 test.txt)
如果我們想以相對路徑的方式進行打開文件操作,那我們要先找到當前的目錄,然后將之前的文件test.txt剪切到當前目錄下:
FILE* pf = fopen("test.txt","r");
除了以上的方式,想表示當前目錄,還可以用 "./"?表示:
FILE* pf = fopen("./test.txt", "r");
而 "../" 表示上級目錄下的 text.txt:
將文件test.txt 剪切到上一個目錄下:
FILE* pf = fopen("../test.txt", "r");
那么 "../../"? 在文件路徑中表示 上上級目錄(即當前目錄的父目錄的父目錄)
前面我們介紹了文件的12種打開模式,而其中出錯會建立一個新的文件是什么意思呢?
現在我們拿 "w" 來進行舉例:
"w" - 只寫,建??個新的文件(覆蓋已有文件或創建新文件(僅在路徑、權限等條件允許時))
如果目標文件已存在,"w" 模式會清空文件內容(即覆蓋原有數據),并將文件指針定位到開頭,準備寫入新內容。
int main()
{FILE* pf = fopen("test.txt","w");if(pf == NULL){perror("fopen");}else{printf("打開文件成功\n");}//寫文件//……//關閉文件fclose(pf);pf = NULL;return 0;
}
我們打開文件進行寫入操作:
當程序運行起來時,"w" 模式會清空文件內容(即覆蓋原有數據),即建立一個新的文件:
四、文件的順序讀寫
4.1 順序讀寫函數介紹
這些函數都包含頭文件 #include <stdio.h>
------------------------------------------------------------------------------------
函數名? ? ? ? ? ? ? ? ? ? ? ? 功能? ? ? ? ? ? ? ? ? ? ? ? ????????適用于
fgetc? ? ? ? ? ? ? ? ? ? ? ? ? 字符串輸入函數? ? ? ? ? ? ? ?所有輸入流
fputc? ? ? ? ? ? ? ? ? ? ? ? ? 字符串輸出函數? ? ? ? ? ? ? ?所有輸出流?
fgets? ? ? ? ? ? ? ? ? ? ? ? ? 文本行輸入函數? ? ? ? ? ? ? ?所有輸入流
fputs? ? ? ? ? ? ? ? ? ? ? ? ? 文本行輸出函數? ? ? ? ? ? ? ?所有輸出流
fscanf? ? ? ? ? ? ? ? ? ? ? ? 格式化輸入函數? ? ? ? ? ? ? ?所有輸入流
fprintf? ? ? ? ? ? ? ? ? ? ? ? ?格式化輸出函數? ? ? ? ? ? ? ?所有輸出流
fread? ? ? ? ? ? ? ? ? ? ? ? ? 二進制輸入? ? ? ? ? ? ? ? ? ? ? 文本輸入流
fwrite? ? ? ? ? ? ? ? ? ? ? ? ? 二進制輸出? ? ? ? ? ? ? ? ? ? ? 文本輸出流
-----------------------------------------------------------------------------------
上?說的適用于所有輸入流?般指適用于標準輸入流和其他輸入流(如文件輸入流);所有輸出流? 般指適用于標準輸出流和其他輸出流(如文件輸出流)。
4.1.1 fgetc 和 fputc
fputc
向流寫入字符
int fputc ( int character, FILE * stream );
- 將一個字符寫入流并推進位置指示器。該字符會被寫入流的內部位置指示器所指示的位置,隨后該位置指示器會自動向前移動一位。?
- 如果操作成功,則返回所寫入的字符。如果發生寫入錯誤,則返回 `EOF` 并設置錯誤指示器(`ferror`)。?
int main()
{FILE* pf = fopen("test.txt","w");if(pf == NULL) {perror("fopen");return 1;}//寫文件 fputc('a',pf);fputc('b',pf);fputc('c',pf);fputc('d',pf);//關閉文件 fclose(pf);pf = NULL;return 0;
}
注意:要先運行起來后才能看到寫入文件的字符(其他的函數都一樣)
fgetc
從流中獲取字符
int fgetc ( FILE * stream );
- 返回指定流的內部文件位置指示器當前所指向的字符。隨后,內部文件位置指示器將前進到下一個字符。如果在調用該函數時流已到達文件末尾,函數將返回 `EOF` 并為該流設置文件結束指示器(`feof`)。如果發生讀取錯誤,函數將返回 `EOF` 并為該流設置錯誤指示器(`ferror`)。
- 如果操作成功,將返回讀取到的字符(提升為 `int` 類型的值)。返回類型為 `int` 是為了能包含特殊值 `EOF`(-1),該值表示操作失敗:如果位置指示器位于文件末尾,函數將返回 `EOF` 并設置流的文件結束指示器(`feof`)。如果發生其他讀取錯誤,函數同樣返回 `EOF`,但會設置其錯誤指示器(`ferror`)。?
int main()
{FILE* pf = fopen("test.txt","r");if(pf == NULL) {perror("fopen");return 1;}//讀文件 int ch = fgetc(pf);printf("%c\n",ch);ch = fgetc(pf);printf("%c\n",ch);ch = fgetc(pf);printf("%c\n",ch);//關閉文件 fclose(pf);pf = NULL;return 0;
}
4.1.2 fgets 和 fputs
fputs
向流寫入字符串
int fputs ( const char * str, FILE * stream );
- 將 `str` 所指向的 C 字符串寫入流。該函數從指定地址(`str`)開始復制,直到遇到終止空字符 `'\0'` 為止。這個終止空字符不會被復制到流中。
- 如果操作成功,將返回一個非負數值。如果發生錯誤,函數將返回 `EOF` 并設置錯誤指示器(`ferror`)。
int main()
{FILE* pf = fopen("test.txt","w");if(pf == NULL) {perror("fopen");return 1;}//寫文件//測試寫一行數據 fputs("hello world\n",pf);fputs("haha\n",pf);//關閉文件 fclose(pf);pf = NULL;return 0;
}
fgets
從流中獲取字符串
char * fgets ( char * str, int num, FILE * stream );
- 從流中讀取字符,并將它們作為 C 字符串存儲到 `str` 中,直到讀取了 `(num - 1)` 個字符,或者遇到換行符或文件結尾,以先發生的情況為準。換行符會使 `fgets` 停止讀取,但該函數會將其視為有效字符,并將其包含在復制到 `str` 的字符串中。在復制到 `str` 的字符之后,會自動追加一個終止空字符。
- 如果操作成功,函數將返回 `str`。如果在嘗試讀取一個字符時遇到文件末尾,則會設置文件結束指示器(`feof`)。如果在讀取任何字符之前就遇到這種情況,返回的指針將是一個空指針(并且 `str` 的內容保持不變)。如果發生讀取錯誤,將設置錯誤指示器(`ferror`),并且也會返回一個空指針(但 `str` 所指向的內容可能已經改變)。?
int main()
{FILE* pf = fopen("test.txt","r");if(pf == NULL) {perror("fopen");return 1;}//讀文件//測試讀一行數據 char buf[20] = {0};fgets(buf,5,pf);printf("%s\n",buf);//hell - 讀4個字符,是因為第5個字符需要放'\0'fgets(buf,5,pf);printf("%s\n",buf);//關閉文件 fclose(pf);pf = NULL;return 0;
}
4.1.3 fscanf 和 fprintf
fprintf
向流寫入格式化數據
int fprintf ( FILE * stream, const char * format, ... );
- 將 `format` 所指向的 C 字符串寫入流。如果 `format` 中包含格式說明符(以 `%` 開頭的子序列),則 `format` 后面的額外參數將被格式化,并插入到結果字符串中,以替換各自的格式說明符。在 `format` 參數之后,函數期望至少有 `format` 中指定數量的額外參數。?
- 如果操作成功,將返回所寫入的字符總數。如果發生寫入錯誤,將設置錯誤指示器(`ferror`),并返回一個負數。
?
我們對比printf函數,它們的區別就是fprintf函數是向流寫入數據,而print函數只是將數據打印到屏幕上,而 …… 表示可變參數列表
printf可以這樣:printf("hehe");、printf("%s","hehe");、printf("%s %d","hehe",100);等可以在添加,那么fprintf就是多了一個FILE*類型的指針變量
struct S
{char name[20];int age;float score;
};
int main()
{struct S s = {"zhangsan",20,95.5};FILE* pf = fopen("test.txt","w");if(pf == NULL) {perror("fopen");return 1;}//格式化的寫入文件fprintf(pf,"%s %d %f\n",s.name,s.age,s.score);//關閉文件 fclose(pf);pf = NULL;return 0;
}
fscanf
從流中讀取格式化數據
int fscanf ( FILE * stream, const char * format, ... );
- 從流中讀取數據,并根據 `format` 參數將數據存儲到額外參數所指向的位置。額外參數應指向已分配好的對象,這些對象的類型要與格式字符串中相應格式說明符所指定的類型一致。
- 如果操作成功,函數將返回參數列表中成功填充的項數。由于匹配失敗、讀取錯誤或到達文件末尾,此計數可能與預期的項數相符,也可能更少(甚至為零)。如果在讀取時發生讀取錯誤或到達文件末尾,會設置相應的指示器(`feof` 或 `ferror`)。并且,如果在任何數據都未能成功讀取之前就出現上述情況,則返回 `EOF`。
struct S
{char name[20];int age;float score;
};
int main()
{struct S s = {0};FILE* pf = fopen("test.txt","r");if(pf == NULL) {perror("fopen");return 1;}//格式化的讀取文件fscanf(pf,"%s %d %f",s.name,&(s.age),&(s.score));//打印看數據printf("%s %d %f\n",s.name,s.age,s.score);//zhang san 20 95.500000 //關閉文件 fclose(pf);pf = NULL;return 0;
}
4.1.4 通過標準輸入輸入流進行操作
前面我們知道,任何一個C語言程序運行的時候,默認打開3個流:stdin、stdout、stderr
上面這些函數適用于所有輸出、輸入流,因此,我們除了可以通過文件進行輸出、輸入操作,還可以通過stdin等標準輸入、輸出流進行操作
例如:
int main()
{int ch = fgetc(stdin);fputc(ch,stdout);return 0;
}
struct S
{char name[20];int age;float score;
};
int main()
{struct S s = {0};fscanf(stdin,"%s %d %f",s.name,&(s.age),&(s.score));fprintf(stdout,"%s %d %f\n",s.name,s.age,s.score);return 0;
}
4.1.5 fread 和 fwrite
這兩個函數只適用于文件輸入、輸入流(二進制文件)
fwrite
向流寫入數據塊
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
- 從 `ptr` 所指向的內存塊中,將包含 `count` 個元素(每個元素大小為 `size` 字節)的數組寫入流的當前位置。流的位置指示器會按寫入的總字節數向前移動。在函數內部,它將 `ptr` 所指向的塊視為一個由 `(size * count)` 個 `unsigned char` 類型元素組成的數組,并依次將這些元素寫入流,就好像對每個字節都調用了 `fputc` 函數一樣。?
- 函數將返回成功寫入的元素總數。如果該數值與 `count` 參數不同,則說明寫入錯誤導致函數未能完成操作。在這種情況下,將為流設置錯誤指示器(`ferror`)。如果 `size` 或 `count` 為零,函數將返回零,且錯誤指示器保持不變。`size_t` 是一種無符號整數類型。?
struct S
{char name[20];int age;float score;
};
int main()
{struct S s = {"張三",20,95.5};FILE* pf = fopen("test.txt","wb");if(pf == NULL) {perror("fopen");return 1;}//寫文件//測試二進制的寫函數fwrite(&s,sizeof(struct S),1,pf); //關閉文件 fclose(pf);pf = NULL;return 0;
}
fread
從流中讀取數據塊
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
- 從流中讀取一個包含 `count` 個元素的數組,每個元素大小為 `size` 字節,并將它們存儲到 `ptr` 指定的內存塊中。流的位置指示器會按讀取的總字節數向前移動。如果讀取成功,讀取的總字節數為 `(size * count)`。?
- 函數將返回成功讀取的元素總數。如果該數值與 `count` 參數不同,則說明在讀取時要么發生了讀取錯誤,要么已到達文件末尾。在這兩種情況下,都會設置相應的指示器,可分別使用 `ferror` 和 `feof` 進行檢查。如果 `size` 或 `count` 為零,函數將返回零,并且流的狀態和 `ptr` 所指向的內容都將保持不變。`size_t` 是一種無符號整數類型。?
struct S
{char name[20];int age;float score;
};
int main()
{struct S s = {0};FILE* pf = fopen("test.txt","rb");if(pf == NULL) {perror("fopen");return 1;}//讀文件//測試二進制的讀函數fread(&s,sizeof(struct S),1,pf);printf("%s %d %f\n",s.name,s.age,s.score); //關閉文件 fclose(pf);pf = NULL;return 0;
}
4.2 對比一組函數
scanf/fscanf/sscanf
printf/fprintf/sprintf
首先認識一下sscanf 和 sprintf
sprintf
將格式化數據寫入字符串
int sprintf ( char * str, const char * format, ... );
- 構建一個字符串,其內容與使用 `printf` 函數并傳入 `format` 參數時所打印的文本相同,但這些內容不會被打印出來,而是作為 C 字符串存儲在 `str` 所指向的緩沖區中。緩沖區的大小應足夠大,以容納整個生成的字符串;內容后面會自動追加一個終止空字符。在 `format` 參數之后,函數期望至少有 `format` 所需數量的額外參數。?
- 如果操作成功,將返回所寫入的字符總數。此計數不包括自動追加在字符串末尾的額外空字符。如果操作失敗,將返回一個負數。?
struct S
{char name[20];int age;float score;
};
int main()
{struct S s = {"zhangsan",20,95.5};char buf[100] = {0};sprintf(buf,"%s %d %f",s.name,s.age,s.score);printf("%s\n",buf);return 0;
}
sscanf
從字符串中讀取格式化數據
int sscanf ( const char * s, const char * format, ...);
- 從字符串 `s` 中讀取數據,并根據 `format` 參數將數據存儲到額外參數所指定的位置,就好像使用了 `scanf` 函數一樣,只不過這里是從字符串 `s` 讀取數據,而非從標準輸入(`stdin`)讀取。額外參數應指向已分配好的對象,這些對象的類型要與格式字符串中相應格式說明符所指定的類型一致。?
- 如果操作成功,函數將返回參數列表中成功填充的項數。此計數可能與預期的項數相符,也可能在匹配失敗的情況下更少(甚至為零)。如果在成功解析任何數據之前就出現輸入失敗的情況,則返回 `EOF`。?
struct S
{char name[20];int age;float score;
};
int main()
{struct S s = {"zhangsan",20,95.5};char buf[100] = {0};sprintf(buf,"%s %d %f",s.name,s.age,s.score);printf("%s\n",buf);//按照打印字符串struct S tmp = {0};sscanf(tmp,"%s %d %f",tmp.name,&(tmp.zge),&(tmp.score));printf("%s %d %f\n",tmp.name,tmp.age,tmp.score);//打印結構體數據return 0;
}
總結:
scanf - 從鍵盤上讀取格式化的數據 stdin
printf - 把數據寫到(輸出)屏幕上 stdout
fscanf - 針對所有輸入流的格式化的輸入函數:stdin、打開的文件
fprintf - 針對所有輸出流的格式化的輸出函數:stdout、打開的文件
sscanf - 從一個字符串中,還原出一個格式化的數據
sprintf - 把格式化的數據,存放在(轉換成)一個字符串中