零基礎學C語言——文件IO

這是一個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

上面列舉出來的函數是用來操作系統中的文件的,文件內容都是以字節流或者字符流的形式被處理的。

什麼叫做流?

我們對文件的操作就好比從一個水管接水。水管就是我們的文件,而水就是文件的內容。那麼接水有兩種方式:

  1. 要多少接多少
  2. 先按需求接水,但每次接的都先放在碗裡,攢到一定量再倒出去處理

此時,第二種就是我們所謂的流式處理。即按照要求將文件內容處理好後放入緩衝區,攢到一定量或者達到某種特殊條件時,才進行後續處理。

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環境高級編程》以及一些操作系統相關知識,奠定一些實用基礎。


喜歡的小夥伴可以關注碼哥,也可以給碼哥留言評論,如有建議或者意見也歡迎私信碼哥,我會第一時間回覆。


分享到:


相關文章: