C語言中的文件操作詳解

1.為什麼使用文件

在學習結構體時,寫瞭一個簡易的通訊錄的程序,當程序運行起來的時候,可以在通訊錄中增加和刪除數據,此時數據是存放在內存當中的,當程序退出的時候,通訊錄中的數據自然就不存在瞭,等下次通訊錄運行的時候,數據又得重新錄入瞭,這樣的通訊錄使用起來會有點難受。

所以應該通訊錄應該要能夠把數據給記錄下來,隻有選擇刪除的時候,數據才不復存在。而這就涉及到瞭數據持久化的問題,一般數據持久化的方法有,把數據存放在磁盤文件、存放到數據庫等方式。

使用文件可以將數據直接存放到電腦的硬盤上,做到瞭數據的持久化。

2.什麼是文件

磁盤上的文件是文件。

但是在程序設計中,文件可以分為兩種:程序文件和數據文件(從文件功能的角度來分類)

2.1程序文件

包括源程序文件(後綴為.c),目標文件(windows環境後綴為.obj),可執行程序(windows環境後綴為.exe)

平時用來寫C語言代碼的那個文件就是源程序文件

可執行程序就是代碼運行起來後彈出的那個黑框框

目標文件就是可執行程序在形成過程中生成的文件

2.2數據文件

文件的內容不一定是程序,而是程序運行起來時讀寫的數據,比如程序運行需要從中讀取數據的文件,或者輸出內容的文件。

以下的內容基本都是圍繞這個數據文件來展開的,而在之前所處理數據的輸入以及輸出都是以終端(終端就是輸入輸出設備)為對象的,即從終端的鍵盤上輸入數據,運行結果輸出(顯示)到顯示器上。

有時候會把信息輸出到磁盤上面,當需要的時候再從磁盤上讀取數據到內存中使用,這裡處理的就是磁盤上文件。

2.3文件名

一個文件要有唯一的文件標識,以便用戶識別和引用。

為瞭方便起見,文件標識通常被稱為文件名。

文件名包含三個部分:文件路徑+文件名主幹+文件後綴

例如:C:\code\test.txt

3.文件的打開和關閉

3.1文件指針

緩沖系統中,關鍵的概念是“文件類型指針”,簡稱“文件指針”

每個被使用的文件都在內存中開辟瞭一個文件信息區,用來存放文件的相關信息(如文件的名字,文件的狀態以及文件的位置等)。這些信息是保存在一個結構體變量中的,而這個結構體類型是由系統來聲明的,取名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指向某個文件的文件信息區(一個結構體變量),通過該文件信息區中的信息就能夠訪問該文件,也就是說,通過文件指針變量能夠找到與他關聯的文件。

如圖:

每個文件在打開的時候都會在內存中開辟一個文件信息區,這個文件信息區就是FILE結構體類型的變量,而此時文件指針就會指向這個變量。

3.2文件的打開和關閉

文件在讀寫之前應該先打開文件,在使用結束之後應該關閉文件。

在編寫程序的時候,在打開文件的同時,都會放回一個FILE*的指針變量指向該文件,也相當於建立瞭指針和文件的關系。

ANSI C規定使用fopen函數來打開文件,使用fclose函數來關閉文件。

打開文件:

FILE* fopen(const char* filename, const char* mode);

關閉文件:

int fclose (FILE* stream); 

用隻寫的方式打開文件

代碼如下:

#include <stdio.h>
int main()
{
	//打開文件
	FILE* pf = fopen("test.txt", "w");
	if (pf == NULL)//遇到錯誤時函數會返回NULL指針
	{
		perror("fopen");
		return 1;
	}
	//文件操作
	// 
	//關閉文件
	fclose(pf);
	pf = NULL;//把文件指針置空,防止野指針的問題
	return 0;
}

在相應的目錄下創建瞭一個test.txt文件,如果文件不存在,則進行創建,如果文件存在,則對文件的內容進行銷毀

用隻讀的方式打開文件

代碼如下:

int main()
{
	//打開文件
	FILE* pf = fopen("test.txt", "r");
	//文件操作
	// 
	//關閉文件
	fclose(pf);
	pf = NULL;//把文件指針置空,防止野指針的問題
	return 0;
}

