C语言文件操作(超详细万字长文)

目录

一、为什么使用文件

二、什么是文件

2.1 程序文件

2.2 数据文件

2.3 文件名

三、文件的打开和关闭

3.1 文件指针

 3.2 文件的打开与关闭

3.2.1 fopen函数

3.2.2 fclose函数

四、文件的顺序读写

1 fputc函数

2 fgetc函数

3 fputs函数

4 fgets函数

5 fprintf函数

6 fscanf函数

7 fwrite函数

8 fread函数

五、文件的随机读写

1 fseek函数

2 ftell函数

3 rewind函数

六、文本文件和二进制文件

 七、文件读取结束的判定

1 文本文件例子

2 二进制文件例子


一、为什么使用文件

我们前面学习结构体时,写了通讯录的程序,当通讯录运行起来的时候,可以给通讯录中增加、删除数据,此时数据是存放在内存中,当程序退出的时候,通讯录中的数据自然就不存在了,等下次运行通讯录程序的时候,数据又得重新录入,如果使用这样的通讯录就很难受。我们在想既然是通讯录就应该把信息记录下来,只有我们自己选择删除数据的时候,数据才不复存在。这就涉及到了数据持久化的问题,我们一般数据持久化的方法有,把数据存放在磁盘文件、存放到数据库等方式。使用文件我们可以将数据直接存放在电脑的硬盘上,做到了数据的持久化。

二、什么是文件

磁盘上的文件是文件。但是在程序设计中,我们一般谈的文件有两种:程序文件(程序文件可以操作数据文件)、数据文件(从文件功能的角度来分类的)。

2.1 程序文件

包括源程序文件(后缀为 .c , 目标文件( windows 环境后缀为 .obj , 可执行程序( windows 环境
后缀为 .exe )。

2.2 数据文件

文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。
本章讨论的是数据文件。在以前各章所处理数据的输入输出都是以终端为对象的,即从终端的键盘输入数据,运行结果显示到显示器上。键盘是标准输入流(stdin),屏幕是标准输出流(stdout)。
其实有时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理的就是磁盘上文件。

2.3 文件名

一个文件要有一个唯一的文件标识,以便用户识别和引用。 文件名包含3 部分:文件路径 + 文件名主干 + 文件后缀
例如: c:\code\test.txt
为了方便起见,文件标识常被称为 文件名

三、文件的打开和关闭

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 指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够找到与它关联 的文件
下图是三个文件的文件信息区:
C语言文件操作(超详细万字长文)_第1张图片

 3.2 文件的打开与关闭

3.2.1 fopen函数

FILE* fopen(const char* filename, const char* mode);
函数说明:文件在读写之前应该先 打开文件 ,在使用结束之后应该 关闭文件。 在编写程序的时候,在打开文件的同时,都会返回一个FILE* 的指针变量指向该文件,也相当于建立了指针和文件的关系。
函数输入:输入文件的相对或者绝对路径;
函数输出:FILE*类型指针;如果打开失败会返回空指针NULL,所以一定要判断返回值是不是空指针再使用;
常见文件打开方式
文件使用方式 作用 如果指针不存在
“ r ”  (只读) 为了输入数据,打开一个已存在的文本文件 报错
“ w ” (只写) 为了输入数据,打开一个文本文件 建立一个新文件
“ a ”    (追加) 向文本文件末尾添加数据

建立一个新文件

“ rb” (只读) 为了输入数据,打开一个二进制文件 报错
“ wb”  (只写) 为了输出数据,打开一个二进制文件 建立一个新文件
“ab”  (追加) 向二进制文件末尾添加数据 报错
“r+”  (读写) 为了读和写,打开一个文本文件 报错
“w+” (读写) 为了读和写,新建一个文本文件 建立一个新文件
”a+” (读写) 在文本文件末尾读和写 建立一个新文件
”rb+” (读写) 为读和写,打开一个二进制文件 报错
“wb+” (读写) 为读和写,新建一个二进制文件 建立一个新文件
”ab+” (读写) 在二进制文件末尾读和写 建立一个新文件

3.2.2 fclose函数

int fclose(FILE* stream);

函数说明:关闭与stream关联的文件并解除其关联;

函数输入:要关闭文件的FILE*类型指针;

函数输出:如果流成功关闭,则返回0。失败时,返回EOF。

四、文件的顺序读写

1 fputc函数

int fputc ( int character, FILE * stream );

函数说明:将字符写入stream并使位置指示器前进。

函数输入:要写入的字符(会被转化为int类型)。
函数输出:指向输出流的FILE结构体的指针。

代码1演示:

# include 
# include 
# include 
int main()
{
	//每次打开文件时系统会自动在文件信息区创建一个FILE结构体
	//用来保存文件的相关信息
	FILE* fp;
	//fopen返回值是FILE*
	fp = fopen("test.txt","w");
	//判断返回值是否为空指针
	if(fp == NULL)
	{
		printf("%s\n",strerror(errno));
	}
	else
	{
		//输入三个字符
		fputc('j',fp);
		fputc('y',fp);
		fputc('y',fp);
	}
	//关闭文件
	fclose(fp);
	//把FILE*类型指针赋值为NULL
	fp = NULL;
	return 0;
}

代码1结果演示:
 

2 fgetc函数

int fgetc ( FILE * stream );

函数说明:返回指定流(stream)的内部文件位置指示符当前指向的字符。然后,内部文件位置指示器前进到下一个字符。

函数输入:所有流;比如(键盘(标准输入流stdin),FILE*);

函数输出:读取成功后,将返回读取的字符(提升为int型)。读取失败或者读取结束返回EOF;

代码2演示:

# include 
# include 
# include 

int main ()
{
  int i;
  FILE * pf;
  int c;
  pf=fopen ("test.txt","r");
  if (pf==NULL)
  {
	printf("%s\n",strerror(errno));
  }
  else
  {
	  for(i=0;i<3;i++)
	  {
		//获取文件字符,并将文件位置指示器前进一位
		c = fgetc(pf);
		printf("%c",c);
	  };
	  //换行
	  printf("\n");
	  //关闭文件
	  fclose (pf);
	  //指针赋值为空指针
	  pf = NULL;
  }
}

代码2结果演示:

jyy
请按任意键继续. . .

3 fputs函数

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

函数说明:将str指向的字符串写入stream。函数开始从指定的地址(str)复制,直到到达终止的空字符(‘\0’)。此终止的空字符不会复制到stream中。fputs与puts的不同之处不仅在于可以指定目标流,而且fputs不会写入额外的字符,而puts会自动在末尾添加换行符

函数输入:str:要输入的字符串;stream:要写入的文件对应的FILE*指针;

函数输出:成功时,返回非负值。出现错误时,函数返回EOF。

代码3演示:

# include 
# include 
# include 
int main()
{
	//每次打开文件时系统会自动在文件信息区创建一个FILE结构体
	//用来保存文件的相关信息
	FILE* fp;
	//fopen返回值是FILE*
	fp = fopen("test.txt","w");
	//判断返回值是否为空指针
	if(fp == NULL)
	{
		printf("%s\n",strerror(errno));
	}
	else
	{
		//输入一个字符串
		fputs("jyy&whq",fp);
	}
	//关闭文件
	fclose(fp);
	//把FILE*类型指针赋值为NULL
	fp = NULL;
	return 0;
}

代码3结果演示:

4 fgets函数

char * fgets ( char * str, int num, FILE * stream );

函数说明:从stream中读取字符并将其作为字符串存储到str中,直到读取(num-1)个字符或到达换行符或文件结尾,以先发生的为准。

函数输入:str:输出字符串指针;拷贝到str字符个数(包括‘\0’);stream:要读取文件的指针;

函数输出:读取成功返回str,读取失败或者读取结束返回空指针;

代码4演示:

# include 
# include 
# include 
int main()
{
	char str[10];
	FILE* fp;
	//fopen返回值是FILE*
	fp = fopen("test.txt","r");
	//判断返回值是否为空指针
	if(fp == NULL)
	{
		printf("%s\n",strerror(errno));
	}
	else
	{
		//输出字符串
		fgets(str, 7, fp);
		//输出jyy&wh表示最后一位是'\0'
		printf("%s\n",str);
	}
	//关闭文件
	fclose(fp);
	//把FILE*类型指针赋值为NULL
	fp = NULL;
	return 0;
}

代码4结果演示:

jyy&wh
请按任意键继续. . .

5 fprintf函数

int fprintf ( FILE * stream, const char * format, ... );

函数说明:将按格式输入的字符串写入stream。如果format包含格式说明符(以%开头的子序列),则格式化格式后面的其他参数并将其插入到结果字符串中,替换它们各自的说明符。

函数输入:stream:要输入的文件指针(流);format:要输入的格式;

函数输出:如果写入成功返回字符个数;如果失败返回负数;

代码5演示:

# include 
# include 
# include 

typedef struct S 
{
	char name[10];
	int age;
};
int main()
{
	//初始化结构体
	struct S s = {"jyy",23};
	char n;
	FILE* fp;
	//fopen返回值是FILE*
	fp = fopen("test.txt","w");
	//判断返回值是否为空指针
	if(fp == NULL)
	{
		printf("%s\n",strerror(errno));
	}
	else
	{
		//输出字符串
		n = fprintf(fp,"%s %d",s.name,s.age);
		//输出应该是6个字符
		printf("%d\n",n);
	}
	//关闭文件
	fclose(fp);
	//把FILE*类型指针赋值为NULL
	fp = NULL;
	return 0;
}

代码5结果演示:

6
请按任意键继续. . .

C语言文件操作(超详细万字长文)_第2张图片

 

6 fscanf函数

int fscanf ( FILE * stream, const char * format, ... );

函数说明:从stream中读取数据,并根据参数格式将其存储到附加参数所指向的位置(地址)。

函数输入:stream:读取文件的指针;format:读取内容的格式(参考scanf);

函数输出:读取成功返回读取的项数,比如“%s %s”是两项,读取失败返回负数;

代码5演示:

# include 
# include 
# include 

typedef struct S 
{
	char name[10];
	int age;
};
int main()
{
	//初始化结构体
	struct S s;
	char n;
	FILE* fp;
	//fopen返回值是FILE*
	fp = fopen("test.txt","r");
	//判断返回值是否为空指针
	if(fp == NULL)
	{
		printf("%s\n",strerror(errno));
	}
	else
	{
		//读取格式数据
		n = fscanf(fp,"%s %d",s.name,&(s.age));
		//输出应该是2个字符
		printf("%d\n",n);
		printf("%s %d",s.name,s.age);
	}
	//关闭文件
	fclose(fp);
	//把FILE*类型指针赋值为NULL
	fp = NULL;
	return 0;
}

代码5结果演示:

2
jyy 23请按任意键继续. . .

7 fwrite函数

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

函数说明:将一个计数元素数组(每个元素的大小为字节)从ptr指向的内存块写入stream中的当前位置。

函数输入:ptr:要输入信息地址;size:每个元素大小;count:元素个数;stream要写入文件对应的FILE*类型指针;

函数输出:返回成功写入的元素总数。如果此数字与计数参数(count)不同,则写入错误会导致函数无法完成。在这种情况下,将为流设置错误指示器(ferror)。

代码演示:

# include 

int main ()
{
	size_t n;
	FILE * pFile;
	char buffer[] = { 'x' , 'y' , 'z' };
	pFile = fopen ("myfile.bin", "wb");
	if (pFile != NULL)
		n = fwrite (buffer , sizeof(char), sizeof(buffer), pFile);
	printf("%d\n%d\n",n,sizeof(buffer));
	fclose (pFile);
	pFile = NULL;
  return 0;
}

代码结果演示:

3
3
请按任意键继续. . .

结果以16进制保存。 

8 fread函数

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

函数说明:从stream中读取数据块;读取到ptr地址;

函数输入:ptr:指向大小至少为(size*count)字节的内存块的指针,转换为void*。;size:元素大小;count:元素个数;stream被读取的文件对应的FILE*类型指针;

函数输出返回成功读取的元素总数。如果此数字与计数参数不同,则表示读取时发生读取错误或到达文件结尾。在这两种情况下,都设置了适当的指示器,可分别使用ferror和feof进行检查。

代码演示:

#include 
#include 

int main () {
	FILE * pFile;
	long lSize;
	char * buffer;
	size_t result;
	
	pFile = fopen ( "myfile.bin" , "rb" );
	if (pFile==NULL) 
	{
		//stdrr表示标准错误
		fputs ("File error",stderr); 
		return 0;
	}

  // 获取文件大小:
  fseek (pFile , 0 , SEEK_END);
  lSize = ftell (pFile);
  rewind (pFile);

  // 申请内存:
  buffer = (char*) malloc (sizeof(char)*lSize);
  if (buffer == NULL) 
  {
	  fputs ("Memory error",stderr); 
	  return 0;
  }

  // 读取数据到buffer:
  result = fread (buffer,1,lSize,pFile);
  if (result != lSize)
  {
	  fputs ("Reading error",stderr); 
	  return 0;
  }

  // 结束
  fclose (pFile);
  free (buffer);
  return 0;
}

代码结果演示:

 

五、文件的随机读写

我们在读写文件时,从哪里开始读,从哪里开始写都是由文件指针位置决定的,第四节介绍的函数都是从文件开始位置读写的。然而有时候我们会根据自己需求来读写文件,这就要求要改变文件指针的位置,下面我们介绍三个改变文件指针位置的函数。

1 fseek函数

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

函数说明:根据文件指针的位置和偏移量来定位文件指针

函数输入:ofset:对于二进制文件:从起始偏移的字节数。对于文本文件:零或ftell返回的值。

origin:用作偏移参考的位置。它由中定义的以下常量之一指定,专门用作此函数的参数:SEEK_SET:SET:文件初始位置;SEEK_CUR:文件指针的当前位置;

函数输出:如果成功,函数返回零。否则,返回非零值(相对于origin位置)。

代码7演示:

# include 

int main ()
{
	FILE * pFile;
	pFile = fopen ( "test.txt" , "w" );
	if(pFile != NULL)
	{
		//输入字符串,并把文件指针转移到apple后面
		fputs ( "This is an apple." , pFile );
	    //把文件指针转移到a和n之间(从文件开始前进九个字符)
		fseek ( pFile , 9 , SEEK_SET );
	    //把 app四个字符替换为 sam
	    fputs ( " sam" , pFile );
	    //关闭文件
	    fclose ( pFile );
	    pFile = NULL;
	}

	return 0;
}

代码7结果演示:

C语言文件操作(超详细万字长文)_第3张图片 

2 ftell函数

long int ftell ( FILE * stream );

函数说明:返回文件指针位置相对于初始位置的偏移量(文件指针当前位置);

函数输入:指向读写文件的FILE*类型指针;

函数输出:成功后返回文件指针当前位置,失败返回-1;

代码8演示:

# include 

int main ()
{
	long end;
	FILE * pFile;
	long size;
	pFile = fopen ("test.txt","rb");
	if (pFile==NULL) 
		perror ("Error opening file");
	else
	{
		//文件结尾位置相对于文件末尾指针位置
		end = fseek (pFile, 0, SEEK_END);
		printf("test.txt文件指针位置:%d 字节\n",end);
		//文件结尾位置相对于初始位置指针位置
		size=ftell (pFile);
		fclose (pFile);
		printf ("test.txt文件大小: %ld 字节.\n",size);
	}
	return 0;
}

代码8结果演示:

test.txt文件指针值:0 字节
test.txt文件大小: 17 字节.
请按任意键继续. . .

3 rewind函数

void rewind ( FILE * stream );

函数说明:让文件指针位置回到初始值

函数输入:指向读写文件的FILE*类型指针;

代码9演示:

#include 
int main (){
	int n;
	FILE * pFile;
	char buffer [30];
	//打开一个读写文件
	pFile = fopen ("test.txt","w+");
	for ( n='A'; n<='Z' ; n++)
		fputc(n, pFile);
	//让文件指针回到初始位置
	rewind(pFile);
	//读取文件
	fgets(buffer,27,pFile);
	fclose (pFile);
	pFile = NULL;
	printf("%s\n",buffer);
	return 0;
}

代码9结果演示:

ABCDEFGHIJKLMNOPQRSTUVWXYZ
请按任意键继续. . .

六、文本文件和二进制文件

根据数据的组织形式,数据文件被称为 文本文件 或者 二进制文件 。 数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件 。 如果要求在外存上以ASCII 码的形式存储,则需要在存储前转换。以 ASCII 字符的形式存储的文件就是 本文件
一个数据在内存中是怎么存储的呢?
字符一律以 ASCII 形式存储,数值型数据既可以用 ASCII 形式存储,也可以使用二进制形式存储。
如有整数 10000 ,如果以 ASCII 码的形式输出到磁盘,则磁盘中占用 5 个字节(每个字符一个字节),而二进制形式输出,则在磁盘上只占4 个字节( VS2013 测试)。
代码10演示:
#include 
int main()
{
	int a = 10000;
	FILE* pf = fopen("test.txt", "wb");
	fwrite(&a, 4, 1, pf);//二进制的形式写到文件中
	fclose(pf);
	pf = NULL;
 return 0;
}

代码10结果演示:

 代码11演示:

#include 
int main()
{
	char a[] = "10000";
	FILE* pf = fopen("test.txt", "w");
	fputs(a,pf);//ASCII的形式写到文件中
	fclose(pf);
	pf = NULL;
 return 0;
}

代码11结果演示:

 七、文件读取结束的判定

在文件读取过程中,不能用 feof 函数的返回值直接用来判断文件的是否结束。而是应用于当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束
  • 文本文件读取是否结束,判断返回值是否为 EOF ,或者 NULL 。 例如: fgetc 判断是否为 EOF ,然后使用ferror和feof判断文件读取结束原因.fgets 判断返回值是否为 NULL ,然后使用ferror判断文件读取结束原因.
  • 二进制文件的读取结束判断,fread判断返回值是否小于实际要读的个数,然后使用ferror和feof判断文件读取结束原因。 

1 文本文件例子:

代码12演示:

#include 
#include 
int main(void)
{
	int c; // 注意:int,非char,要求处理EOF
	FILE* fp = fopen("test.txt", "r");
	if(!fp) {
		perror("File opening failed");
		return EXIT_FAILURE;
	}
	//fgetc 当读取失败的时候或者遇到文件结束的时候,都会返回EOF
	while ((c = fgetc(fp)) != EOF) 
	{ 
		putchar(c);
	}
	//判断是什么原因结束的
	if (ferror(fp))
		puts("I/O error when reading");
	else if (feof(fp))
		puts("End of file reached successfully");
	fclose(fp);
}

代码12结果演示:

10000
End of file reached successfully
请按任意键继续. .

2 二进制文件例子:

代码13演示:

#define _CRT_SECURE_NO_WARNINGS
#include 
enum {SIZE = 5};
int main(void)
{
	int n;
	double a[SIZE] = {1.,2.,3.,4.,5.};
	double b[SIZE];
	unsigned int ret_code;
	FILE *fp = fopen("test.bin", "wb"); // 必须用二进制模式
	fwrite(a, sizeof(double), SIZE, fp); // 写 double 的数组
	fclose(fp);
	fp = NULL;

	fp = fopen("test.bin","rb");
	ret_code = fread(b, sizeof(double), SIZE, fp); // 读 double 的数组
	if(ret_code == SIZE)
	{
		puts("Array read successfully, contents: ");
		for(n = 0; n < SIZE; ++n)
			printf("%f ", b[n]);
		putchar('\n');
	} 
	else
	{ // error handling
		if (feof(fp))
			printf("Error reading test.bin: unexpected end of file\n");
		else if (ferror(fp))
		{
			perror("Error reading test.bin");
		}
	}
	fclose(fp);
	fp =NULL;
}

代码13结果演示:

Array read successfully, contents:
1.000000 2.000000 3.000000 4.000000 5.000000
请按任意键继续. . .

八、文件缓冲区

ANSIC 标准采用 缓冲文件系统 处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“ 文件缓冲区 。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C 编译系统决定的。
C语言文件操作(超详细万字长文)_第4张图片

 代码14演示:

#include 
#include 

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;
}

代码14结果演示:

睡眠10秒-已经写数据了,打开test.txt文件,发现文件没有内容
刷新缓冲区
再睡眠10秒-此时,再次打开test.txt文件,文件有内容了
请按任意键继续. . .
这里可以得出一个 结论: 因为有缓冲区的存在,C 语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。如果不做,可能导致读写文件的问题。

你可能感兴趣的:(C语言学习,C,C++,c语言,开发语言)