C语言文件操作

一.前言

1.为什么使用文件

我们平常在写代码的时候可能会遇到这样一个问题:代码在运行完之后,你不管输入的什么东西都没了,你第二次运行的时候就要重新输入你想输入的东西。
我们有没有什么办法能让你输入好的东西保存起来,下次运行的时候还可以用?
我们平时在玩电脑的时候想保存一个东西的时候,是不是可以新建一个文件夹,这样你电脑即使关了也不用担心。因为文件我们可以将数据直接存放在电脑的硬盘上,做到了数据的持久化。
所以我们可不可以将C语言中输入的数据也能存到我们电脑上去呢?

2.什么是文件

一般我们把磁盘上的文件都称为文件

但是在程序设计中,我们谈到的一般是:程序文件,数据文件。

2.1程序文件

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

2.2数据文件

文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。

我们后面讨论的都是数据文件,因为我们目的就是把数据存起来。比如一个text.txt文件。

以前我们用到的printf,scanf这些函数时,我们操作的对象是键盘,屏幕。输入的时候从键盘输进去,输出的时候就是从屏幕输出,你就可以看到了。
而今天,我们操作的对象就是文件。

2.3文件名

一个文件要有一个唯一的文件标识(文件名),以便用户识别和引用。
文件名包含3部分:文件路径+文件名主干+文件后缀
eg.c:\code\test.txt

二.打开和关闭文件

在我们接触动态内存开辟的时候,先创建一块空间,在用完之后在销毁。文件也和这个类似,在用到的时候打开文件,然后对文件里面的内容进行操作,操作完在关闭文件。

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;
FILE* pf;//文件指针变量

这个结构体的类型通过重定义定义成FILE。文件里的信息都有什么,这些我们不需要关心。我们只要记住这个结构体类型即可。

不同的C编译器的FILE类型包含的内容不完全相同,但是大同小异

我们知道这个结构体类型后,我们一般通过FILE类型的指针来维护我们打开的这个文件

 FILE* pf;//这就是我们定义的文件指针
 //指针名:pf
 //指针类型:FILE

我们通过我们定义的这个指针就可以找到我们需要操作的文件的文件信息区,通过文件信息区就可以访问这个文件了。

2.文件的打开和关闭

ANSIC 规定使用fopen函数来打开文件,fclose来关闭文件。

2.1打开文件

//打开文件
FILE * fopen ( const char * filename, const char * mode );

参数

filename:你需要打开的文件的文件名,如果没有这个文件就会自动给你创建一个
mode:你这个文件的打开方式

返回值

我们刚才说过,系统会自动为你的文件定义一个结构体(文件信息区),返回值,就是返回一个FILE类型的指针,这个指针指向的就是这个结构体的地址。
如果文件打开失败,则返回一个空指针

打开模式
在这里插入图片描述

这里的模式在写的时候都要加上“ ”,如果你加的是‘ ’就会报错

2.1.1操作模式"r",“w”,“a”

“r”,只读,对文件进行读的操作,如果你要读的文件找不到就会出错,在下面代码我把"w"换成"r"。我们看结果

int main()
{
	FILE* p = NULL;
	//打开文件
	p = fopen("C:\\lizi\\test.txt", "r");
	if (p == NULL)
	{
		perror("fopen");
		return 1;
	}

	//关闭文件
	fclose(p);
	p = NULL;
	return 0;
}

在这里插入图片描述

“w",只写操作,如果要写的那个文件没有,他不会报错,而是自己帮你自动创建一个。
如果文件里面本来就有内容,就会将其销毁然后重新录入

int main()
{
	FILE* p = NULL;
	FILE* p1 = NULL;

	//打开文件
	p = fopen("C:\\lizi\\test.txt", "w");
	if (p == NULL)
	{
		perror("fopen");
		return 1;
	}

	//操作文件
	fputc('a', p);
	//后面会讲,就是将文件里面写一个字符a

	//关闭文件
	fclose(p);
	p = NULL;

	//再打开一次
	p1 = fopen("C:\\lizi\\test.txt", "w");
	if (p1 == NULL)
	{
		perror("fopen");
		return 1;
	}

	fputc('b', p1);

	//关闭文件
	fclose(p1);
	p1 = NULL;
	return 0;
}