相應的目錄下必須要存在這個test.txt文件,否則編譯器會報錯。

註意:在相應的目錄下查看文件時要在查看中打開文件擴展名

以上fopen函數中的文件名參數寫的都是相對路徑,如果要寫絕對路徑的話要從對應的根目錄開始寫。

4.文件的順序讀寫

先來認識一些輸入和輸出函數:

這裡的輸出流和輸出流是一個抽象的概念,比如說一些外部設備如鍵盤、顯示器、U盤等等,這些設備的輸入和輸出方式都是不同的,這個時候C語言中的庫將這些輸入和輸出的方式都封裝成一個流,隻需要知道這個流就能完成一個輸入和輸出,而不用去學習硬件上的其他知識。

再打開編譯器的時候,會默認打開三個流,分別是標準輸入流、標準輸出流和標準錯誤流

  • 標準輸入流:stdin,指的是鍵盤
  • 標準輸出流:stdout,指的是屏幕
  • 標準錯誤流:stderr,也是屏幕

接下來具體講講輸入和輸出函數

字符輸出函數:

int  fputc(int c, FILE* stream);

fputc會返回被輸出字符的ASCII碼值,遇到錯誤時返回EOF。 

#include <stdio.h>
int main()
{
	//打開文件
	FILE* pf = fopen("test.txt", "w");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//寫文件
	char ch = 0;
	for (ch = 'a'; ch <= 'z'; ch++)
	{
		fputc(ch, pf);
	}
	//關閉文件
	fclose(pf);
	pf = NULL;
	return 0;
}

將字符a到z寫到文件當中,也可以理解為將數據輸出到文件裡面,因為適用於所有輸出流,所以這裡的參數可以用文件流,文件流也是文件指針。

此時可以查看test.txt裡面的數據:

字符輸入函數:

int fgetc(FILE* stream);

fgetc會返回讀到的字符的ASCII碼值,當讀到文件末尾或者遇到錯誤時會返回EOF。

int main()
{
	//打開文件
	FILE* pf = fopen("test.txt", "r");
	if (pf == EOF)
	{
		perror("fopen");
		return 1;
	}
	//讀文件 - 輸入操作
	int ch = 0;
	while ((ch = fgetc(pf)) != EOF)
	{
		printf("%c ", ch);
	}
	//關閉文件
	fclose(pf);
	pf = NULL;
	return 0;
}

從剛剛寫的test.txt中讀取信息,也就是從test.txt上的數據輸入到瞭ch中,並在屏幕上輸出。

可以將鍵盤和顯示器作為輸入和輸出的參數:

int main()
{
	int ch = fgetc(stdin);
	fputc(ch, stdout);
	return 0;
}

文本行輸出函數:

int fputs(const char* string, FILE* stream);

成功則返回非負指,遇到錯誤則返回EOF

int main()
{
	//打開文件
	FILE* pf = fopen("test1.txt", "w");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//寫文件
	fputs("abcdefg\n", pf);
	fputs("hijklmn\n", pf);
	//關閉文件
	fclose(pf);
	pf = NULL;
	return 0;
}

將字符串輸出到文件中

 此時打開test1.txt查看數據:

如果輸出流寫stdout,可以輸出到屏幕上來

文本行輸入函數:

 char* fgets(char* string, int n, FILE* stream);

將輸入的數據存儲到string,會最多讀取n-1個字符,最後1個字符為'\0',返回值為讀取到的字符串,遇到錯誤時或EOF時返回NULL指針。

