【C語言】—— 文件操作(下)
- 前言:
- 五、文件的順序讀寫
- 5.1、 順序讀寫函數介紹
- 5.2、 f p u t c fputc fputc 函數
- 5.3、 f g e t c fgetc fgetc 函數
- 5.4、 f p u t s fputs fputs 函數
- 5.5、 f g e t s fgets fgets 函數
- 5.6、 f p r i n t f fprintf fprintf 函數
- 5.7、 f s c a n f fscanf fscanf 函數
- 5.8、 p r i n t f / f p r i n t f / s p r i n t f printf/fprintf/sprintf printf/fprintf/sprintf 函數對比
- 5.9、 s c a n f / f s c a n f / s s c a n f scanf/fscanf/sscanf scanf/fscanf/sscanf 函數對比
- 5.10、 f w r i t e fwrite fwrite 函數
- 5.11、 f r e a d fread fread 函數
- 六、文件的隨機讀寫
- 七、文件讀取結束的判定
- 7.1、 被錯誤使用的 f e o f feof feof
- 7.2、如何判斷文件讀取結束
- (1)文本文件判斷
- (2)二進制文件判斷
- 八、 文件緩沖區
??
前言:
??
??
??在 【C語言】—— 文件操作(上) 一文中,我們對文件有了一個簡單的了解,并學會了如何打開和關閉文件,下面就讓我們一起來學學如何對文件進行讀寫吧。
??
??
五、文件的順序讀寫
5.1、 順序讀寫函數介紹
函數名 | 功能 | 適用于 |
---|---|---|
fgetc | 字符輸入函數 | 所有輸入流 |
fputc | 字符輸出函數 | 所有輸出流 |
fgets | 文本行輸入函數 | 所有輸入流 |
fputs | 文本行輸出函數 | 所有輸出流 |
fscanf | 格式化輸入函數 | 所有輸入流 |
fprintf | 格式化輸出函數 | 所有輸出流 |
fread | 二進制輸入 | 文件 |
fwrite | 二進制輸出 | 文件 |
??注:上面說的適用于所有輸入流一般指適用于標準輸入流和其他輸入流(如文件輸入流);所有輸出流一般指適用于標準輸出流和其他輸出流(如文件輸出流)
??下面我們對上述函數一一進行介紹
??
5.2、 f p u t c fputc fputc 函數
- 函數功能:將一個字符寫入流中,這個流其實就是文件流
- 函數參數:
- i n t int int? c h a r a c t e r character character:
要寫入的字符
,字符的本質就是 A S C I I ASCII ASCII 碼值,因此這里參數類型為 i n t int int 沒有問題 - F I L E FILE FILE * s t r e a m stream stream :指向要寫入文件的
文件指針
- i n t int int? c h a r a c t e r character character:
- 返回類型:返回類型是
int
:當寫入成功,返回寫入的值
,當寫入失敗,返回EOF(-1)
??
函數使用:
#include<stdio.h>int main()
{FILE* pf = NULL;//打開文件pf = fopen("test.txt", "w");//文件操作if (NULL == pf){perror("fopen fail");return 1;}//寫文件fputc('a', pf);fputc('b', pf);fputc('c', pf);//寫入26個字母char ch = 0;for (ch = 'a'; ch <= 'z'; ch++){fputc(ch, pf);}//關閉文件fclose(pf);pf = NULL;
}
??這樣,字符就寫好了。
??
??當寫入字符時,還有一些細節需要注意:當一個文件打開時,最開始其實是有一個光標指向第一個位置
,每當用 f p u t c fputc fputc 函數寫入一個字符
,光標則后退一格
。光標是用來維護此時此刻我們這個文件寫到哪的,而且是按照一定的順序往后走的,因此叫做順序讀寫
??
5.3、 f g e t c fgetc fgetc 函數
- 函數功能:從流(文件)中獲取一個字符
- 返回值:返回類型為 i n t int int 。如果成功,就會將
讀到的字符返回
,如果讀取失敗或者遇到文件末尾返回EOF(-1)
- 為什么返回類型是
int
呢?正是因為它會返回兩種類型的值:字符的 A S C I I ASCII ASCII碼 值和 EOF;如果返回類型為char
,則EOF 無法返回
??
- 為什么返回類型是
函數使用:
#include<stdio.h>int main()
{FILE* pf = NULL;//打開文件pf = fopen("test.txt", "r");//文件操作if (NULL == pf){perror("fopen fail");return 1;}//讀文件int ch = 0;while ((ch = fgetc(pf)) != EOF){printf("%c ", ch);}printf("\n");//關閉文件fclose(pf);pf = NULL;
}
運行結果:
??而同樣,以只讀的方式打開文件,剛開始光標是在第一個位置,即指向 a a a,每讀一個字符,光標向后退一位
。
??
5.4、 f p u t s fputs fputs 函數
- 函數功能:
將 str 字符串寫入文件流中
,直至遇到 ‘\0’ 停止(‘\0’不會被寫入)
?? 注:多次調用該函數,并不會實現主動換行
,要想換行應主動輸入‘\n’
??
函數使用:
#include<stdio.h>int main()
{//打開文件FILE* pf = NULL;pf = fopen("test.txt", "w");if (NULL == pf){perror("fopen fail");return 1;}//寫文件fputs("hello", pf);fputs("world\n", pf);fputs("hello csdn\n", pf);//關閉文件fclose(pf);pf = NULL;return 0;
}
運行結果:
??我們可以看到,加了換行符后,文件的光標是直接落到下一行的。
??
5.5、 f g e t s fgets fgets 函數
- 函數功能:
從流中最多讀取 num 個字符,并放在 str 所指向的空間中
- 函數讀 n u m num num 個字符,但是最多只能讀取 n u m num num - 1個,因為最后一個位置函數會自己加上 ‘\0’
- 該函數不會換行讀取。當 n u m num num 大于字符數時,遇到換行符 ‘\n’,將 ‘\n’ 讀取后,不再往下讀取,自己加上 ‘\0’ 后停止。
- 當函數讀取成功,返回的是目標空間的地址;讀取失敗則返回空指針(NULL)
??
函數使用:
#include<stdio.h>int main()
{//打開文件FILE* pf = NULL;pf = fopen("test.txt", "r");if (NULL == pf){perror("fopen fail");return 1;}//讀文件char arr1[10] = "xxxxxxxxx";fgets(arr1, 8, pf);char arr2[10] = "xxxxxxxxx";fgets(arr2, 8, pf);//關閉文件fclose(pf);pf = NULL;return 0;
}
運行結果:
??
5.6、 f p r i n t f fprintf fprintf 函數
??
??該函數的功能是將數據以格式化的形式寫入流中(以文本的形式)
??
??其實, f p r i n t f fprintf fprintf 函數和 p r i n t f printf printf 函數是非常相像的,讓我們來對比一下
??
??
??他們的區別僅僅是第一個參數的有無
而已,其他都是一模一樣的,所以你會用 p r i n t f printf printf你就會用 f p r i n t f fprintf fprintf
??
??多的一個參數是什么呢?是文件流,你需要將數據輸出到的那個文件流
??
#include<stdio.h>struct S
{char name[20];int age;float score;
};
int main()
{struct S s = { "張三", 20, 75.5f };//打開文件FILE* pf = NULL;pf = fopen("test.txt", "w");if (NULL == pf){perror("fopen fail");return 1;}//寫文件fprintf(pf, "%s %d %f", s.name, s.age, s.score);//關閉文件fclose(pf);pf = NULL;return 0;
}
運行結果:
??
5.7、 f s c a n f fscanf fscanf 函數
??
??該函數的功能是從文件流中讀取格式化的數據。
??
??不難發現, f s a n f fsanf fsanf 與 s c a n f scanf scanf 函數很像,我們來對比一下
??
??
??同 f p r i n t f fprintf fprintf 一樣, f s c a n f fscanf fscanf 與 s c a n f scanf scanf 只是相差一個參數而已,你會用 s c a n f scanf scanf 自然也就會用 f s c a n f fscanf fscanf 函數,第一個參數即是你所要讀取的文件流。
??
#include<stdio.h>struct S
{char name[20];int age;float score;
};
int main()
{struct S s = { 0 };//打開文件FILE* pf = NULL;pf = fopen("test.txt", "r");if (NULL == pf){perror("fopen fail");return 1;}//讀文件fscanf(pf, "%s %d %f", s.name, &(s.age), &(s.score));//printf("%s %d %.2f\n", s.name, s.age, s.score);fprintf(stdout, "%s %d %.2f\n", s.name, s.age, s.score);//關閉文件fclose(pf);pf = NULL;return 0;
}
??
運行結果:
??注意看,上述代碼用了 f p r i n t f fprintf fprintf 來將數據打印在屏幕上
??還記得最開始的表格中, f p r i n t f fprintf fprintf 最后一列寫的是所有輸出流嗎?這所有輸出流就包括了文件流
和標準輸出流
,既然 f p r i n t f fprintf fprintf 可以輸出到文件中,那么自然也就可以輸出到屏幕中,完成 p r i n t f printf printf 一樣的功能。
??而同理,前面講的 f p u t c fputc fputc、 f g e t s fgets fgets、 f s c a n f fscanf fscanf 等函數也可以從標準輸入(輸出)流中獲取(輸出)數據。
??
5.8、 p r i n t f / f p r i n t f / s p r i n t f printf/fprintf/sprintf printf/fprintf/sprintf 函數對比
??
通過我們前面的學習,我們已經知道了 p r i n t f printf printf 和 f p r i n t f fprintf fprintf 函數的作用:
- p r i n t f printf printf:把數據以格式化的形式打印在標準輸出流上
- f p r i n t f fprintf fprintf : 把數據以格式化的形式打印在 指定的輸出流 上
那么 s p r i n t f sprintf sprintf 函數又是作什么的呢?我們一起來看看
??
??該函數的作用是:將數據以格式化的形式寫到字符串上。其實就是把格式化的數據轉換成字符串
??
#include<stdio.h>struct S
{char name[20];int age;float score;
};
int main()
{struct S s = { "張三", 20, 75.5f };char buf[50] = { 0 };sprintf(buf, "%s %d %f", s.name, s.age, s.score);printf("%s\n", buf);return 0;
}
運行結果:
??該代碼完全是以 %s 的形式打印的,說明數據已經完全轉換成字符串
了。
??
5.9、 s c a n f / f s c a n f / s s c a n f scanf/fscanf/sscanf scanf/fscanf/sscanf 函數對比
??同樣,通過我們前面的學習,我們已經知道了 s c a n f scanf scanf 和 f s c a n f fscanf fscanf 函數的作用:
- s c a n f scanf scanf:從 標準輸入流 中讀取格式化的數據
- f s c a n f fscanf fscanf:從 指定輸入流 中讀取格式化的數據
??那 s s c a n f sscanf sscanf 的功能又是什么呢?學習了 s p r i n t f sprintf sprintf ,我們猜測,其應該是從字符串中讀取格式化數據
,是不是呢?我們一起來看看
??
??
函數功能:從字符串中讀取格式化數據
??
#include<stdio.h>struct S
{char name[20];int age;float score;
};
int main()
{struct S s = { "張三", 20, 75.5f };char buf[50] = { 0 };sprintf(buf, "%s %d %f", s.name, s.age, s.score);struct S a = { 0 };sscanf(buf, "%s %d %f", s.name, &(s.age), &(s.score));printf("%s %d %f\n", s.name, s.age, s.score);return 0;
}
運行結果:
??
??
5.10、 f w r i t e fwrite fwrite 函數
- 函數功能:以二進制的形式將內存塊中的數據寫入文件中
- 參數介紹:
- c o n s t const const? v o i d void void * p t r ptr ptr: p t r ptr ptr 是指向要寫入數據的數組的
指針
- s i z e size size_ t t t? s i z e size size:表示要寫入的每個元素的
大小
- s i z e size size_ t t t? c o u n t count count:表示要寫入元素的
個數
- c o n s t const const? v o i d void void * p t r ptr ptr: p t r ptr ptr 是指向要寫入數據的數組的
??
下面我們直接上代碼:
#include<stdio.h>int main()
{int arr[] = { 1,2,3,4,5 };int sz = sizeof(arr) / sizeof(arr[0]);FILE* pf = NULL;pf = fopen("test.txt", "wb");if (NULL == pf){perror("fopen fail");return 1;}fwrite(arr, sizeof(arr[0]), sz, pf);fclose(pf);pf = NULL;return 0;
}
??
我們以二進制的方式打開:
??
5.11、 f r e a d fread fread 函數
??該函數的作用是:以二進制的形式讀取數據到內存中
??我們可以看到,這函數的參數與 f w r i t e fwrite fwrite 是大同小異的,這里就不一一介紹了,我們直接上代碼
#include<stdio.H>int main()
{int arr[5] = { 0 };FILE* pf = NULL;pf = fopen("test.txt", "rb");if (NULL == pf){perror("fopen fail");return 1;}fread(arr, sizeof(arr[0]), 5, pf);for (int i = 0; i < 5; i++){printf("%d ", arr[i]);}printf("\n");fclose(pf);pf = NULL;return 0;
}
運行結果:
??但是,上面代碼是我提前知道了總共的數據個數,當我不知道數據具體個數是又該怎么辦呢?
??這里,我們需要知道 f r e a d fread fread 函數的返回值
,該函數的返回值是讀取到的數據的個數。這時,當我要求讀 7 個數據,而返回值是 5 時,說明數據讀完了。
??上面的代碼我們可以做如下修改:
#include<stdio.h>int main()
{int arr[5] = { 0 };FILE* pf = NULL;pf = fopen("test.txt", "rb");if (NULL == pf){perror("fopen fail");return 1;}int i = 0;while (fread(arr + i, sizeof(arr[0]), 1, pf)){printf("%d ", arr[i]);i++;}printf("\n");fclose(pf);pf = NULL;return 0;
}
??
??
六、文件的隨機讀寫
??前面我們所學習到的函數都是順序讀寫,光標是依次往后移動。那能不能做到隨機讀寫呢,即我想在哪里讀寫就在哪讀寫,指那打那。
??當然是可以的,下面讓我們一起來學習。
??
6.1、 f s e e k fseek fseek 函數
- 功能:根據文件指針的位置和偏移量來定位文件指針(光標)。
- 參數介紹:
- l o n g long long? i n t int int? o f f s e t offset offset:相對于起始位置的
偏移量
,可正可負 - i n t int int? o r i g i n origin origin:
起始位置
- l o n g long long? i n t int int? o f f s e t offset offset:相對于起始位置的
起始位置選擇:
常量 | 所指位置 |
---|---|
SEEK_SET | 文件的起始位置 |
SEEK_CUR | 當前光標位置 |
SEEK_END | 文件結尾 |
??
??這個函數有什么用呢?比如文件中有 a b c d e f g abcdefg abcdefg 的數據,當前光標指向 a a a,而我想直接讀 e e e,這時就可以用該函數移動光標啦。
??
例子:
#inclu<stdio.h>int main()
{FILE* pf = NULL;pf = fopen("test.txt", "r");if (NULL == pf){perror("fopen fail");return 1;}char ch = 0;ch = fgetc(pf);printf("%c\n", ch);fseek(pf, 3, SEEK_CUR);ch = fgetc(pf);printf("%c\n", ch);fclose(pf);pf = NULL;return 0;
}
運行結果:
??
??
6.2、 f t e l l ftell ftell 函數
f t e l l ftell ftell 函數會返回文件指針(光標) 相對于文件起始位置的 偏移量
??這里,我們想:如果我們讓光標讀到文件末尾
,在返回偏移量
,是不是就能知道文件的長度呢?答案是肯定
的。
??
例子:
#includ<stdio.h>int main()
{FILE* pf;long size;pf = fopen("test.txt", "rb");if (NULL == pf)perror("Error opening file");fseek(pf, 0, SEEK_END); // non-portablesize = ftell(pf);fclose(pf);printf("Size of test.txt: %ld bytes.\n", size);return 0;
}
??
??
6.3、 r e w i n d rewind rewind 函數
?? r e w i n d rewind rewind 函數可以讓文件指針回到起始位置
??走的太遠,別忘了回頭路
例子:
#include<stdio.h>int main()
{int n;FILE* pf;char buffer[27];pf = fopen("test.txt", "w+");for (n = 'A'; n <= 'Z'; n++){fputc(n, pf);}rewind(pf);fread(buffer, 1, 26, pf);fclose(pf);buffer[26] = '\0';printf(buffer);return 0;
}
??
??
七、文件讀取結束的判定
7.1、 被錯誤使用的 f e o f feof feof
??
??很多人都以為 f e o f feof feof函數是用來直接判斷文件讀取是否結束。其實這是大錯特錯的
?? f e o f feof feof 的作用是:當文件讀取結束時,判斷讀取結束的原因是否是因為:遇到文件末尾結束。
??現在假設文件讀取結束了,但是是什么原因讀取結束的呢?
- 有可能遇到文件末尾
- 讀取的時候發生了錯誤
?? f e o f feof feof 函數是判斷是否是因為遇到文件末尾而結束的。
??而還有個函數叫 f e r r o r ferror ferror 是用來判斷是否是因為遇到錯誤而讀取結束的
??
??其實在我們打開一個流時,會有兩個標記值
- 是否遇到文件末尾
- 是否發生錯誤
????當讀文件的過程中確實是遇到文件末尾了,就會將第一個值
標記
;遇到錯誤就會將第二個值標記
。
?? f e o f feof feof是用來檢測第一個標記
的; f e r r o r ferror ferror是用來檢測第二個標記
的
f e o f feof feof 函數:當文件確實是因為讀取到文件末尾而結束時,返回一個非零值,反之返回 0
??
??
7.2、如何判斷文件讀取結束
??那么如何來判斷文件是否讀取結束呢?其實在前面結束各個函數時已經順便介紹了:通過函數的返回值進行判斷!
??
(1)文本文件判斷
函數名 | 正常讀取返回值 | 讀取結束或遇到錯誤的返回值 |
---|---|---|
fgetc | 返回讀取到的字符的ASCII碼值 | EOF |
fgets | 返回目標空間的地址 | NULL |
??
(2)二進制文件判斷
??二進制文件用 f r e a d fread fread 進行讀取, f r e a d fread fread 返回值是其讀取到的個數。當其返回值小于
實際要讀取的個數時,表示文件讀取結束
??
八、 文件緩沖區
??我們想一個問題:當我們想往文件中存 26 個字母,這 26 個字母是直接從程序(內存)中存到文件(硬盤)中的嗎?
??其實不是的。
??ANSI?C 標準采用 “緩沖文件系統” 處理的數據文件的,所謂緩沖文件系統指的是系統自動在內存中為程序中為每一個正在使用的文件開辟一塊“文件緩沖區”
。
??從內存向磁盤輸出數據會先送達
內存中的緩沖區
,裝滿緩沖區
或主動刷新緩沖區
才將數據送到磁盤上。
??如果從磁盤向計算機讀入數據,則從磁盤文件中讀取數據輸入到內存緩沖區(充滿緩沖區或刷新緩沖區),然后在從緩沖區逐個地將數據送到程序數據區(程序變量等)。
??緩沖區的大小根據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語言在操作文件的時候,需要做刷新緩沖區
或者在文件操作結束的時候關閉文件
。
??如果不做,可能導致讀寫文件的問題
??
??
??
??
??好啦,本期關于文件操作的知識就介紹到這里啦,希望本期博客能對你有所幫助。同時,如果有錯誤的地方請多多指正,讓我們在C語言的學習路上一起進步!