我们将这个文件打开两次,第一次写入字符"a"第二次写入"b"。我们看文件会有什么变化:
在这里插入图片描述
我们发现文件里只有一个第二次写的“b”,说明在执行写的操作时,会把之前有的值销毁掉,然后重新录入。

“a”,追加操作,在向文件写入新数据之前不删除EOF标记的情况下,在文件末尾(追加)打开用于写入; 如果文件不存在,则首先创建该文件。 我们刚才看到写的操作,发现每次打开都会销毁之前的数据,但我们希望他能把第一次的数据留着:

int main()
{
	FILE* p = NULL;
	FILE* p1 = NULL;

	//打开文件
	p = fopen("C:\\lizi\\test.txt", "w");
	if (p == NULL)
	{
		perror("fopen");
		return 1;
	}

	//操作文件
	fputc('a', p);
	//后面会讲,就是将文件里面写一个字符a

	//关闭文件
	fclose(p);
	p = NULL;

	//再打开一次
	p1 = fopen("C:\\lizi\\test.txt", "a");
	if (p1 == NULL)
	{
		perror("fopen");
		return 1;
	}

	fputc('b', p1);

	//关闭文件
	fclose(p1);
	p1 = NULL;
	return 0;
}

我们第二次打开文件的时候,以追加的方式打开,看看效果:
在这里插入图片描述
发现两次写入的数据都存起来了。

2.1.2操作模式“rb”“wb”“ab”

同样是写,读,追加三种模式,只是这三个操作的是二进制文件,在后面用函数fread,fwrite时会用到。

2.1.3操作模式“r+”“w+”“a+”

r+
打开一个文件进行更新(用于输入和输出)。该文件必须存在。

w+
创建一个空文件并打开它进行更新(输入和输出)。如果已经存在具有相同名称的文件,则其内容将被丢弃,并将该文件视为新的空文件。

a+
打开一个文件进行更新(包括输入和输出),所有输出操作都在文件末尾写入数据。如果文件不存在,则创建该文件。

2.1.4操作模式“rb+”“wb+”“ab+”

和r+,w+,a+类似,只是它们操作的是二进制文件

2.1.5随便看看

新的C标准(C2011,它不是c++的一部分)增加了一个新的标准子说明符(“x”),它可以被附加到任何"w"说明符(形成"wx", “wbx”, “w+x"或"w+bx”/“wb+x”)。如果文件存在,此子说明符强制函数失败,而不是覆盖它。

对于打开进行更新的文件(包含“+”号的文件),允许对其进行输入和输出操作,在写入操作之后的读取操作之前,流将被刷新(fflush)或重新定位(fseek、fsetpos、rewind)。在读取操作之后的写入操作(只要该操作没有到达文件末尾)之前,流将被重新定位(fseek, fsetpos, rewind)。

2.2关闭文件

//关闭文件
int fclose ( FILE * stream );

参数

一个FILE类型的指针,这个指针指向的是你需要关闭的那个文件的结构体的地址。

返回值

int类型,关闭成功返回0,关闭失败则返回EOF

2.3举例

在这里插入图片描述
首先我们看这里是没有test.txt文件的。

int main()
{
	FILE* p = NULL;
	//打开文件
	p = fopen("test.txt", "w");

	//关闭文件
	fclose(p);
	p = NULL;
	return 0;
}

我们现在还没有对文件进行操作,如果我们本身就没有这个文件,运行后就会自动给你创建一个。
在这里插入图片描述
这就说明我们创建好了,如果本身就有这个文件,就不会给你创建。

这里我们有没有注意一个问题,我们这个文件是创建在哪里的?

如果我们在fopen里面只写一个文件名,我们叫这个为相对路径
会自动为我们在我们这个程序的工程文件夹里创建

如果我们在前面在添上地址,就是绝对路径。会在我们指定的位置上创建。
在这里插入图片描述

现在我们就在C盘的“lizi”文件夹上创建一个

int main()
{
	FILE* p = NULL;
	//打开文件
	p = fopen("C:\\lizi\\test.txt", "w");
	if (p == NULL)
	{
		perror("fopen");
		return 1;
	}

	//关闭文件
	fclose(p);
	p = NULL;
	return 0;
}

在这里插入图片描述
看,这样我们就创建好了

这里注意,写路径的时候要用"\ \ ",因为只有一个斜杠的话,系统会认为他是转义字符

三.文件的读写(顺序读写)

在这里插入图片描述

3.1字符输入输出函数

3.1.1fputc函数

int fputc( FILE *stream );

参数

一个指向文件的指针,我们要知道把字符写到哪里去

返回值

返回读取为int的字符,或返回EOF以指示错误或文件结束
比如你想在文件里写个字符’c’,它返回的就是c。

再用fputc这种输出函数时,文件打开方式要用写,否则你输出不进去。

例子:

int main()
{
	FILE* p = NULL;

	p = fopen("C:\\lizi\\test.txt", "w");
	if (p == NULL)
	{
		perror("fopen");
		return 1;
	}

	fputc('a', p);


	fclose(p);
	p = NULL;
	return 0;
}

我们看结果:
在这里插入图片描述
发现我们确实把字符a放进去了。那我们想录进去abcd呢?

int main()
{
	FILE* p = NULL;

	p = fopen("C:\\lizi\\test.txt", "w");
	if (p == NULL)
	{
		perror("fopen");
		return 1;
	}

	fputc('a', p);
	fputc('b', p);
	fputc('c', p);
	fputc('d', p);


	fclose(p);
	p = NULL;
	return 0;
}

既然是顺序读写,我们就可以在后面继续添加自己希望添加的字符,这样我们再看结果:
在这里插入图片描述
的确是的。

但是这样写,为什么可以在后面继续添加呢?

我们可以这样理解,每录入一个字符,这个文件的文件指针向后偏移一位,然后继续录入。
但是不要与指向这个文件的指针弄混了,你不管怎么添加字符,这个指向文件的指针都是不会动的。

3.1.2fgetc函数

字符输入函数。

int fgetc( FILE *stream );

参数

一个文件类型的指针,这个指针指向的是一个文件,你可以在这个文件里得到字符。

返回值

返回读取为int的字符,或返回EOF以指示错误或文件结束。

举例:我们在此之前现在相应文件写入abcd。

int main()
{
	FILE* p = NULL;
	p = fopen("text.txt", "r");
	if (p == NULL)
	{
		perror("fopen()");
		return;
	}

	printf("%c\n", fgetc(p));
	printf("%c\n", fgetc(p));
	printf("%c\n", fgetc(p));
	printf("%c\n", fgetc(p));

	fclose(p);
	p = NULL;
	return 0;
}

我们看输出:
在这里插入图片描述
在这里我们就把文件里的内容读出来了。如果再多写一行的话,得到的就是空,也就是什么都没有。

注意这里文件打开方式可不能写成“w”,因为这不仅会写不出来,还会把你文件本来就有的内容清除掉。

3.2文本行输入输出函数

3.2.1fputs函数

我们再用fputc函数时,发现想输入很多字符的时候要连续用到很多fputc函数。这样很麻烦,所以我们就有了fputs函数-一行一行的输入

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

参数

FILE *stream:这个应该很好明白,就是想输入到哪个文件里面。
const char *string:这个就是我们想输入的那个字符串的地址。

返回值

如果成功,这些函数都返回一个非负值。 出现错误时,fputs返回EOF

举例

int main()
{
	FILE* p = NULL;
	p = fopen("text.txt", "w");
	if (p == NULL)
	{
		perror("fopen()");
		return;
	}

	fputs("hello", p);

	fclose(p);
	p = NULL;
	return 0;
}

我们看结果:
在这里插入图片描述
如果我们再写一行:

int main()
{
	FILE* p = NULL;
	p = fopen("text.txt", "w");
	if (p == NULL)
	{
		perror("fopen()");
		return;
	}

	fputs("hello", p);
	fputs("word", p);

	fclose(p);
	p = NULL;
	return 0;
}

我们文件里显示的时候会分成两行显示吗?

在这里插入图片描述
发现并不可以,他只是一行一行的写入,并不能帮你换行。

int main()
{
	FILE* p = NULL;
	p = fopen("text.txt", "w");
	if (p == NULL)
	{
		perror("fopen()");
		return;
	}

	fputs("hello\n", p);
	fputs("word", p);

	fclose(p);
	p = NULL;
	return 0;
}

