【C語言】程序員自我修養之文件操作
🔥個人主頁:大白的編程日記
🔥專欄:C語言學習之路
文章目錄
- 【C語言】程序員自我修養之文件操作
- 前言
- 一.文件介紹
- 1.1為什么使用文件
- 1.2文件分類
- 1.3二進制文件和文本文件
- 二.文件的打開和關閉
- 2.1流和標準流
- 2.2文件指針
- 2.3文件的打開和關閉
- 三.文件的順序讀寫
- 3.1fgetc和fputc
- 3.2fgets和fputs
- 3.3fprintf和fscanf
- 3.4對比函數
- 3.5fwrite和fread
- 四.文件的隨機讀寫
- 五. 文件讀取結束的判定
- 六.文件緩沖區
- 后言
前言
哈嘍,各位小伙伴大家好!今天給大家分享的是,作為程序員的自我修養的文件操作。話不多說,咱們直接進入正題!向大廠沖鋒!
一.文件介紹
1.1為什么使用文件
以這段代碼為例:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{int n = 0;scanf("%d", &n);printf("%d", n);return 0;
}
當我們輸入10時,發現n這時確實時10。
但當我們退出程序,重新運行時n的值已經不見了。
可以發現我們剛才再程序輸入的10,在內存中并沒有持久化的保存。
但是我們硬盤中的文件的數據就可以永久的保存起來。
- 文件
如果沒有文件,我們寫的程序的數據是存儲在電腦的內存中,如果程序退出,內存回收,數據就丟失了,等再次運行程序,是看不到上次程序的數據的,如果要將數據進行持久化的保存,我們可以使用文件。
1.2文件分類
磁盤(硬盤)上的文件是文件。
但是在程序設計中,我們?般談的文件有兩種:程序文件、數據文件(從文件功能的角度來分類的)。
- 程序文件
程序文件包括源程序文件(后綴為.c),目標文件(windows環境后綴為.obj),可執行程序windows環境后綴為.exe)。 - 數據文件
文件的內容不?定是程序,而是程序運行時讀寫的數據,比如程序運行需要從中讀取數據的文件,或者輸出內容的文件。
今天主要我們討論的是數據文件。
在以前各章所處理數據的輸入輸出都是以終端為對象的,即從終端的鍵盤輸入數據,運行結果顯示到顯示器上。
其實有時候我們會把信息輸出到磁盤上,當需要的時候再從磁盤上把數據讀取到內存中使用,這里處理的就是磁盤上文件。
- 文件名
?個文件要有一個唯一的文件標識,以便用戶識別和引用。
文件名包含3部分:文件路徑+文件名主干+文件后綴
例如: c:\code\test.txt
為了方便起見,文件標識常被稱為文件名。
1.3二進制文件和文本文件
根據數據的組織形式,數據?件被稱為文本文件或者二進制文件。
- 二進制文件
數據在內存中以?進制的形式存儲,如果不加轉換的輸出到外存的文件中,就是?進制文件。例如obj文件:因為是二進制的文件,我們用文本編輯器看到的就是亂碼。
- 文本文件
如果要求在外存上以ASCII碼的形式存儲,則需要在存儲前轉換。以ASCII字符的形式存儲的文件就是文本文件。
txt后綴的就是文本文件
?個數據在文件中是怎么存儲的呢?
- 存儲方式
字符一律以ASCII形式存儲,數值型數據既可以用ASCII形式存儲,也可以使用二進制形式存儲。
如有整數10000,如果以ASCII碼的形式輸出到磁盤,則磁盤中占用5個字節(每個字符?個字節),而?進制形式輸出,則在磁盤上只占4個字節
#include <stdio.h>
int main()
{int a = 10000;FILE* pf = fopen("test.txt", "wb");fwrite(&a, 4, 1, pf);//?進制的形式寫到?件中fclose(pf);pf = NULL;return 0;
}
- 我們以文本編輯器打開是看不看不懂的。
- 所以我們以二進制編輯器打開
二.文件的打開和關閉
2.1流和標準流
- 流
我們程序的數據需要輸出到各種外部設備,也需要從外部設備獲取數據,不同的外部設備的輸入輸出操作各不相同,為了方便程序員對各種設備進行方便的操作,我們抽象出了流的概念,我們可以把流想象成流淌著字符的河。
C程序針對文件、畫面、鍵盤等的數據輸入輸出操作都是通過流操作的。
一般情況下,我們要想向流里寫數據,或者從流中讀取數據,都是要打開流,然后操作。
- 標準流
那為什么我們從鍵盤輸入數據,向屏幕上輸出數據,并沒有打開流呢?
int main()
{int a = 0;scanf("%d", &a);printf("%d", a);return 0;
}
那是因為C語言程序在啟動的時候,默認打開了3個流:
-
stdin
標準輸入流,在?多數的環境中從鍵盤輸入,scanf函數就是從標準輸入流中讀取數據 -
stdout
標準輸出流,大多數的環境中輸出至顯示器界面,printf函數就是將信息輸出到標準輸出流中。 -
stderr
標準錯誤流,大多數環境中輸出到顯示器界面。
這是默認打開了這三個流,我們使用scanf、printf等函數就可以直接進行輸入輸出操作的。
stdin、stdout、stderr 三個流的類型是: FILE* ,通常稱為文件指針。
C語言中,就是通過 FILE* 的文件指針來維護流的各種操作的。
2.2文件指針
緩沖文件系統中,關鍵的概念是“文件類型指針”,簡稱“文件指針”。
每個被使?的?件都在內存中開辟了?個相應的文件信息區,?來存放文件的相關信息(如文件的名字,文件狀態及文件當前的位置等)。這些信息是保存在?個結構體變量中的。該結構體類型是由系統聲明的,取名 FILE.
例如,VS2013 編譯環境提供的 stdio.h 頭文件中有以下的文件類型申明:
struct _iobuf {char *_ptr;int _cnt;char *_base;int _flag;int _file;int _charbuf;int _bufsiz;char *_tmpfname;};
typedef struct _iobuf FILE;
不同的C編譯器的FILE類型包含的內容不完全相同,但是大同小異。
每當打開一個文件的時候,系統會根據文件的情況自動創建?個FILE結構的變量,并填充其中的信息,使用者不必關心細節。
一般都是通過?個FILE的指針來維護這個FILE結構的變量,從而維護整個文件流的讀寫操作,這樣使用起來更加方便。
下?我們可以創建?個FILE*的指針變量:
FILE* pf;//?件指針變量
定義pf是?個指向FILE類型數據的指針變量。可以使pf指向某個文件的文件信息區(是?個結構體變量)。通過該文件信息區中的信息就能夠訪問該文件。也就是說,通過文件指針變量能夠間接找到與它關聯的文件。
2.3文件的打開和關閉
文件操作就像喝水一樣:
-
喝水
打開瓶蓋——喝水——關閉瓶蓋 -
文件操作
打開文件——讀寫文件——關閉文件
那文件是如何打開關閉的呢?
在編寫程序的時候,在打開文件的同時,都會返回?個FILE*的指針變量指向該文件,也相當于建立了指針和文件的關系。
ANSIC 規定使用 fopen 函數來打開文件, fclose 來關閉文件。
//打開?件
FILE * fopen ( const char * filename, const char * mode );
//關閉?件
int fclose ( FILE * stream );
- 參數
filename是要操作的文件名,mode表示文件的打開模式,
stream是要關閉文件的的文件指針
mode表示文件的打開模式,下面都是文件的打開模式:
- "w"和"a"的區別
”w“會清空文件中原有的數據再寫。"a"則再原來的數據后追加數據
注意是雙引號不是單引號!因為char*指針指向的是字符串首元素的地址,所以要用雙引號表示字符串
實例代碼:
#include <stdio.h>
int main ()
{FILE * pFile;//打開?件pFile = fopen ("myfile.txt","w");//?件操作if (pFile!=NULL)//檢查指針返回值{fputs ("fopen example",pFile);//關閉?件fclose (pFile);pFile=NULL;//置空return 0;
}
注意fopen 如果打開成功返回文件指針,否則返回空指針,所以我們需要對fopen的返回值做判斷。
fclose關閉文件后不會把文件指針置為空,所以我們需要手動置空
三.文件的順序讀寫
順序讀寫函數介紹
3.1fgetc和fputc
fputc是字符輸入函數,可以把一個字符寫進文件。
-
參數
character是要寫進去的字符,ASCLL碼值表示,所以用int類型。
stream是指向文件的文件指針 -
光標
文件中有光標進行讀寫操作的維護,寫數據后光標就會向后移動。 -
返回值
如果寫操作成功的話,就返回寫的字符。失敗就返回EOF,就是-1。同時會把錯誤標記起來。
int main()
{FILE* pf = fopen("test.txt", "w");if (pf == NULL){perror("fopen");return 1;}for (int i = 'a'; i < 'z'; i++){fputc(i, pf);}fclose(pf);pf = NULL;return 0;
}
那如何讀取文件的數據呢?那就要使用fgetc
-
參數
stream是要讀取文件的文件指針 -
返回值
如果讀取成功返回值讀取成功的字符,ascll碼值表示是int
如果讀取失敗或遇到文件末尾返回EOF就是-1,所以用int返回,兼容兩種返回值的類型 -
光標
文件中有光標進行讀寫操作的維護,讀數據后光標就會向后移動。
int main()
{FILE* pf = fopen("test.txt", "r");if (pf == NULL){perror("fopen");return 1;}int ch=0;while ((ch=fgetc(pf))!= EOF){printf("%c", ch);}fclose(pf);pf = NULL;return 0;
}
這些函數都是一個一個字符讀寫的,如果我們要讀寫字符串呢?
3.2fgets和fputs
fputs是把字符串寫到文件里的函數。
-
參數
str指向要寫入字符串的指針,
stream是要寫入文件的文件指針 -
換行
如果想換行寫入字符串,需要再字符串末尾添加\0
那如何都出來呢?這就得使用fgets。
- 參數
str是讀取內存存放的地址
num是讀取的長度,但實際他只會讀取num-1個字符,最后一個字符會補上\0。如果文件中的字符串長度不夠,就會把字符串讀完(包括\n),然后補上\0
stream是讀取文件的文件指針 - 返回值
成功讀取就返回str的地址
讀取失敗就返回空指針。
所以多次讀取時就可以這樣寫
int main()
{FILE* pf = fopen("test.txt", "r");if (pf == NULL){perror("fopen");return 1;}char s[20] = "xxxxxxxxxxxxxxxxxx";while (fgets(s, 10, pf) != NULL){printf("%s", s);}fclose(pf);pf = NULL;return 0;
}
3.3fprintf和fscanf
當我們要以指定格式寫數據到文件時,我們就可以用fprintf函數
我們可以發現printf和fprintf函數參數非常相似,fprintf多了一個文件指針。
其實fprintf使用跟printf基本一樣,只是多了一個文件指針而已
int main()
{FILE* pf = fopen("test.txt", "w");if (pf == NULL){perror("fopen");return 1;}struct S s = { "張三", 18, 80.5f};fprintf(pf, "%s %d %f", s.name, s.age, s.score);fclose(pf);return 0;
}
那我們讀取就用fscanf讀取
我們發現scanf和fscanf也非常相似,就差了一個文件指針的參數
所以fscanf的使用也只需多加一個讀取文件的文件指針即可。
int main()
{FILE* pf = fopen("test.txt", "w");if (pf == NULL){perror("fopen");return 1;}struct S s = { "張三", 18, 80.5f};fscanf(pf, "%s %d %f", s.name, &s.age, &s.score);printf("%s %d %f", s.name, s.age, s.score);fclose(pf);return 0;
}
我們發現這些函數都適用于所有流
所有流分為文件流和標準流
那是不是我們用標準流也可以呢?
這里我們來驗證一下:
- 驗證
大家發現我們把文件流替換成標準輸出流結果也是一樣的。
3.4對比函數
sprintf是把格式化的數據轉化為字符串的函數
sprintf多了一個str參數,其實就是格式化數據轉化字符串后存放的地址
int main()
{FILE* pf = fopen("test.txt", "w");if (pf == NULL){perror("fopen");return 1;}char a[30];struct S s = { "張三", 18, 80.5f};sprintf(a, "%s %d %f", s.name, &s.age, &s.score);printf("%s", a);fclose(pf);return 0;
}
sscanf就是在字符串中讀取格式化數據的函數
int main()
{FILE* pf = fopen("test.txt", "w");if (pf == NULL){perror("fopen");return 1;}char a[30];struct S s = { "張三", 18, 80.5f};printf("字符串打印:");sprintf(a, "%s %d %f\n", s.name, s.age, s.score);printf("%s", a);struct S t = {0};sscanf(a, "%s%d%f", t.name, &t.age, &t.score);printf("格式打印: %s %d %f", t.name, t.age, t.score);fclose(pf);return 0;
}
現在我們來對比一下這些相似的函數
3.5fwrite和fread
前面我們用的函數寫進去都能看懂,這是因為我們是以文本或字符的形式寫進去的。 那以二進制寫進去又是怎樣的呢?
fwrite把數據以二進制寫入文件
-
參數
ptr指向一個數組,數組存放要寫入的數據
size是每個數據的大小
count數據的個數
stream寫入文件的文件指針 -
打開方式
"wb"打開
int main()
{FILE* pf = fopen("test.txt", "wb");if (pf == NULL){perror("fopen");return 1;}int arr[] = { 1,2,3,4 };fwrite(arr, 4, 4, pf);fclose(pf);return 0;
}
當然以二進制形式寫進去我們看不懂,我們再以二進制形式都出來就好了
fread就是把數據以二進制形式讀出來。
-
參數
ptr指向數組,讀取后的數據存放到數據中
size讀取數據的大小
count讀取數據的個數
strenm讀取文件的文件指針 -
打開方式
"rb"打開 -
返回值
size_t返回值,表示成功讀取的個數。如果讀取小于count參數說明是最后一次讀取
當我們不知道文件中的數據個數是就可以這樣寫
int main()
{FILE* pf = fopen("test.txt", "rb");if (pf == NULL){perror("fopen");return 1;}int arr[20] = { };int i = 0;while (fread(arr+i, 4, 1, pf)){printf("%d ", arr[i]);i++;}fclose(pf);return 0;
}
四.文件的隨機讀寫
大家發現我們講的這些函數都是一個一個讀寫,所以叫順序讀寫
那我們可不可以想從任意位置開始讀寫呢?這就是我們接下來要講的隨機讀寫
fseek函數
- 參數
stream:操作文件的文件指針
offset 想讓光標偏移的偏移量
origin 偏移的起始位置 - 起始位置
起始位置有三種
int main()
{FILE* pf = fopen("test.txt", "r");if (pf == NULL){perror("fopen");return 1;}fseek(pf, 2, SEEK_SET);printf("%c", fgetc(pf));fclose(pf);return 0;
}
- ftell
返回文件指針相對于起始位置的偏移量
int main ()
{FILE * pFile;long size;pFile = fopen ("myfile.txt","rb");if (pFile==NULL) perror ("Error opening file");else{fseek (pFile, 0, SEEK_END); // non-portablesize=ftell (pFile);fclose (pFile);printf ("Size of myfile.txt: %ld bytes.\n",size);}return 0;
}
- rewind
讓文件指針的位置回到文件的起始位置
int main ()
{int n;FILE * pFile;char buffer [27];pFile = fopen ("myfile.txt","w+");for ( n='A' ; n<='Z' ; n++)fputc ( n, pFile);rewind (pFile);fread (buffer,1,26,pFile);fclose (pFile);buffer[26]='\0';printf(buffer);return 0;
}
五. 文件讀取結束的判定
牢記:在文件讀取過程中,不能?feof函數的返回值直接來判斷文件的是否結束。
feof 的作用是:當文件讀取結束的時候,判斷是讀取結束的原因是否是:遇到文件末尾結束。
- 文本文件
判斷返回值是否為 EOF ( fgetc結束),或者 NULL ( fgets結束 )
int main(void)
{int c; // 注意:int,?char,要求處理EOFFILE* fp = fopen("test.txt", "r");if(!fp) {perror("File opening failed");return EXIT_FAILURE;}//fgetc 當讀取失敗的時候或者遇到?件結束的時候,都會返回EOFwhile ((c = fgetc(fp)) != EOF) // 標準C I/O讀取?件循環{ putchar(c);}//判斷是什么原因結束的if (ferror(fp))puts("I/O error when reading");else if (feof(fp))puts("End of file reached successfully");fclose(fp);
}
- 二進制文件
?進制文件的讀取結束判斷,判斷返回值是否小于實際要讀的個數。
#include <stdio.h>
enum { SIZE = 5 };
int main(void)
{double a[SIZE] = {1.,2.,3.,4.,5.};FILE *fp = fopen("test.bin", "wb"); // 必須??進制模式fwrite(a, sizeof *a, SIZE, fp); // 寫 double 的數組fclose(fp);double b[SIZE];fp = fopen("test.bin","rb");size_t ret_code = fread(b, sizeof *b, SIZE, fp); // 讀 double 的數組if(ret_code == SIZE) {puts("Array read successfully, contents: ");for(int n = 0; n < SIZE; ++n) printf("%f ", b[n]);putchar('\n');} else { // error handlingif (feof(fp))printf("Error reading test.bin: unexpected end of file\n");else if (ferror(fp)) {perror("Error reading test.bin");}}fclose(fp);
}
六.文件緩沖區
ANSIC 標準采?“緩沖文件系統” 處理的數據?件的,所謂緩沖文件系統是指系統自動地在內存中為 程序中每?個正在使用的?件開辟?塊“?件緩沖區”。從內存向磁盤輸出數據會先送到內存中的緩沖區,裝滿緩沖區后才?起送到磁盤上。如果從磁盤向計算機讀?數據,則從磁盤?件中讀取數據輸?到內存緩沖區(充滿緩沖區),然后再從緩沖區逐個地將數據送到程序數據區(程序變量等)。緩 沖區的大小根據C編譯系統決定的。
#include <stdio.h>
#include <windows.h>
//VS2019 WIN11環境測試
int main()
{FILE*pf = fopen("test.txt", "w");fputs("abcdef", pf);//先將代碼放在輸出緩沖區printf("睡眠10秒-已經寫數據了,打開test.txt?件,發現?件沒有內容\n");Sleep(10000);printf("刷新緩沖區\n");fflush(pf);//刷新緩沖區時,才將輸出緩沖區的數據寫到?件(磁盤)//注:fflush 在?版本的VS上不能使?了printf("再睡眠10秒-此時,再次打開test.txt?件,?件有內容了\n");Sleep(10000);fclose(pf);//注:fclose在關閉?件的時候,也會刷新緩沖區pf = NULL;return 0;
}
這?可以得出?個結論:
因為有緩沖區的存在,C語?在操作文件的時候,需要做刷新緩沖區或者在文件操作結束的時候關閉文件。
如果不做,可能導致讀寫文件的問題。
后言
這就是文件操作的全部內容,這些內容可能沒那么重要,但是作為一個程序員,這些就像內功一樣,是一個程序員的自我修養。所以我們還是要多加了解學習。好啦,今天就分享到這里!咱們下期見!拜拜~