int main()
{
	//打開文件
	FILE* pf = fopen("test1.txt", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//寫文件
	char arr[256] = { 0 };
	while (fgets(arr, 256, pf) != NULL)
	{
		fputs(arr, stdout);
	}
	//關閉文件
	fclose(pf);
	pf = NULL;
	return 0;
}

將剛才test1.txt裡面的內容作為輸入的數據,然後在屏幕上將數據輸出。

格式化輸出函數:

int fprintf( FILE *stream, const char *format [, argument ]…); 

和printf很像,隻是printf的默認輸出流是標準輸出流,而這裡的參數stream適用於所有的輸出流

struct S
{
	char name[20];
	int age;
	double score;
};
 
int  main()
{
	struct S s = { "zhangsan", 20, 75.5 };
	//打開文件
	FILE* pf = fopen("test2.txt", "w");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//寫文件
	fprintf(pf, "%s %d %f", s.name, s.age, s.score);
	//關閉文件
	fclose(pf);
	pf = NULL;
	return 0;
}

此時已經把數據寫入到瞭test2.txt裡面,打開文件來查看一下:

 同樣的,stream位置的參數可以是標準輸出流,這樣數據就會輸出到屏幕上面來瞭

struct S
{
	char name[20];
	int age;
	double score;
};
 
int  main()
{
	struct S s = { "zhangsan", 20, 75.5 };
	fprintf(stdout, "%s %d %f", s.name, s.age, s.score);
	return 0;
}

格式化輸入函數:

int fscanf( FILE *stream, const char *format [, argument ]… ); 

fscanf和scanf和很像,隻是scanf默認stream位置的參數是標準輸入流,也就是鍵盤。

int  main()
{
	struct S s = {0};
	//打開文件
	FILE* pf = fopen("test2.txt", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//讀文件
	fscanf(pf, "%s %d% lf", s.name, &(s.age), &(s.score));
	fprintf(stdout, "%s %d %lf", s.name, s.age, s.score);
	//關閉文件
	fclose(pf);
	pf = NULL;
	return 0;
}

 將數據從文件上讀取,然後在屏幕上輸出。

 再來認識兩個與printf和scanf很像的函數:

sprintf:將格式化的數據轉換為字符串

int sprintf( char *buffer, const char *format [, argument] … );

 sscanf:將字符串轉換為格式化的數據

int sscanf( const char *buffer, const char *format [, argument ] … );

sprintf的使用:

struct S
{
	char name[20];
	int age;
	double score;
};
 
int main()
{
	char buf[256] = { 0 };
	struct S s = { "wangwu", 30, 85.5 };
	sprintf(buf, "%s %d %lf", s.name, s.age, s.score);
	printf("%s\n", buf);
	return 0;
}

將結構體的數據轉換為字符串存入buf中,然後將buf在屏幕上輸出出來。 

 sscanf的使用:

struct S
{
	char name[20];
	int age;
	double score;
};
 
int main()
{
	char buf[256] = { 0 };
	struct S s = { "wangwu", 30, 85.5 };
	sprintf(buf, "%s %d %lf", s.name, s.age, s.score);
	//將字符串轉化為格式化的數據
	struct S tmp = { 0 };
	sscanf(buf, "%s %d %lf", tmp.name, &(tmp.age), &(tmp.score));
	printf("%s %d %lf", tmp.name, tmp.age, tmp.score);
	return 0;
}

也可以認為是從字符串中提取數據存入到結構體tmp中來。

這裡列在一起方便對比一下:

 二進制輸出函數:

size_t fwrite( const void *buffer, size_t size, size_t count, FILE *stream );

  • buffer:指向被讀取的數據的指針
  • size:被讀取的項目的字節大小
  • count:被讀取的項目數的最大值
  • stream:文件流

fwrite的使用:

struct S
{
	char name[20];
	int age;
	double score;
};
 
int main()
{
	struct S s = { "lisi", 15, 95.5 };
	//打開文件
	FILE* pf = fopen("test3.txt", "wb");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//寫文件
	fwrite(&s, sizeof(struct S), 1, pf);
	//關閉文件
	fclose(pf);
	pf = NULL;
	return 0;
}

 因為是以二進制的方式寫的,所以文件中存放的數據是這樣子的:

二進制輸入函數:

size_t fread( void *buffer, size_t size, size_t count, FILE *stream ); 

跟fwrite相反,將文件中的數據作為輸入數據存儲到buffer指向的空間中。

fread的使用:

struct S
{
	char name[20];
	int age;
	double score;
};
 
int main()
{
	struct S s = { 0 };
	//打開文件
	FILE* pf = fopen("test3.txt", "rb");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//讀文件
	fread(&s, sizeof(struct S), 1, pf);
	printf("%s %d %lf\n", s.name, s.age, s.score);
	//關閉文件
	fclose(pf);
	pf = NULL;
	return 0;
}

5.文件的隨機讀寫

5.1fseek

根據文件指針的位置和偏移量來定位文件指針

int fseek( FILE *stream, long offset, int origin );

  • stream:文件流
  • offset:相對於此時文件指針的位置的偏移量,單位是字節
  • origin:文件指針此時的位置

origin可以分為3個值:

SEEK_CUR – 文件指針當前的位置

SEEK_SET – 文件開始的位置

SEEK_END – 文件末尾的位置

先在對應的目錄下創建一個test.txt文件,往裡面寫入a到f的字符

#include <stdio.h>
int main()
{
	FILE* pf = fopen("test.txt", "w");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//把a到f的字符寫入文件
	char ch = 0;
	for (ch = 'a'; ch <= 'f'; ch++)
	{
		fputc(ch, pf);
	}
	fclose(pf);
	pf = NULL;
	return 0;
}

fseek的使用:

int main()
{
	FILE* pf = fopen("test.txt", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//讓文件指針從此時的位置向後移動5個字節
	fseek(pf, 5, SEEK_CUR);
	int ch = fgetc(pf);
	printf("%c\n", ch);
	fclose(pf);
	pf = NULL;
	return 0;
}

找到f的位置並將其讀取,輸出在屏幕上,結果是f

offset輸入負值也可以讓指針往前走

int main()
{
	FILE* pf = fopen("test.txt", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//讓文件指針從文件尾的位置向前移動3個字節
	fseek(pf, -3, SEEK_END);
	int ch = fgetc(pf);
	printf("%c\n", ch);
	fclose(pf);
	pf = NULL;
	return 0;
}

最終屏幕上輸出的結果是d

註意:文件末尾是f之後的那個位置

5.2ftell

放回文件指針相對於起始位置的偏移量

long int ftell ( FILE * stream );

ftell的使用: 

int main()
{
	FILE* pf = fopen("test.txt", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	fseek(pf, 0, SEEK_END);
	long size  = ftell(pf);
	printf("%ld\n", size);
	fclose(pf);
	pf = NULL;
	return 0;
}

對於test.txt這個文件,文件指針末尾的位置相對於起始位置是6.

使用比較簡單,瞭解一下即可

5.3rewind

讓文件指針回到文件的起始位置

void rewind( FILE *stream ); 

rewind的使用:

int main()
{
	FILE* pf = fopen("test.txt", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	fseek(pf, 0, SEEK_END);
	//從文件尾回到文件起始位置
	rewind(pf);
	long size  = ftell(pf);
	printf("%ld\n", size);
	fclose(pf);
	pf = NULL;
	return 0;
}

文件指針先是走到文件末尾,最後回到起始位置,所以最終屏幕輸出結果是0

6.文本文件和二進制文件

 根據數據的儲存形式,數據文件被稱為文本文件和二進制文件。

數據在內存中以二進制的形式存儲,如果不加轉換的輸出到外存,就是二進制文件。外存可以理解為硬盤。

如果要求在外存上以ASCII碼的形式存儲,則需要在存儲前轉換。以ASCII字符的形式存儲的文件就是文本文件。前面fputc就是以ASCII字符的形式將數據存儲在文件中。

下面來看一個數據在內存中是怎麼存儲的

字符一律以ASCII形式進行存儲,數值型數據即可以用ASCII形式存儲,也可以使用二進制形式來存儲。

假設有一個整數為10000,如果以ASCII碼的形式輸出到磁盤,則在磁盤中占用5個字節(每個字符占用一個字節),而如果以二進制的形式輸出到磁盤,則在磁盤上占用4個字節。

直接在記事本中輸入10000,就是以ASCII碼的形式進行存儲。

可以將這個文件在編譯器中打開

然後選擇二進制的打開方式:

然後可以看到顯示的結果是將二進制的形式轉換為十六進制的形式

剛好對應著10000的ASCII碼的存儲形式

接下來看看以二進制的形式存儲:

int main()
{
	FILE* pf = fopen("test.txt", "w");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	int a = 10000;
	fwrite(&a, sizeof(int), 1, pf);//二進制的形式寫
	fclose(pf);
	pf = NULL;
	return 0;
}

再以二進制的形式打開test.txt

 由於該編譯器是小端字節序存儲,所以可以看到16進制顯示的數字是反過來的。

7.文件讀取結束的判定

7.1被錯誤使用的feof

文件在讀取的過程中,不能用feof函數的返回值來判斷文件讀取是否結束,而是應當用於在文件讀取結束的時候,判斷是因為讀取失敗造成的文件讀取結束,還是因為遇到文件尾造成的文件讀取結束。

文件的讀取結束判斷:

文本文件的讀取結束判斷:

fgetc函數判斷返回值是否為EOF

fgets函數判斷返回值是否為NULL

二進制文件的讀取結束判斷:

fread判斷返回值(實際讀到的個數)是否小於預計要讀的個數

feof和ferror函數

feof

int feof( FILE *stream ); 

 文件是由於讀取到文件尾結束時返回一個非0值,反之返回0

ferror

int ferror( FILE *stream );

文件是由於讀取錯誤而讀取結束時返回一個非0值,反之返回0

文本文件的讀取結束判斷代碼:

int main()
{
	int c; // 註意:int,非char,要求處理EOF
	FILE* fp = fopen("test.txt", "r");
	if (fp == NULL)
	{
		perror("fopen");
		return 1;
	}
	//fgetc 當讀取失敗的時候或者遇到文件結束的時候,都會返回EOF
	while ((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);
	fp = NULL;
	return 0;
}

二進制文件的讀取結束判斷代碼:

enum
{ 
	SIZE = 5 
};
 
int main(void)
{
	double a[SIZE] = { 1.,2.,3.,4.,5. };
	FILE* fp = fopen("test.txt", "wb");
	if (fp == NULL)
	{
		perror("fopen");
		return 1;
	}
	fwrite(a, sizeof * a, SIZE, fp); // 寫 double 的數組
	fclose(fp);
	fp = NULL;
 
	double b[SIZE];
	fp = fopen("test.txt", "rb");
	size_t ret_code = fread(b, sizeof * b, SIZE + 1, fp); // 讀 double 的數組
	if (ret_code == SIZE) 
	{
		puts("Array read successfully, contents: ");
		for (int n = 0; n < SIZE; ++n) 
		printf("%f ", b[n]);
		putchar('\n');
	}
	if (feof(fp))//判斷是否因為遇到文件尾造成的文件讀取結束
		printf("Error reading test.txt: unexpected end of file\n");
	else if (ferror(fp)) //判斷是否因為讀取失敗造成的文件讀取結束
	{
		perror("Error reading test.txt");
	}
	fclose(fp);
	fp = NULL;
}

8.文件緩沖區

ANSIC標準采用“緩沖文件系統”處理的數據文件的,所謂緩沖文件系統是指系統自動地在內存中為程序中每一個正在使用的文件開辟一塊“文件緩沖區”。從內存向磁盤輸出數據會先送到內存中的緩沖區,裝滿緩沖區後才一起送到磁盤上。如果從磁盤向計算機讀入數據,則從磁盤文件中讀取數據輸入到內存緩沖區(充滿緩沖區),然後再從緩沖區逐個地將數據送到程序數據區(程序變量等)。緩沖區的大小根據C編譯系統決定的。

圖解:

int main()
{
	FILE* pf = fopen("test.txt", "w");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	fputs("abcdef", pf);//先將代碼放在輸出緩沖區
	printf("睡眠10秒-已經寫數據瞭,打開test.txt文件,發現文件沒有內容\n");
	Sleep(10000);
	printf("刷新緩沖區\n");
	fflush(pf);//刷新緩沖區時,才將輸出緩沖區的數據寫到文件(磁盤)
	printf("再睡眠10秒-此時,再次打開test.txt文件,文件有內容瞭\n");
	Sleep(10000);//此時睡眠10秒,是為瞭說明數據是由於fflush的刷新才輸出到文件中的
	fclose(pf);
	//註:fclose在關閉文件的時候,也會刷新緩沖區
	pf = NULL;
	return 0;
}

fflush函數可以讓數據不充滿緩沖區時就直接輸入或者輸出

結論

因為有緩沖區的存在,C語言在操作文件的時候,需要做刷新緩沖區或者在文件操作結束的時候關閉文件,如果不做,可能會導致讀寫文件時數據不完全讀寫的問題。

到此這篇關於C語言中的文件操作詳解的文章就介紹到這瞭,更多相關C語言文件操作內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: