C语言文件操作

目录

1.为什么要使用文件

2. 什么是文件

2.1 程序文件

2.2  数据文件

2.3 文件名

3. 二进制文件与文本文件

4. 文件的打开与关闭

4.1 流与标准流

4.1.1 流

4.1.2 标准流

4.2 文件指针

4.3 文件的打开与关闭

5. 文件的顺序读写

5.1 顺序读写函数介绍

5.2 scanf/fscanf/sscanf printf/sprintf/fprintf

5.3 fwrite

5.4 fread

6. 文件的随机读写

6.1 fseek

6.2 ftell

6.3  rewind

7. 文件结束读取的判定

7.1 feof

7.2 ferror

8. 文件缓冲区


1.为什么要使用文件

如果没有文件,我们写的程序的数据是存储在电脑的内存中,如果程序退出,内存回收,数据就丢
失了,
#include<stdio.h>
int main()
{
   int a = 10;
   return 0;
}//这里a里面确实放的是10的值,但是退出程序内存回收,就没有这个a了

等再次运行程序,是看不到上次程序的数据的,如果要将数据进行持久化的保存,我们可以使用
文件。

2. 什么是文件

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

2.1 程序文件

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

2.2  数据文件

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

2.3 文件名

⼀个文件要有⼀个唯⼀的文件标识,以便用户识别和引用。
文件名包含3部分:文件路径 + 文件名主干 + 文件后缀
例如,本篇文章的代码在笔者的电脑中存放的是
为了方便起见,文件标识常被称为文件名。

3. 二进制文件与文本文件

