這是一個C語言系列文章,如果是初學者的話,建議先行閱讀之前的文章。筆者也會按照章節順序發佈。
在編程的世界中,我們主要打交道的對象是內存,但是很多實用的軟件卻也很難離開對文件的操作。例如,nginx這樣著名的開源web server,它會讀取文件中的配置。
因此,學習編程語言語法後,緊跟的也必然是文件IO——即文件輸入(Input)輸出(Output)。
在類UNIX操作系統環境下,通常都是把終端(初學者暫時理解為命令行界面吧)當作文件(/dev/tty...)來看待,因此標準庫封裝了同一套函數來處理終端的IO與文件的IO。
本文所討論的函數,其聲明都在stdio.h中,因此需要使用 指令include將其引入。
我們所要討論的函數有:
- fopen
- fclose
- fputc
- fputs
- fgetc
- fgets
- fread
- fwrite
- fseek
- feof
- fprintf
- fscanf
- printf
- scanf
流式IO
上面列舉出來的函數是用來操作系統中的文件的,文件內容都是以字節流或者字符流的形式被處理的。
什麼叫做流?
我們對文件的操作就好比從一個水管接水。水管就是我們的文件,而水就是文件的內容。那麼接水有兩種方式:
- 要多少接多少
- 先按需求接水,但每次接的都先放在碗裡,攢到一定量再倒出去處理
此時,第二種就是我們所謂的流式處理。即按照要求將文件內容處理好後放入緩衝區,攢到一定量或者達到某種特殊條件時,才進行後續處理。
fopen
既然我們將文件比作水管,那麼要讀取文件內容(水)必然需要先打開水管。
fopen就是用來打開文件的。我們看下函數原型:
<code>FILE *fopen(const char * path, const char * mode);/<code>
path——文件的路徑及文件名
mode——打開方式,打開方式有很多種組合,我們僅介紹如下幾種:
- r——字符只讀模式,打開文件只可讀不可寫,且讀取的內容將作為字符類型(char)
- w——字符只寫模式,打開文件只可寫不可讀,且寫入的內容為字符類型(char),如果文件不存在則會新建一個文件,如果文件已存在則會清空文件(也稱截斷)
- a——字符寫追加模式,打開文件只可寫且寫入位置是從當前文件內容的末尾處開始,寫入的內容為字符類型(char),如果文件不存在則新建文件
- rb——二進制只讀模式,打開文件只可讀不可寫,且讀取的內容將作為無符號字符類型(unsigned char)
- wb——二進制只寫模式,打開文件只可寫不可讀,且寫入的內容為無符號字符類型(unsigned char),如果文件不存在則會新建一個文件,如果文件已存在則會清空文件
- ab——二進制寫追加模式,打開文件只可寫且寫入位置是從當前文件內容的末尾處開始,寫入的內容為無符號字符類型(unsigned char),如果文件不存在則新建文件
如果在上述打開方式後再加個+,例如:r+、a+、rb+,那麼文件則變為即可讀寫模式,其餘特性依賴於+前的前綴。
函數的返回值為FILE指針類型,讀者不必糾結這個類型的具體定義,每個標準庫的實現也有可能不同,因此不必記下。有些文章會管這個返回值叫做文件句柄,也可以簡稱其為fp(file pointer)。
我們看個例子:
<code>#include <stdio.h>
int main(void)
{
FILE *fp = fopen("a.txt", "r");
if (fp == NULL) //為NULL時,表明打開失敗,一般失敗原因有:文件不存在,文件及其路徑上的目錄的權限不正確
return -1;
return 0;
}/<stdio.h>/<code>
除卻利用fopen打開的句柄,標準庫都會預定義三個句柄:stdin, stdout, stderr,分別代表終端的:標準輸入、標準輸出、標準出錯。我們常用的printf其實就是對stdout的輸出操作。
fclose
既然水管可以打開,自然也需要關閉。
函數原型如下:
<code>int fclose(FILE *stream);/<code>
參數為fopen打開的文件句柄或者stdin、stdout、stderr。
返回值:如果成功關閉返回0,否則為EOF(是個宏,一般值為-1)。一般工程中,如果沒有特殊需求,通常會忽略返回值。
fputc
這個函數是針對字符流句柄的處理。
函數原型:
<code>int fputc(int c, FILE *stream);/<code>
函數功能:將字符c,注意是字符(char)不是整數(雖然是整型)
,寫入stream句柄指代的文件的當前位置。返回值:成功返回0,否則返回EOF。
這裡有個當前位置的概念,即文件的讀取和寫入都是依賴於一個位置指示器,這個指示器指示的就是當前操作到的位置。例如,fputc前,這個位置為0,表示文件起始位置。當fputc一個字符後,當前位置就變為了1。
示例:
<code>#include <stdio.h>
int main(void)
{
FILE *fp = fopen("a.txt", "w");
if (fp == NULL)
return -1;
int rc = fputc('c', fp);
if (rc == EOF)
return -1;
fclose(fp);
return 0;
}/<stdio.h>/<code>
打開a.txt可以看到剛剛寫入的c。
fputs
fputc是用來寫入單個字符的,fputs是用來寫入字符數組的。
函數原型:
<code>int fputs(const char *s, FILE *stream);/<code>
返回值:成功返回非負值,否則返回EOF。
fgetc
有了寫入字符就有讀取字符操作。
函數原型:
<code>int fgetc(FILE *stream);/<code>
功能:從頭stream指代的文件的當前位置讀取一個字符。
返回值:如果成功,則返回字符,否則返回EOF。
示例:
<code>#include <stdio.h>
int main(void)
{
FILE *fp = fopen("a.txt", "r");
if (fp == NULL)
return -1;
int rc = fgetc(fp);
if (rc == EOF)
return -1;
printf("%c\\n", (char)rc);
fclose(fp);
return 0;
}/<stdio.h>/<code>
輸出結果為:c
fgets
同理,可以讀取一個字符,也可以讀取一段字符數組。
函數原型:
<code>char *fgets(char *str, int size, FILE *stream);/<code>
功能:從stream指代的文件的當前位置處讀取最多為size-1個字符到str中,如果讀取時遇到了換行符,則讀取的內容只截止到換行符及其以前。
返回值:成功,則返回字符數組首地址,否則返回NULL。
示例:
加入a.txt中的內容為:
<code>hello
world/<code>
那麼執行如下代碼:
<code>#include <stdio.h>
int main(void)
{
char s[64] = {0}, *ret;
FILE *fp = fopen("a.txt", "r");
if (fp == NULL)
return -1;
ret = fgets(s, sizeof(s), fp);
if (ret == NULL)
return -1;
printf("%s\\n", ret);
fclose(fp);
return 0;
}/<stdio.h>/<code>
執行結果為:
<code>hello
/<code>
你沒看錯,hello自帶了一個\\n,printf中還有個\\n,因此會是如此輸出結果。
fseek
前面提到過當前位置這個概念,那這個位置可否修改呢?當然可以,正是利用fseek進行修改的。
函數原型:
<code>int fseek(FILE *stream, long offset, int whence);/<code>
功能:將stream指代的文件中的當前位置相對於whence指定的位置移動offset字節。offset可以為負數,即向前移動。
其中,whence的值有:
- SEEK_SET——文件開始處
- SEEK_CUR——當前位置處
- SEEK_END——文件末尾
示例:
<code>#include <stdio.h>
int main(void)
{
char s[64] = {0}, *ret;
FILE *fp = fopen("a.txt", "r+");
if (fp == NULL)
return -1;
fseek(fp, 5, SEEK_CUR);
fputc(' ', fp);
fclose(fp);
return 0;
}/<stdio.h>/<code>
利用fseek定位到hello和world之間的換行符,然後用寫入空格來覆蓋換行符,此時a.txt的內容變為:
<code>hello world/<code>
feof
當讀取文件內容時,雖然讀取函數會返回EOF來表示讀取結束或者讀取出錯,但有時我們不希望改變當前位置,同時獲知當前位置是否達到文件末尾。這時就要使用feof函數。
函數原型:
<code>int feof(FILE *stream);/<code>
返回值:如果未到結尾,則返回0,否則返回非0值。
fprintf
接下來的這個是一種格式輸入,即輸入的字符數組中存在一種約定好的格式符,不同的格式符對應不同的數據類型,函數會利用後續參數的值來替換字符數組中對應的格式符,然後將替換好的內容寫入到文件中。
函數原型:
<code>int fprintf(FILE *stream, const char *format, ...);/<code>
最後的...不是省略的意思,而是一種特殊的參數,叫做
可變參數,即後面的參數個數不確定,類型不確定。關於可變參的內容,本系列不打算講解。功能:將format字符數組中特殊的格式符利用後續參數替換後,寫入stream指代的文件的當前位置。
返回值:如果出錯,則返回負值,否則返回輸出到文件中的字符數。
其中,format支持的常用格式符有:
- %d——對應int型數值
- %u——對應unsigned int型數值
- %ld——long型數值
- %lu——unsigned long型數值
- %lld——long long型數值
- %llu——unsigned long long數值
- %f——float和double數值
- %lf——long double數值
- %c——char型字符
- %s——char型字符數組(必須以\\0結尾)
示例:
<code>#include <stdio.h>
int main(void)
{
char s[64] = {0};
FILE *fp = fopen("a.txt", "w");
if (fp == NULL)
return -1;
fprintf(fp, "Hello %s", "World");
fclose(fp);
return 0;
}/<stdio.h>/<code>
a.txt的內容變為:
<code>Hello World/<code>
fscanf
與fprintf相反,fscanf用於格式輸出,即給定一個格式字符數組(字符串),其格式剛好匹配文件內容的格式,那麼將字符數組中的特殊格式符處的值,寫入到其後相應的參數變量中。
函數原型:
<code>int fscanf(FILE *stream, const char *format, ...);/<code>
返回值:成功則返回成功匹配格式符並賦值的個數,失敗則返回EOF。
format支持的格式符與fprintf的一致。
這裡要注意的是,format後的參數,都是指針,因為fscanf在其內部要對變量賦值,而函數參數是自動變量,在函數生命週期結束後就會銷燬回收,因此修改的內容無法傳遞給調用方,所以需要傳遞指針。
示例:
<code>#include <stdio.h>
int main(void)
{
char s[64] = {0};
FILE *fp = fopen("a.txt", "r");
if (fp == NULL)
return -1;
int n = fscanf(fp, "Hello %s", s);
fclose(fp);
printf("n:%d s:%s\\n", n, s);
return 0;
}/<stdio.h>/<code>
輸出的結果為:
<code>n:1 s:World/<code>
注意:本例中,World的長度遠小於數組s的長度,因此如此使用沒有問題。但是如果s只有2字節長度,那麼調用fscanf就會導致緩衝區溢出。緩衝區溢出是一種bug,輕則程序崩潰,重則會被黑客利用奪取本機遠程操作權限。對於任何一個C開發人員來說,都應慎重對待此類bug。
printf
printf相當於fprintf(stdout, format, ...);
其函數原型為:
<code>int printf(const char *format, ...);/<code>
scanf
scanf相當於fscanf(stdin, format, ...);
其函數原型為:
<code>int scanf(const char *restrict format, ...);/<code>
fwrite
下面要介紹的這兩個函數都是針對二進制流的操作。
二進制流(字節流)與字符流一樣,都需要有輸入操作,即向文件中寫入數據。
函數原型:
<code>size_t fread(void *ptr, size_t size, size_t nitems, FILE *stream);/<code>
功能:將ptr所指向的數組(nitems個size字節數據)寫入到stream指代的文件的當前位置中。
返回值:成功,返回寫入的size大小的數據個數(理論上應該等於nitems),否則返回0或者一個小於nitems的數。
示例:
<code>#include <stdio.h>
int main(void)
{
int i = 65536;
FILE *fp = fopen("a.dat", "wb");
if (fp == NULL)
return -1;
size_t n = fwrite(&i, sizeof(i), 1, fp);
fclose(fp);
return 0;
}/<stdio.h>/<code>
此時,a.dat的內容我們用xxd看下文件中的十六進制:
<code>$ xxd a.dat
00000000: 0000 0100 /<code>
可以看到,文件中的十六進制值為0000 0100剛好是十進制的65536。
fread
二進制流(字節流)輸出操作。
函數原型:
<code>size_t fread(void *ptr, size_t size, size_t nitems, FILE *stream);/<code>
功能:從stream指向的文件的當前位置中,讀取nitems個size字節大小(一共nitems*size字節)的數據到ptr指向的數組中。
返回值:成功,返回讀取的size大小的數據個數(理論上應該等於nitems),否則返回0或者一個小於nitems的數。
示例:
<code>#include <stdio.h>
int main(void)
{
int i = 0;
FILE *fp = fopen("a.dat", "rb");
if (fp == NULL)
return -1;
size_t n = fread(&i, sizeof(i), 1, fp);
fclose(fp);
printf("%d\\n", i);
return 0;
}/<stdio.h>/<code>
輸出結果為:65536
到此,零基礎學C語言系列文章完結。讀者如果通讀過這個系列的文章,並且學會其中的各個知識點,那麼恭喜你,你已經站在了C語言大門內了,雖然依舊是貼著門站。
筆者推薦下一步學習《UNIX環境高級編程》以及一些操作系統相關知識,奠定一些實用基礎。
喜歡的小夥伴可以關注碼哥,也可以給碼哥留言評論,如有建議或者意見也歡迎私信碼哥,我會第一時間回覆。
閱讀更多 碼哥比特 的文章