我们手动加一个换行符,再来看结果:
在这里插入图片描述
发现这样就可以了,\n确实也可以帮我们在文件里也能达到换行的效果。

3.2.2fgets函数

从文件里得到一行字符串。

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

参数

FILE *stream:需要从指向的文件里得到字符串
int n:需要从这个字符串里得到多少个字符
char *string:你得到的这个字符串放到的地方,一般是放到字符数组里。

返回值

返回这个字符串,如果结束或者返回失败则返回NULL

例子:在此之前我们文件里有两行字符串”hello“,”word“

int main()
{
	FILE* p = NULL;
	p = fopen("text.txt", "r");
	if (p == NULL)
	{
		perror("fopen()");
		return;
	}

	char str[20] = { 0 };
	fgets(str, 3, p);
	printf("%s", str);

	fclose(p);
	p = NULL;
	return 0;
}

在这里我们希望能得到文件里的字符串,并且这个字符串有三个字符,我们看结果是不是输出"hel"。
在这里插入图片描述
我们发现得到的结果只有两个字符,这是为什么呢?我们通过调试看看:(我们将str数组里面先放入字符#,这样可以清楚的看见传进来的是什么)
在这里插入图片描述

我们发现,它除了传进来一个”he“还传进了一个\0,这三个就是我们要的字符串了。所以如果我们希望读到n个字符,就要在函数里写n+1。

我们记得这个文件里第一行是hello,如果我们希望一下子读10个字符,他会不会继续往第二行开始读呢?

int main()
{
	FILE* p = NULL;
	p = fopen("text.txt", "r");
	if (p == NULL)
	{
		perror("fopen()");
		return;
	}

	char str[20] = { "############" };
	fgets(str, 10, p);
	printf("%s", str);

	fclose(p);
	p = NULL;
	return 0;
}

我们直接看调试结果吧:
在这里插入图片描述

我们发现并不会,他只把第一行读完就结束了,根本不管其他行,他甚至换行符都能读出来。

我们看结果:
在这里插入图片描述

hello下面有个空格,是因为它把换行符也打印出来,就直接给你换行了。

如果我们希望把第二行也打印出来该怎么办呢?

int main()
{
	FILE* p = NULL;
	p = fopen("text.txt", "r");
	if (p == NULL)
	{
		perror("fopen()");
		return;
	}

	char str[20] = { "############" };
	fgets(str, 10, p);
	printf("%s", str);

	char str1[20] = { "############" };
	fgets(str1, 10, p);
	printf("%s", str1);

	fclose(p);
	p = NULL;
	return 0;
}

既然是顺序读写,再来一段代码即可,而且第一行本身就可以换行,也不需要我们再多写\n.
我们看结果:
在这里插入图片描述

3.3格式化输入输出函数

3.3.1fprintf函数

我们之前想在文件里写一个字符,一个字符串都是很容易的事情,假如我现在有个结构体,我们想把这个结构体里的内容写到文件里去,只用fputs,fputc是不是挺麻烦的。但是fprintf函数就可以写这种麻烦的东西。

我们看到fprintf函数第一个想到的是不是printf函数?我们现在来对比一下二者,看看有什么区别

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

我们发现基本都一样啊?就是fprintf函数多了一个参数而已,而这个参数不就是一个文件指针吗?加上这个不就可以到一个文件里面写入任何想写的东西了吗?

返回值

返回写入的字节数,如果写入失败,则返回一个负值

例子

struct S
{
	char str[20];
	int age;
};

int main()
{
	struct S s = { { "zhangsan "}, { 20 } };
	FILE* p = NULL;
	p = fopen("text.txt", "w");
	if (p == NULL)
	{
		perror("fopen()");
		return;
	}

	fprintf(p, "%s %d", s.str, s.age);

	fclose(p);
	p = NULL;
	return 0;
}

平时怎么用printf函数,在这里就怎么用,写完在前面加个文件指针就可以了。

我们看结果:
在这里插入图片描述

3.3.2fscanf函数

同理我们先将fscanf函数与scanf函数比较:

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

这也是多了文件指针的参数。

返回值

返回分配的字段的数量。返回值为0表示没有分配字段。如果发生错误,或者在第一次转换之前到达文件流的末尾,返回的是小于格式串中指定的数据的个数.

例子:文件里我们放的就是zhangsan 20

int main()
{
	struct S s = { 0 };
	FILE* p = NULL;
	p = fopen("text.txt", "r");
	if (p == NULL)
	{
		perror("fopen()");
		return;
	}

	fscanf(p, "%s %d", s.str, &(s.age));
	printf("%s %d", s.str, s.age);

	fclose(p);
	p = NULL;
	return 0;
}

在这里我们就是将文件里的内容以%s %d的形式写到结构体s里面,然后我们用printf打印出了。

我们看结果:
在这里插入图片描述
我们再来看看这个函数的返回值是多少:

int main()
{
	struct S s = { 0 };
	FILE* p = NULL;
	p = fopen("text.txt", "r");
	if (p == NULL)
	{
		perror("fopen()");
		return;
	}

	printf("%d\n", fscanf(p, "%s %d", s.str, &(s.age)));
	printf("%s %d", s.str, s.age);

	fclose(p);
	p = NULL;
	return 0;
}

我们看结果:
在这里插入图片描述
说明函数返回的字符段是2个,一个zhangsan,一个20

3.4二进制输入输出

二进制输入输出的函数,需要再打开文件时“rb”“wb”“ab"方式打开。

3.4.1fwrite函数

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

参数

void *buffer:是你需要写道文件里的那块内容的地址
size_t size:你要写入的那块空间的大小
size_t count:你需要写的元素的个数
FILE *stream:被写到的文件的地址

返回值

返回的可以理解为count的值,返回实际读取的个数,如果发生错误或在达到count之前遇到文件结束,则该数可能小于count,返回一个小于count的数。

例子

int main()
{
	FILE* p = NULL;
	struct S s = { {"zhangsan"},{20},{75.5f} };
	p = fopen("text.txt", "wb");
	if (p == NULL)
	{
		perror("fopen");
		return;
	}

	printf("%zd\n", fwrite(&s, sizeof(struct S), 1, p));

	fclose(p);
	p = NULL;
	return 0;
}

我们希望将1个结构体的数据写入文件中

我们看结果:
在这里插入图片描述
我们发现,确实写进来了,但是除了zhangsan,我们好像其他的都看不懂,这是为什么呢?

因为我们是以二进制的形式写进去的,那记事本打开肯定是看不懂的,但是不要紧,虽然我们看不懂,但是我们的电脑看得懂,我们再以二进制形式读出来也是可以的。

3.4.2fread函数

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

我们发现参数和fwrite完全一样,但是每个参数的意义还是略有区别。

fwrite是从buffer里面写入count个数据,每个数据大小是size。写到stream里面
fread是从文件stream里面,拿count个数据,每个数据大小是size,把它们放到buffer的空间里面去

返回值:

返回值也和fwrite类似

例子:我们文件里放的是刚才fwrite写进去的内容

struct S
{
	char name[20];
	int age;
	float grade;
};

int main()
{
	FILE* p = NULL;
	struct S s = { 0 };
	p = fopen("text.txt", "rb");
	if (p == NULL)
	{
		perror("fopen");
		return;
	}

	fread(&s, sizeof(struct S), 1, p);
	printf("%s %d %f", s.name, s.age, s.grade);

	fclose(p);
	p = NULL;
	return 0;
}

我们看结果:
在这里插入图片描述
我们可以看到虽然文件里的内容我们看不懂,但是在读的时候依然可以读出来。

3.5补充

在这里插入图片描述

我们可以发现这几个函数适用于所有输入流\输出流。
通过上面对这些函数的分析,我们知道它们可以作用于文件,但是除了文件,还可以操作的对象是什么呢?

C语言在运行的时候会默认打开三个流:
标准输入流->键盘—(stdin)
标准输出流->屏幕—(stdout)
标准错误流->屏幕—(stderr)
这三个流的类型也是FILE*类型的。

看到这里,我们是不是可以从键盘上输入信息,然后从屏幕上读取我们刚输入的信息?

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

我们看结果:
在这里插入图片描述

这里我们就是从键盘这个“文件”里获得信息,把信息写入屏幕这个“文件”里。

在这里插入图片描述
而这两个函数只适用于文件。

3.6sscanf函数和sprintf函数

我们再来看两组函数

3.6.1sprintf函数

将格式化的数据写入字符串。

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

参数

输出到的位置
const char *format [, argument] … :和printf函数一样,输入格式化的内容。

sprintf函数格式化并在缓冲区中存储一系列字符和值,在最后一个写入的字符之后追加一个空字符。
就是将const char *format [, argument] …输入的内容,转化成字符串然后放到buffer的位置上。并在最后一个转化的字符上加个\0.

返回值

返回缓冲区中存储的字符个数,不计算结束的空字符
如果将sprintf函数作为参数输出,看看输出多少个数
在这里插入图片描述
这里我数过了从z到最后一个0,总共21个字符。

例子

struct S
{
	char name[20];
	int age;
	float grade;
};

int main()
{
	char str[40];
	struct S s = { {"zhangsan"},{20},{75.5f} };

	sprintf(str, "%s %d %f", s.name, s.age, s.grade);

	printf("%s\n", str);
}

这里我们就是将结构体里面的内容,转化成一个字符串放到数组str里面去。我们看结果:
在这里插入图片描述

3.6.2sscanf函数

从字符串中读取格式化的数据。
我们既然可以将一个格式化数据转换成一个字符串,肯定也能将一个字符串转换成一个格式化数据

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

参数

const char *buffer:需要转换的字符串的地址

返回值

每个函数都返回成功转换和分配的字段的数量;返回值不包括已读取但未赋值的字段。返回值为0表示没有分配字段。如果出现错误,或者在第一次转换之前到达字符串的末尾,则返回值为EOF。

例子

struct S
{
	char name[20];
	int age;
	float grade;
};

int main()
{
	char str[40] = { 0 };
	struct S s = { {"zhangsan"},{20},{75.5f} };
	struct S tmp = { 0 };
	sprintf(str, "%s %d %f", s.name, s.age, s.grade);
	//我们先将s结构体里的内容转换成字符串放到数组str里

	printf("%d\n", sscanf(str, "%s %d %f", tmp.name, &(tmp.age), &(tmp.grade)));
	//将字符串str里的内容按照%s %d %f的格式打印到tmp的name,age,grade里面
	//我们顺便把sscanf的返回值打印出来

	printf("%s %d %f", tmp.name, tmp.age, tmp.grade);
}

我们看结果:
在这里插入图片描述
我们成功转换成了3段,所有返回值是3.

四.文件的读写(随机读写)

我们之前看到的都是顺序读写,就是从文件开始的那个地方开始,假如文件里的内容是abcdef,fgetc就是得到的第一个字符a,但是我想第一次直接读到d可不可以呢?

4.1fseek函数

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

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

移动的文件指针不要和FILE* stream弄混了,这个是只文件指针的偏移量,假如我们读到了文件里的一个字符,这个文件指针会自动向后偏移一个位置。

参数

FILE *stream:需要操作的文件的地址
long offset:文件指针偏移的个数
int origin:
在这里插入图片描述
SEEK_SET就是从文件的开始位置计算偏移量
SEEK_CUR从当前文件指针的位置开始计算偏移量
SEEK_END从文件的末尾开始计算偏移量

返回值

如果成功,函数返回0。否则,它返回非零值。

例子:文件里的内容是“abcdef”

int main()
{
	//abcdef
	FILE* p = NULL;
	p = fopen("text.txt", "r");
	char ch = 0;

	fseek(p, 3, SEEK_SET);
	//将文件指针指向开始的位置,然后向后偏移3个位置指向'd'
	ch = fgetc(p);
	//因为文件指针指向的是'd'所以得到的就是字符d
	printf("%c\n", ch);

	fseek(p, -3, SEEK_END);
	//将文件指针移动到末尾,偏移量为-3就是向前挪动三位
	//指向的也是'd'
	ch = fgetc(p);
	printf("%c\n", ch);

	fseek(p, 1, SEEK_CUR);
	//这里我们上面那个代码已经读到了字符'd',文件指针会自动向后移动一位
	//此时我们再通过函数fseek,将指针在当前位置向后偏移一位
	//此时指向的就是'f'的位置
	ch = fgetc(p);
	printf("%c\n", ch);

	fclose(p);
	p = NULL;
	return 0;
}

我们了来看结果:
在这里插入图片描述

4.2ftell函数

返回文件指针相对于起始位置的偏移量。
可以让我们知道此时文件指针的偏移量是多少

long int ftell ( FILE * stream );

参数:

指向这个文件的指针

返回值:

如果成功,则返回位置指示器的当前值。
如果失败,将返回-1L。

例子

int main()
{
	//abcdef
	FILE* p = NULL;
	p = fopen("text.txt", "r");
	char ch = 0;

	fseek(p, 3, SEEK_SET);
	printf("%ld", ftell(p));
	//我们用fseek函数已经把偏移量设置成从开始向后的3位

	fclose(p);
    p = NULL;
	return 0;
}

我们看结果:
在这里插入图片描述

4.3rewind函数

让文件指针的位置回到文件的起始位置

void rewind ( FILE * stream );

参数:

指向文件的指针

例子

int main()
{
	//abcdef
	FILE* p = NULL;
	p = fopen("text.txt", "r");
	char ch = 0;

	fseek(p, 3, SEEK_SET);
	//我们用fseek函数先把偏移量设置成从开始向后的3位
	printf("%ld\n", ftell(p));
	//我们看此时的位置

	rewind(p);
	//在设置成起始位置
	printf("%ld", ftell(p));

	fclose(p);
	p = NULL;
	return 0;
}

我们看结果:
在这里插入图片描述

五.文本文件和二进制文件

根据数据的组织形式,数据文件被称为文本文件或者二进制文件。数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件。如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件

我们之前用二进制写的时候,再用文本文件打开,发现看到的是乱码,这就说明文本文件它读不了二进制文件,但是你如果用能读得了二进制文件的东西去读,也可以读出来

int main()
{
	//abcdef
	FILE* p = NULL;
	p = fopen("text.txt", "wb");
	char ch = 0;

	int a = 10000;
	fwrite(&a, 4, 1, p);

	fclose(p);
	p = NULL;
	return 0;
}

我们用二进制写个10000进去,看文件里的内容
在这里插入图片描述
我们确实是乱码,我们知道10000在内存中是以二进制存放的,所有如果以二进制文本打开应该是:

00000000 00000000 00100111 00010000
00 00 27 10

现在我们用一个能读懂二进制的来读一下看看是不是的
在这里插入图片描述
我们将text.txt添加进来,右键点击打开方式,然后点击二进制编辑器
在这里插入图片描述
前面的0不用管,我们看后面的,因为我这个编译器是小端字节序存储。这样看和我们之前算的00 00 27 10是不是就一样了。这就说明虽然用文本文件打开看的是乱码,它实际的数据其实是完整的,只是我们看不懂。

六.文件读取结束的判定

我先来总结一下之前用到的函数的返回值

6.1函数的返回

1.fgetc函数

如果成功,则返回已读取的字符。如果位置指示符位于文件末尾(就是没有东西来返回了)或者发生其他错误,返回EOF

2.fgets函数

如果成功,函数返回str,就是这个字符串的地址。如果错误或者结束,返回一个空指针

3.fscanf函数

成功时,函数返回成功填充的参数列表的项数。如果返回失败,返回的值是小于这个元素个数的值

4.fread函数

返回成功读取的元素总数。如果size或count为零,则函数返回零.返回的值是小于这个元素个数的值

6.2feof函数

我们刚总结了一下文件读写用到的函数,和其他函数的返回值。我们可能会发现,有些函数在遇到读取结束和读取错误的时候返回值是一样的。
所以我们有feof这个函数来判断是读取失败还是读取结束

int feof ( FILE * stream );

参数

指向文件的指针

返回值

如果设置了与流相关的文件结束指示符,则返回一个非零值。否则,如果是因为读取失败,返回0。

我们一般是通过输出函数的返回值来判断是否读取结束,然后用feof函数来判断,是因为读取失败,还是因为读取结束。
切记,不能直接用feof函数来判断是否读取结束

七.缓冲区

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

看到这里可能会有人想,如果你一次装不满这个缓冲区,难道还没办法传输了?当然不是,如果你写完了这个程序,在关闭文件时。或者在遇到\n时。再或者强制刷新fflush(stdout)时,都会将缓冲区当前的值传输过去。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值