根据数据的组织形式,数据文件被称为文本文件或者⼆进制文件。
数据在内存中以⼆进制的形式存储,如果不加转换的输出到外存的文件中,就是⼆进制文件。
如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件
就是文本文件。
⼀个数据在文件中是怎么存储的呢?
字符⼀律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用⼆进制形式存储。
这里以整数10000举例
这时候来验证一下
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main()
{
	FILE* fp1 = fopen("data.txt", "wb");
	int a = 10000;
	fwrite(&a, sizeof(a), 1, fp1);//这里表示将a这个数据以二进制的方式写进文件data.txt里面去
	fclose(fp1);
	fp1 = NULL;
	return 0;
}
这里我们通过VS2019用二进制形式打开这个文件看看里面存放的是啥
这里可以看到是以16进制的形式存放的,VS中采用的是小端存放的方式进行储存
(在笔者这篇文章 http://t.csdnimg.cn/jWX9K中有提到),我们调出计算器验证一下
可以看到是符合的。

4. 文件的打开与关闭

在生活中,喝一瓶水,需要以下步骤

1.拧开瓶盖

2.喝水

3.关上瓶盖

那么,类似的,对文件的操作有以下步骤

1.打开文件

2.操作

3.关闭文件

4.1 流与标准流

4.1.1 流

我们程序的数据需要输出到各种外部设备,也需要从外部设备获取数据
红色的矩形圈出来的我们统称他们位外部设备,在一个C程序要进行读,写时,需要程序员细致了

解到每一个外部设备与C程序是怎么进行操作的,这无疑是非常麻烦的。为了方便程序员对各种设

备进行方便的操作,我们抽象出了流的概念,我们可以把流想象成流淌着字符的河。

C程序针对文件、画面、键盘等的数据输入输出操作都是通过流操作的
⼀般情况下,我们要想向流里写数据,或者从流中读取数据,都是要打开流,然后操作。

4.1.2 标准流

那为什么我们从键盘输入数据,向屏幕上输出数据,并没有打开流呢?
其实 是因为C语言程序在启动的时候,默认打开了3个流:
stdin - 标准输入流,在大多数的环境中从键盘输入,scanf函数就是从标准输入流中读取数据。
stdout - 标准输出流,大多数的环境中输出至显示器界面,printf函数就是将信息输出到标准输出
流中。
stderr - 标准错误流,大多数环境中输出到显示器界面。
这是默认打开了这三个流,我们使用scanf、printf等函数就可以直接进行输入输出操作的。
stdin、stdout、stderr 三个流的类型是: FILE* ,通常称为文件指针。
C语言中,就是通过 FILE* 的文件指针来维护流的各种操作的。

4.2 文件指针

缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。
每个被使用的文件都在内存中开辟了⼀个相应的文件信息区,用来存放文件的相关信息(如文件的
名字,文件状态及文件当前的位置等)。这些信息是保存在⼀个结构体变量中的。该结构体类型
由系统声明的,取名 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 * fp;
定义pf是⼀个指向FILE类型数据的指针变量。可以使pf指向某个⽂件的⽂件信息区(是⼀个结构体
变量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够间接找
到与它关联的文件。
如下:

4.3 文件的打开与关闭

文件在读写之前应该先打开文件,在使用结束之后应该关闭文件。
在编写程序的时候,在打开文件的同时,都会返回⼀个FILE*的指针变量指向该文件,也相当于建
立了指针和文件的关系。
ANSIC 规定使用 fopen 函数来打开文件, fclose 来关闭文件。
mode表示文件打开的方式,有下列:
文件打开方式含义若指定文件不存在
"r"(只读)为输入数据,打开一个已经存在的文件出错
"w"(只写)为写数据,打开一个文本文件建立一个新的文件
"a"(追加)
向⽂本⽂件尾添加数据
建立一个新的文件
"rb"(只读)
为了输⼊数据,打开⼀个⼆进制⽂件
出错
"wb"(只写)为输出数据,打开一个二进制文件建立一个新的文件
"ab"(追加)向一个二进制文件尾添加数据建立一个新的文件
"r+"(读写)为了读和写,打开一个文本文件出错
"w+"(读写)
为了读和写,建立⼀个新的⽂件
建立⼀个新的⽂件
"a+"(读写)
打开⼀个⽂件,在⽂件尾进⾏读写
建立⼀个新的⽂件
 "rb+"(读写)
为了读和写打开⼀个⼆进制⽂件
出错
“wb+”(读写)
为了读和写,新建⼀个新的⼆进制⽂件
建立一个新的文件
"ab+"(读写)
打开⼀个⼆进制⽂件,在⽂件尾进⾏读和写
建立一个新的文件

int main ()
{
 FILE * pFile;
 //打开⽂件
 pFile = fopen ("myfile.txt","w");
 //⽂件操作
 if (pFile!=NULL)
 {
 fputs ("fopen example",pFile);
 //关闭⽂件
 fclose (pFile);
 }
 return 0;
}

5. 文件的顺序读写

5.1 顺序读写函数介绍

函数名功能适用于
fgetc字符输入函数所有输入流
fputc字符输出函数所有输出流
fgets文本行输入函数所有输入流
fpuits文本行输出函数所有输出流
fscanf格式化输入函数所有输入流
fprintf格式化输出函数所有输出流

fread

二进制输入文件
fwrite二进制输出文件

下面是示例当首次打开一个文件的时候,文件指针是默认指向文件开头的位置

我们可以看到,他的返回值是返回读到的字符,那为什么返回值要设定为int类型呢

这里可以看到,当读取发生错误或读取到文件尾的时候,会返回EOF,而EOF就是-1,为了适合所有情况,所以将返回值类型设置成int。

类似的,fgetc也是如此

//在当前目录下放一个文件data.txt,写一个程序,将data.txt文件拷贝一份,生成data_copy.txt文件。

int main()
{
	FILE* fp1 = fopen("data.txt", "r");//从data.txt里面读
	FILE* fp2 = fopen("data_copy.txt", "w");//写道data_copy里面去
	int ch = 0;
	if (fp1 == NULL || fp2 == NULL)//判断是否打开失败
	{
		perror("fopen");
		return 1;
	}
	while ((ch = fgetc(fp1)) != EOF)
	{
		fputc(ch, fp2);
	}
	fclose(fp1);
	fclose(fp2);
	FILE * fp3 = fopen("data_copy.txt", "r");//以读的方式打开,把拷贝的读出来,看看是不是拷进去了
	int b = 0;
	if (fp3 == NULL)
	{
		perror("fopen");
		return 1;
	}
	while ((b = fgetc(fp3))!= EOF)
	{
		printf("%c ", b);
	}
	fclose(fp3);
	return 0;
}

data.txt中存放的是下列

下面是fgets的运用

int main()
{
	char arr[10];
	FILE* p1 = fopen("data.txt","r");//以读的方式打开
	if (p1 == NULL)//判断是否能正常打开
	{
		perror("fopen");//如果打开失败,打印一下错误信息
		return 1;
	}
	fgets(arr, 7, p1);//给定7个字符的长度,不够读整个第一行的字符数
	printf(arr);//这边输出结果是Abcdef,
	//调试可以看到因为要留个位置放\0

	fgets(arr, 7, p1);//这里给定字符长度超出了文件指针所指向的字符串长度
	//可以看到只会在原字符串后打印一个g,以及一个空的行
	printf(arr);//调试可以看到,将f后的g \n \0都读进去后,就没有再读了

	fgets(arr, 7, p1);//这里已经从下一行开始读了,长度同样也不够读第二行的字符
	printf(arr);

	fgets(arr, 7, p1);//长度同样也不够读文件指针指向的字符串长度
	printf(arr);//可以看到会输出world!,调试看到同样的,要留个位置放 \0 

	fgets(arr, 7, p1);
	printf(arr);
	return 0;
}

得出结论,当fgets从文件中读字符串的时候,始终要给str留一个元素的空间放\0

 如果当前文件指针所指向的字符串长度小于n-1(有个空间要放\0)时,fgets在进行读操作时,会将\n也读过来

 如果当前文件指针所指向的字符串长度大于等于n时,那么会读n-1个字符(有个空间要放\0)

这里是fputs的运用

int main()
{
	char arr1[20] = { "hello world!" };
	char arr2[20] = { "work at alibaba\n" };
	char arr3[20] = { "china num.1\n" };
	file* fp1 = fopen("data2.txt", "w");//fputs往文件里写
	if (fp1 == null)
	{
		perror("fopen");
		return 1;
	}
	fputs(arr1, fp1);
	fputs(arr2, fp1);//这里运行程序后,发现文件中将两组字符串都写在了同一行
	fputs(arr3, fp1);//这里运行后,可以发现文件换行写入了,原因就是arr1字符串没有\n
	//而arr2字符串有\n
	fputs("china is the most powerful country in this city!\n", fp1);
	//也可以直接这样写字符串进去
	fclose(fp1);
	return 0;
}

上面说的适用于所有输入流⼀般指适用于标准输入流和其他输入流(如文件输入流);所有输出流
⼀般指适用于标准输出流和其他输出流(如文件输出流)
下面来看一下一组函数:

5.2 scanf/fscanf/sscanf printf/sprintf/fprintf

scanf与printf我们已经非常熟悉

我们来看一下printf 与 fprintf 这两个函数

可以看到这两函数在参数上的区别仅是fprintf多了一个文件类型的指针

struct S
{
	char name[20];
	int age;
};
int main()
{
	FILE* fp1 = fopen("test.txt", "w");
	if (fp1 == NULL)
	{
		perror("fopen");
		return 1;
	}
	struct S s = { "张三",38 };
	fprintf(fp1, "%s %d", s.name, s.age);//将s的信息输出到文件里去
	fclose(fp1);
	return 0;
}

这里我们打开文件看一下,是不是写进去了

可以看到,确实是写进去了,且是以文本的方式写进去的。

既然可以写进去, 那就可以读出来

int main()
{
	struct S s = { 0 };
	FILE* fp1 = fopen("test.txt", "r");//将数据再读出来
	if (fp1 == NULL)
	{
		perror("fopen");
		return 1;
	}
	fscanf(fp1, "%s %d", s.name, &(s.age));
	printf("%s %d", s.name, s.age);
	fclose(fp1);//看一下都出来没

	return 0;
}

那么我们其实也可以用fprintf进行打印


int main()
{
	struct S s = { 0 };
	FILE* fp1 = fopen("test.txt", "r");//将数据再读出来
	if (fp1 == NULL)
	{
		perror("fopen");
		return 1;
	}
	fscanf(fp1, "%s %d", s.name, &(s.age));
	fprintf(stdout ,"%s %d", s.name, s.age);//stdout就是显示器
	fclose(fp1);//看一下都出来没

	return 0;
}

也是没问题的

再来看下sprintf sscanf

sscanf

从指定的字符串中读格式化的数据出来

int main()
{
	char arr1[20] = {"王五 48"};
	struct S s;
	sscanf(arr1, "%s %d", s.name, &(s.age));//name本身就是数组名,是个地址,所以不用&
	printf("%s %d",s.name,s.age);//看一下成没成功
	return 0;
}

sprintf

将格式化的字符串写入到一个字符串中去

int main()
{
	char arr1[20];
	struct S s = { "李四",38 };
	sprintf(arr1, "%s %d", s.name, s.age);
	printf(arr1);//看一下成没成功
	return 0;
}

可以看到都成功了

总结:printf :把数据以格式化的形式打印在标准输出流上

fprintf:把数据以格式化的形式打印在指定输出流上

sprintf:把格式化数据转化为字符串

scanf:从标准输入流上读取格式化的数据

fscanf:从指定的输入流上读取格式化的数据

sscanf:从字符串中读格式化的数据

5.3 fwrite

可以看到buffer指向的是我们想要写入的数据,size是我们写入的数据的类型大小,count是我们想

要写入多少个size类型的数据。

int main()
{
	int arr[] = { 10,11,12,13,14,15 };
	FILE* fp1 = fopen("datab.txt", "wb");//二进制的形式写
	if (fp1 == NULL)
	{
		perror("fopen");
		return 1;
	}
	int sz = sizeof(arr) / sizeof(arr[0]);
	fwrite(arr, sizeof(int), sz, fp1);//以二进制方式写进去
	fclose(fp1);
	return 0.;
}

为了验证有没有写进去,我们再以二进制的方式读出来试试

5.4 fread

int main()
{
	int arr1[10] = { 0 };
	FILE* fp2 = fopen("datab.txt", "rb");//二进制的形式写
	if (fp2 == NULL)
	{
		perror("fopen");
		return 1;
	}
	fread(arr1, sizeof(int), 10, fp2);//以二进制方式写进去
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", arr1[i]);
	}
	fclose(fp2);
	return 0.;
}

可以看到 是没问题的。

同时我们在这里可以看到,要求fread是读10个,但通过fwrite写进去的元素是没有10个的,但是

fread还是成功读取了,我们可以看到fread的返回值是size_t,所以我们可以这样写

int main()
{
	int arr1[10] = { 0 };
	FILE* fp2 = fopen("datab.txt", "rb");//二进制的形式写
	if (fp2 == NULL)
	{
		perror("fopen");
		return 1;
	}
	int i = 0;
	while (fread(&arr1[i], sizeof(int), 1, fp2))//根据返回值进行判断
	{
		printf("%d ", arr1[i]);
		i++;
	}
	fclose(fp2);
	return 0.;
}

可以看到 也是没问题的。

注:文本形式与二进制形式不存在绝对的效率高低,只是提供更多选择

6. 文件的随机读写

6.1 fseek

根据文件指针的位置和偏移量来定位文件指针。

这个参数的意思是从origin这个位置开始偏移多少(正数为向后偏移,负数为向前偏移

这个参数有以下选择


int main()
{
	FILE* fp = fopen("test.txt", "r");
	if (fp == NULL)
	{
		perror("fopen");
		return 1;
	}
	int ch = fgetc(fp);
	printf("%c\n", ch);

	fseek(fp, 4, SEEK_CUR);//从当前指针位置进行了偏移
	ch = fgetc(fp);
	printf("%c\n", ch);//看看偏移成功没

	fseek(fp, 4, SEEK_SET);//从文件起始进行了偏移
	ch = fgetc(fp);
	printf("%c\n", ch);//看看偏移成功没

	fseek(fp, -4, SEEK_END);//从文件尾进行了偏移
	ch = fgetc(fp);
	printf("%c\n", ch);//看看偏移成功没

	fclose(fp);
	return 0;
}

这是文件中存放的数据

可以看到是符合的

6.2 ftell

返回文件指针相对于起始位置的偏移量

这个没啥好说的

当文件指针指向文件尾的时候返回的偏移量及字符个数

6.3  rewind

让文件指针的位置回到文件的起始位置 ​​​​​​​
这个也没啥好说的,就是让文件指针返回文件起始的位置
int main()
{
	FILE* fp = fopen("test.txt", "r");
	if (fp == NULL)
	{
		perror("fopen");
		return 1;
	}
	int ch = fgetc(fp);
	printf("%c\n", ch);

	fseek(fp, 4, SEEK_CUR);//从当前指针位置进行了偏移
	ch = fgetc(fp);
	printf("%c\n", ch);//看看偏移成功没

	fseek(fp, 4, SEEK_SET);//从文件起始进行了偏移
	ch = fgetc(fp);
	printf("%c\n", ch);//看看偏移成功没

	fseek(fp, -4, SEEK_END);//从文件尾进行了偏移
	ch = fgetc(fp);
	printf("%c\n", ch);//看看偏移成功没

	rewind(fp);
	ch = fgetc(fp);
	printf("%c\n", ch);


	fclose(fp);
	return 0;
}

返回后输出的就是起始的那个字符

7. 文件结束读取的判定

在我们打开一个流的时候,这个流上会有两个标记值

一个标记值是是否遇到文件末尾

另一个是是否发生错误

任意一个流上都有这两个标记值

7.1 feof

作用是:当文件读取结束的时候,判断读取结束的原因是否是:遇到文件尾结束。(检测是否设置
了文件末尾的标记值)
注:不能直接用feof的返回值来判断文件结束

那么如何判断文件是否读取结束呢?

1.文本文件是否读取结束

Ⅰ fgetc如果在读取的时候遇到文件尾,或者发生错误,那么就返回EOF

Ⅱ fgets在读取的时候遇到文件尾,或者发生错误,那么就返回NULL

2.二进制文件是否读取结束

判断返回值是否是小于实际要读的个数

7.2 ferror

作用是:当文件读取结束的时候,判断读取结束的原因是否是:遇到错误结束。(检测是否设置

了错误的标记值)
int main()
{
	int c; 
	FILE* fp = fopen("test.txt", "r");
	if (!fp) 
	{
		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);
	return 0;
}

二进制例子

enum { SIZE = 5 };
int main(void)
{
	double a[SIZE] = { 1.,2.,3.,4.,5. };
	FILE* fp = fopen("test.bin", "wb"); // 必须⽤⼆进制模式
	fwrite(a, sizeof * a, SIZE, fp); // 写 double 的数组
	fclose(fp);
	double b[SIZE];
	fp = fopen("test.bin", "rb");
	size_t ret_code = fread(b, sizeof * b, SIZE, fp); // 读 double 的数组
	if (ret_code == SIZE) 
	{
		puts("Array read successfully, contents: ");
		for (int 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);
	return 0;
}

8. 文件缓冲区

ANSIC 标准采用“缓冲文件系统” 处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中
为程序中每⼀个正在使用的文件开辟⼀块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的
缓冲区,装满缓冲区后才⼀起送到磁盘上。如果从磁盘向计算机读⼊数据,则从磁盘文件中读取数
据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量
等)。缓冲区的大小根据C编译系统决定的。
因为有缓冲区的存在,C语⾔在操作⽂件的时候,需要做刷新缓冲区或者在⽂件操作结束的时候
关闭文件。 如果不做,可能导致读写文件的问题。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值