文章目录
一、概述
1. 磁盘文件和设备文件
1.1 磁盘文件
指一组相关数据的有序集合,通常存储在外部介质(如磁盘)上,使用时才调入内存。简单点说就是将数据存放到外部介质上,比如磁盘,U盘啊等等,要使用的时候再导进来。
1.2 设备文件
在操作系统中把每一个与主机相连的输入、输出设备看作是一个文件,把它们的输入、输出等同于对磁盘文件的读和写。
2. 磁盘文件分类
计算机的存储在物理上是二进制的,所以物理上所有的磁盘文件本质上都是一样的:以字节为单位进行顺序存储。
从用户或者操作系统使用的角度(逻辑上)把文件分为:文本文件和二进制文件。
(2)二进制文件:基于值编码的文件,也就是说,如果要存放一个数字,那么就存放这个数字本身的值,比如说要存 123 ,这是数字的值为 一百二十三,那么就将 123 作为整体化为二进制的一百二十三存放,二进制的一百二十三为:0111 1011
3. 文本文件和二进制文件
3.1 文本文件
基于字符编码,常见编码有ASCII,UNICODE等。一般可以使用文本编辑器直接打开。也就是说文本文件存放的是字符对应的编码值,如果用 ASCII 编码,那么存放的就是每个字符对应的 ASCII 码值。
比如数5678的以ASCII存储形式(ASCII码)为:
00110101 00110110 00110111 00111000。为什么会是这样呢,来分析一下。我的理解是:
(1)文本文件基于字符编码,就是将数据看成字符串,字符串由一个个字符组成。在存储的时候,就将字符串拆分为一个个字符,然后将每个字符对应的 ASCII 码值存放到内存中,又因为计算机只认识0和1,所以 ASCII 码值也要转换为二进制的形式。
(2)拿上面的例子 5678 来说明,如果以文本的形式存放,那么就将 5678 看成一个字符串,这个字符串由5,6,7,8 这四个字符组成。这四个字符对应的 ASCII 码值为:
5—》53(ASCII)
6—》54(ASCII)
7—》55(ASCII)
8—》56(ASCII)
(3)计算机只认识0和1,所以要将它们对应的 ASCII 码值转换为二进制:
5—》53(ASCII)—》00110101
6—》54(ASCII)—》00110110
7—》55(ASCII)—》00110111
8—》56(ASCII)—》00111000
(4)所以5678以文本形式存放就是存放它们对应的 ASCII 码值的二进制,也就是:
00110101 00110110 00110111 00111000
(5)可以总结文本文件存放数据的步骤:
1)将数据看成字符串,然后拆分成字符
2)将字符转换 ASCII 码值代替
3)将ASCII 码值转化为二进制代表
3.2 二进制文件
基于值编码,自己根据具体应用,指定某个值是什么意思。把内存中的数据按其在内存中的存储形式原样输出到磁盘上
比如数5678的存储形式(二进制码)为:
00010110 00101110
为什么二进制文件是这存放呢?我的理解是:5678 这个数字对应的值是五千六百七十八,那么就将五千六百七十八化为二进制存储。
5678(五千六百七十八)-——》00010110 00101110。
从这里可以看出来,以二进制文件的方式存储时比较省空间的,但是不利于查看。二进制文件的方式就是直接存放整个数字本身的值。
二、打开文件与关闭文件
在目录一里,主要介绍了文本文件和二进制文件,那么我们如何在程序中打开文件呢?下面就来简单地介绍一下。
1.文件指针
想要知道在程序中如何打开文件,先来了解一下文件指针。如果将文件比作房子,那么文件指针可以简单地比作房卡,房卡也是钥匙,是用来打开和关闭房子的。
在C语言中用一个指针变量指向一个文件,这个指针称为文件指针。文件指针是一种结构体类型变量:
typedef struct
{
short level; //缓冲区"满"或者"空"的程度
unsigned flags; //文件状态标志
char fd; //文件描述符
unsigned char hold; //如无缓冲区不读取字符
short bsize; //缓冲区的大小
unsigned char *buffer;//数据缓冲区的位置
unsigned ar; //指针,当前的指向
unsigned istemp; //临时文件,指示器
short token; //用于有效性的检查
}FILE;
FILE是系统使用typedef定义出来的有关文件信息的一种结构体类型,结构中含有文件名、文件状态和文件当前位置等信息。
声明FILE结构体类型的信息包含在头文件“stdio.h” 中,一般设置一个指向FILE类型变量的指针变量,然后通过它来引用这些FILE类型变量。通过文件指针就可对它所指的文件进行各种操作。
2. 文件的打开和关闭
在程序中想要使用文件,就先要打开文件。就像是想要使用房间,就先打开房间。
2.1 打开文件函数
#include <stdio.h>
FILE * fopen(const char * filename, const char * mode);
(1)功能:打开文件
(2)参数:
filename:需要打开的文件名,根据需要加上路径。这个就像是要打开房间,要知道房间在哪。
mode:打开文件的模式设置,打开文件的模式指的是打开了文件要做什么,是将数据读出来,还是将数据写进去。
(3)返回值:
成功:文件指针
失败:NULL
(4)如何理解:FILE * fp = fopen(const char * filename, const char * mode);
之前在定义普通的变量时,比如说 int a=5;这句话的意思是请给我分配 4 个字节的大小的内存,我用来存放整型数据5,这块内存的名字就叫做a。
那么int a=4,FILE *fp = fopen(const char * filename, const char * mode);这句代码的意思可以理解为:我想要打开一个文件,名字是xxx,将数据写进去或者读出来;请把这个文件的钥匙fp给我,我用来操作文件。
2.2 打开的模式
打开模式 | 含义 |
---|---|
r或rb | 以只读方式打开一个文本文件(不创建文件,若文件不存在则报错)。 读文件,如果文件没有,你不能凭空捏造一个文件出来读。 |
w或wb | 以写方式打开文件(如果文件存在则清空文件,文件不存在则创建一个文件) |
a或ab | a是 append 的简写。以追加方式打开文件,在末尾添加内容,不覆盖(删除)文件之前存在的内容。若文件不存在则创建文件,写东西,如果这个文件不存在,那么你可以自己创建一个。 |
r+或rb+ | 以可读、可写的方式打开文件(不创建新文件) |
w+或wb+ | 以可读、可写的方式打开文件(如果文件存在则清空文件,文件不存在则创建一个文件) |
a+或ab+ | 以添加方式打开可读、可写的文件。若文件不存在则创建文件;如果文件存在,则写入的数据会被加到文件尾后,即文件原先的内容会被保留。 |
2.3 打开文件失败
打开一个文件失败的原因大致有三个:
(1)找不到文件
(2)没有权限
(3)打开的文件数量超出上限
2.4 关闭文件
任何文件在使用后应该关闭:
打开的文件会占用内存资源,如果总是打开不关闭,会消耗很多内存。一个进程同时打开的文件数是有限制的,超过最大同时打开文件数,再次调用fopen打开文件会失败。
如果没有明确的调用fclose关闭打开的文件,那么程序在退出的时候,操作系统会统一关闭。
#include <stdio.h>
int fclose(FILE * stream);
功能:关闭先前fopen()打开的文件。此动作让缓冲区的数据写入文件中,并释放系统所提供的文件资源。
参数:
stream:文件指针
返回值:
成功:0
失败:-1
FILE * fp = NULL;
fp = fopen("abc.txt", "r");
fclose(fp);
三、文件的顺序读写
1.按照字符读写文件fgetc、fputc
1.1 一次写一个字符到文件
#include <stdio.h>
int fputc(int ch, FILE * stream);
f:file 文件
put:放
c:字符
fputc:文件放字符进去,就是每次将一个字符写入文件
功能:将ch转换为unsigned char后写入stream指定的文件中
参数:
ch:需要写入文件的字符
stream:文件指针
返回值:
成功:成功写入文件的字符
失败:返回-1
测试代码:
char buf[] = "this is a test for fputc";
int i = 0;
int n = strlen(buf);
for (i = 0; i < n; i++)
{
//往文件fp写入字符buf[i]
int ch = fputc(buf[i], fp);
printf("ch = %c\n", ch);
}
1.2 文件结尾
在C语言中,EOF表示文件结束符(end of file)。在while循环中以EOF作为文件结束标志,这种以EOF作为文件结束标志的文件,必须是文本文件。在文本文件中,数据都是以字符的ASCII代码值的形式存放。我们知道,ASCII代码值的范围是0~127,不可能出现-1,因此可以用EOF作为文件结束标志。
#define EOF (-1)
当把数据以二进制形式存放到文件中时,就会有-1值的出现,因此不能采用EOF作为二进制文件的结束标志。为解决这一个问题,ANSI C提供一个feof函数,用来判断文件是否结束。feof函数既可用以判断二进制文件又可用以判断文本文件。
#include <stdio.h>
int feof(FILE * stream);
功能:检测是否读取到了文件结尾。判断的是最后一次“读操作的内容”,不是当前位置内容(上一个内容)。
参数:
stream:文件指针
返回值:
非0值:已经到文件结尾
0:没有到文件结尾
1.3 一次从文件读一个字符
#include <stdio.h>
int fgetc(FILE * stream);
功能:从stream指定的文件中读取一个字符
参数:
stream:文件指针
返回值:
成功:返回读取到的字符
失败:-1
测试代码:
char ch;
#if 0
while ((ch = fgetc(fp)) != EOF)
{
printf("%c", ch);
}
printf("\n");
#endif
while (!feof(fp)) //文件没有结束,则执行循环
{
ch = fgetc(fp);
printf("%c", ch);
}
printf("\n");
2.按照行读写文件fgets、fputs
2.1 写文件
#include <stdio.h>
int fputs(const char * str, FILE * stream);
S:string,是字符串的意思
功能:将str所指定的字符串写入到stream指定的文件中,
字符串结束符 '\0' 不写入文件。
参数:
str:字符串
stream:文件指针
返回值:
成功:0
失败:-1
注意事项
(1) fputs() 函数 不会将字符串结束符 ‘\0’ 写入文件,但是会将 ‘\n’ 写入文件,在文件中就会换行。也就是说 fputs() 函数遇到了 ‘\0’ 就会停止本次的写入。
测试:将helloword 用两个等号夹起来," == helloword\0 == ";
#include <stdio.h>
#include <string.h>
int main()
{
// 1.以追加的方式打开一个文件
FILE *fp = fopen("/oracle/heima/CBasics/day10/test.txt","a");
// 2.定义一个字符串
char *str = "==helloword\0==";
char *str1 = "ilike\nyou";
// 3.将字符串写入文件
fputs(str,fp);
fputs(str1,fp);
fclose(fp);
return 0;
}
测试结果:这一部分(\0 ==)并没有写入文件,也就是fputs() 遇到了 ‘\0’ 就停止本次的写入了,进行第二次的写入,将 ilikeyou 写进去。我们还会发现 you 这部分字符串换行了,所以说明了 fputs() 函数可以将换行符写入文件。
(2) fputs() 函数每次写入一个字符串,不会自动加上换行符(\n)。如果没有换行符,那么第二次写入的字符串就会紧接在第一次写入的字符串后面,并不是每次写入一个字符串就作为新的一行。因为写入一个字符串,文件内光标就会移动到那个字符串的最后一个字符的位置。
测试:先写入字符串str,然后在写入字符串str1。
#include <stdio.h>
#include <string.h>
int main()
{
// 1.以追加的方式打开一个文件
FILE *fp = fopen("/oracle/heima/CBasics/day10/test.txt","a");
// 2.定义一个字符串
char *str = "helloword";
char *str1 = "ilikeyou";
// 3.将字符串写入文件
fputs(str,fp);
fputs(str1,fp);
fclose(fp);
return 0;
}
查看结果:查看写入的文件的内容,str1 紧接着 str 后面,并没有另一起一行。
2.2 读文件
#include <stdio.h>
char * fgets(char * str, int size, FILE * stream);
功能:从stream指定的文件内读入字符,保存到str所指定的内存空间,直到出现换行字符、读到文件结尾或是已读了size - 1个字符为止,最后会自动加上字符 '\0' 作为字符串结束。
参数:
str:字符串
size:指定最大读取字符串的长度(size - 1)
stream:文件指针
返回值:
成功:成功读取的字符串
读到文件尾或出错: NULL
注意事项:
fgets() 函数是从文件中读取字符(内容),那么一次读取操作什么时候结束呢 ?
(1)遇到换行符 ‘\n’ 就结束本次读取操作。
测试:
1)先看要读取的文件中的内容:一共有三行,至少前面两行都有换行符,如果进行一次读取操作,那么读取到的将会是第一行,因为有换行符。
2)测试代码:
#include <stdio.h>
#include <string.h>
int main()
{
// 1.以的方式打开一个文件
FILE *fp = fopen("/oracle/heima/CBasics/day10/test.txt","r");
// 2.定义一个字符串
char strBuffer[1024];
memset(strBuffer,0,sizeof(strBuffer));
// 3.将文件数据读取出来放到字符串
fgets(strBuffer,sizeof(strBuffer),fp);
printf("strBuffer= %s\n",strBuffer);
printf("strlen(strBuffer) = %d\n",strlen(strBuffer));
fclose(fp);
return 0;
}
3)测试结果:只读取了一行。
(2)如果遇不到换行符
就一直读取下去,直到文件的末尾或者是读取了 size-1 个字符。
1)测试读取到文件的末尾:先将读取的文件的内容修改,去掉换行符,也就是将三行内容放到同一行。
代码与2.2 中的(2)的测试代码一样。执行代码查看结果:
一次将文件的全部内容读取出来,因为没有遇到换行符,也没有读取到 size-1 个字符。
2)测试读取到了 size-1 个字符的情况:
修改代码,将 size 参数改为5:
// 3.将文件数据读取出来放到字符串
fgets(strBuffer,5,fp);
printf("strBuffer = %s\n",strBuffer);
执行程序,查看结果,读取了4个字符,也就是5-1,同时我们也将读取出来的字符串长度打了出来。
(3)读取结束后会自动加上字符 ‘\0’ 作为字符串结束。
这也是为什么读取 size-1 个字符的原因,要留一个位置给 ‘\0’ 。如果读取出来的字符串没有结束符,那么在操作字符串的时候就会出现意想不到的情况。
(4)fgets() 会将换行符作为字符读取出来
fgets() 会将换行符作为字符读取出来,如果将读取出来的字符串打印,也会将换行符打出来,也就是换行。但是不要错误地认为 fgets() 函数会自动加上换行符。下面就来测试一下:
1)先修改要读取的文件的内容:分成了四行。
2)测试代码:
// 1.以的方式打开一个文件
FILE *fp = fopen("/oracle/heima/CBasics/day10/test.txt","r");
// 2.定义一个字符串
char strBuffer[1024];
char strBuffer_1[1024];
memset(strBuffer,0,sizeof(strBuffer));
memset(strBuffer_1,0,sizeof(strBuffer_1));
// 3.将文件数据读取出来放到字符串
fgets(strBuffer,6,fp);
fgets(strBuffer_1,6,fp);
printf("strBuffer = %s\n",strBuffer);
printf("strBuffer = %s\n",strBuffer_1);
printf("strlen(strBuffer) = %d\n",strlen(strBuffer));
printf("strlen(strBuffer_1) = %d\n",strlen(strBuffer_1));
fclose(fp);
return 0;
}
3)查看结果:设置的 size 参数为5,那么应该是读取4个字符的,如果没有遇到换行符。那么我们看到 strBuffer 的长度是4,但是只显示了 hel 三个字符,还有一个跑哪去了,还有一个是换行符。我们也可以看到打印的两个行之间有空行,就是因为读取的字符串中本身就有一个换行符,然后在打印的时候我们又加上了一个换行符,所以就出现空白行了。
4)那么再测试一次,我们打印的时候不加换行符了,看看会不会换行。修改代码:
printf("strBuffer = %s",strBuffer);
printf("strBuffer = %s",strBuffer_1);
执行查看结果:strBuffer 和 strBuffer_1 依旧会换行。这也说明了会将换行符作为字符读取出来。因为我们在打印的时候没有加换行符,但是它们却换行了,说明字符串中有换行符。
字符串是这样的:“hel\n\0”,"lo\n\0"
3.按照格式化文件fprintf、fscanf
3.1 写文件
这个函数是将字符串以指定的格式写到文件中。
#include <stdio.h>
int fprintf(FILE * stream, const char * format, ...);
功能:根据参数format字符串来转换并格式化数据,然后将结果输出到stream指定的文件中,指定出现字符串结束符 '\0' 为止。
参数:
stream:已经打开的文件
format:字符串格式,用法和printf()一样
返回值:
成功:实际写入文件的字符个数
失败:-1
测试代码:这里的格式就是 a*b=c,a,b,c 是变量。
#include <stdio.h>
#include <string.h>
int main()
{
// 1.以追加的方式打开文件
FILE *fp = fopen("/oracle/heima/CBasics/day10/fprintf.txt","a");
if( fp == NULL )
{
printf("打开文件失败.\n");
return -1;
}
int a,b,c;
a=b=c=0;
int i=10;
while(i--)
{
a = i;
b = i+3;
c = a*b;
fprintf(fp,"%d*%d=%d\n",a,b,c);
}
fclose(fp);
return 0;
}
测试结果:符合 a*b=c 的格式。
3.2 读文件
#include <stdio.h>
int fscanf(FILE * stream, const char * format, ...);
功能:从stream指定的文件读取字符串,并根据参数format字符串来转换并格式化数据。
参数:
stream:已经打开的文件
format:字符串格式,用法和scanf()一样
返回值:
成功:参数数目,成功转换的值的个数
失败: - 1
int a = 0;
int b = 0;
int c = 0;
fscanf(fp, "%d %d %d\n", &a, &b, &c);
printf("a = %d, b = %d, c = %d\n", a, b, c);
4.按照块读写文件fread、fwrite
前面的 fgetc() ,fputc(),fgets(),fputs() 函数,可以简单而又不完全正确(这只是我的理解)地说,这几个函数都是操作文本文件的。在将数据写入文本文件之前,数据类型是多种多样的,但是将数据写入了文本文件,就都变成了字符型或者字符串类型。
或者说我要将一些数据写入文本文件,那必须先转换为字符或者字符串类型。使用 sprintf 字符串格式化函数,可以将不同的数据类型的数据,写到字符串里面。
在前面我们也说过,文件大致分为两种文本文件和二进制文件。有时候我不想将数据转换为文本文件,也就是说数据在计算机内存中是什么样子,我就将这个计算机内存中的数值存到文件里面,而不是转换为ASCII码(字符类型)。或者这样说,我就想存放数字本身的数值,怎么办。
举个前面的例子:
数5678的存储形式(二进制码)为:00010110 00101110
如果以ASCII存储形式(文本文件的形式)为:00110101 00110110 00110111 00111000
现在我想将数5678的值(00010110 00101110),写到文件中,也就是二进制文件;而不是以文本文件的形式(00110101 00110110 00110111 00111000)写到文件中,怎么办。
C语言就推出了下面的函数:
4.1 写文件
这个写文件的函数,也可以这样说:将内存中存放的二进制数值存放到指定的文件中。
#include <stdio.h>
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
功能:以数据块的方式给文件写入内容。
将内存中存放的二进制数存放到指定的文件中。
参数:
ptr:准备写入文件数据的地址,这个地址就是要找到内存,然后取出内存中存放的二进制数。
size: size_t 为 unsigned int类型,此参数指定写入文件内容的块数据大小,单位是字节。我们知道内存是分块的,最小的内存单位就是一个字节,就是说可以一个字节为一块。那么我也可以4个字节为单位,4个字节为一块内存。这个通常可以写你要写的数据类型,或者填1.
nmemb:写入文件的块数,写入文件数据总大小为:size * nmemb。这个就是一次写的操作要写多少块的内存进文件。
stream:已经打开的文件指针
返回值:
成功:实际成功写入文件数据的块数目,此值和 nmemb 相等
失败:0
注意size 和 nmemb 参数
其实size 和 nmemb 参数是共同决定一次写操作要将多少字节的数据,分成多少次写入文件而已。
就比如说有100块砖头,要一天搬完。有的人一筐就装了50块,分两次就搬完了。有的人一筐装20块,要分5次才能搬完。不管怎么搬,他们只要一天之内搬完就行了。
回到 fwrite() 函数,ptr 是数据存放的地址,你找到了数据在哪, 一次搬多少(size),搬多少次( nmemb ),由你决定,最终能搬完就行,总数目对就行。
4.1.1 测试1
typedef struct Stu
{
char name[50];
int id;
}Stu;
Stu s[3];
int i = 0;
for (i = 0; i < 3; i++)
{
sprintf(s[i].name, "stu%d%d%d", i, i, i);
s[i].id = i + 1;
}
int ret = fwrite(s, sizeof(Stu), 3, fp);
printf("ret = %d\n", ret);
4.1.2 测试2
(1)测试代码:
#include <stdio.h>
#include <string.h>
struct stu
{
char name[31];
int age;
};
int main()
{
struct stu s1;
strcpy(s1.name,"伊丽莎白");
s1.age = 19;
// 以写的方式打开文件
FILE *fp = fopen("/oracle/heima/CBasics/day10/fw.txt","w");
fwrite(&s1,sizeof(struct stu),1,fp);
fclose(fp);
return 0;
}
(2)测试结果:用 vi 编辑器打开 fw.txt 文件。
可以看到很多乱码,其实并不是文件的内容乱,而是vi无法识别文件的格式,把内容当成ASCII码显示,如果内容刚好是ASCII码(名字),就能正确显示,如果不是ASCII码(如年龄),就无法正常显示了。
4.2 读文件
这个函数就是,从文件中搬数据出来。搬到指定位置,不管你怎么搬,最后搬完指定的数目就行。搬到了指定的地址之后,别人就可以使用这个数据了。
还有个注意的地方就是,地址一般是连续的。
#include <stdio.h>
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
功能:以数据块的方式从文件中读取内容
参数:
ptr:存放读取出来数据的内存空间
size: size_t 为 unsigned int类型,此参数指定读取文件内容的块数据大小
nmemb:读取文件的块数,读取文件数据总大小为:size * nmemb
stream:已经打开的文件指针
返回值:
成功:实际成功读取到内容的块数,如果此值比nmemb小,但大于0,说明读到文件的结尾。
失败:0
0: 表示读到文件结尾。(feof())
4.2.1 测试1
typedef struct Stu
{
char name[50];
int id;
}Stu;
Stu s[3];
int ret = fread(s, sizeof(Stu), 3, fp);
printf("ret = %d\n", ret);
int i = 0;
for (i = 0; i < 3; i++)
{
printf("s = %s, %d\n", s[i].name, s[i].id);
}
4.2.2 测试2
struct stu
{
char name[31];
int age;
};
int main()
{
struct stu s1;
// 以读的方式打开文件
FILE *fp = fopen("/oracle/heima/CBasics/day10/fw.txt","r");
// 将数据从文件中读出来
fread(&s1,sizeof(struct stu),1,fp);
printf("s1.name=%s,s1.age=%d\n",s1.name,s1.age);
return 0;
}
测试结果:
4.3 注意读写数据格式一致
要注意我们将什么样格式的数据写进二进制文件,那么读取出来时,为了减少不必要的麻烦,在将数据读取出来时,就应用什么格式的数据接收。
比如4.1 是将下面这样格式的数据写入文件
typedef struct Stu
{
char name[50];
int id;
}Stu;
那么 4.2 将数据读取了出来,就用这样格式的数据接收。
typedef struct Stu
{
char name[50];
int id;
}Stu;
5. 再次测试 fputs 和 fread 函数
先来回顾一下 fputs 函数,可以看到这个函数的第一个参数是将要写入文件的字符串的地址。从第一个参数来看,这个函数一般是用来将字符串写入文件的。现在来重新想一下,为什么叫做文本文件?因为写入文件的一般是字符串或者字符,字符类型的数据,本质上就是ASCII码,在内存中存放的就是ASCII码,所以文件里面本质上是 ASCII 码(如果采用ASCII 编码)
int fputs(const char * str, FILE * stream);
fputs() 函数要从参数1传进来的地址,去找到那片内存空间,然后将内存中的数写进文件里。其实 fputs() 函数并不知道内存中的数是ASCII码还是数值本身的二进制数,它只需要从内存中拿出来,然后写到文件里面就行了。也就是说,fputs() 只是负责将数据从内存中取出来,然后放到文件里,它不知道是文本文件还是二进制文件,它只是个搬运工。文本文件和二进制文件只是我们人类给他们取的名字,在计算机眼里只有1和0。
根据上面的说法,fputs() 函数只是负责搬运,那么我将其他类型的数据写入文件也是可以的,使用fputs() 函数,不只是字符类型的数据。
那么用这个 fputs() 来将其他类型的数据写入文件有什么区别呢?还是回到这个函数的第一个参数:char * str,这是字符型的指针,指针移动每次移动一个字节,也就是说在一次写操作里面,每次写到缓冲区中的数据是一个字节。我们知道,如果是 int * ,整型的指针,指针每次移动4个字节。
但是对于 fputs() 来说无论是什么类型的指针,都当做 char * 字符型的指针来处理,在写数据时每次就移动一个字节,那么最后也是可以将数据写进去的。下面就来测试一下:
5.1 fputs 写字符类型外的数据进文件
(1)测试代码:
struct stu
{
char name[31];
int age;
};
int main()
{
// 1.以追加的方式打开文件
FILE *fp = fopen("/oracle/heima/CBasics/day10/e.txt","a");
if( fp == NULL )
{
printf("打开文件失败。\n");
return -1;
}
// 2.测试用其他类型的数据指针传进去
int a=1234567;
int *p_a = &a;
if( fputs(p_a,fp) == -1 )
{
printf("写入文件失败。\n");
}
double b = 3.14;
double *p_b = &b;
if( fputs(p_b,fp) == -1 )
{
printf("写入文件失败。\n");
}
float c = 6.111;
float *p_c = &c;
if( fputs(p_c,fp) == -1 )
{
printf("写入文件失败。\n");
}
// 结构体类型
struct stu s;
strcpy(s.name,"伊利斯");
s.age = 19;
struct str *p_s = &s;
if( fputs(p_s,fp) == -1 )
{
printf("写入文件失败。\n");
}
fclose(fp);
return 0;
}
(2)执行程序,就会有警告,但不是错误,就是因为我们传的指针不是字符类型的指针。
(3)查看文件中的内容,测试打开的文件是e.txt,是乱码。因为 vi 编辑器将写进去的二进制数当成ASCII码来显示了。在前面的内容说过:可以看到很多乱码,其实并不是文件的内容乱,而是vi无法识别文件的格式,把内容当成ASCII码显示,如果内容刚好是ASCII码,就能正确显示,如果不是ASCII码(如年龄和身高是整数),就无法正常显示了。
5.2 将文件的内容读出来
在 5.1 中,我们使用 fputs() 函数将其他类型的数据写进去了,但文件里面并不是 ASCII 码值。现在我们来将文件里面的数据读取出来,那么我们应该用什么函数去读,fgets() 还是 fread() ?
先来看看这两个函数的声明:
char * fgets(char * str, int size, FILE * stream);
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
会发现这两个函数的第一个参数都是指针,不过一个要求传字符型指针,一个是泛型指针。我们知道 fread() 函数肯定可以将数据读取出来,一是前面测试过,可以将多种数据读取出来,二是这文件里面的内容本身就是二进制数,也就是二进制文件。下面就来测试一下:
(1)fgets() 读e.txt文件
测试代码:
struct stu
{
char name[31];
int age;
};
int main()
{
// 1.以读的方式打开文件
FILE *fp = fopen("/oracle/heima/CBasics/day10/e.txt","r");
if( fp == NULL )
{
printf("打开文件失败。\n");
return -1;
}
// 2.将数据读取出来
int a=0;
int *p_a = &a;
if( fgets(p_a,sizeof(int),fp) == -1 )
{
printf("读文件失败。\n");
}
printf("a=%d\n",a);
double b = 0;
double *p_b = &b;
if( fgets(p_b,sizeof(double),fp) == -1 )
{
printf("读文件失败。\n");
}
printf("b=%d\n",b);
float c = 0;
float *p_c = &c;
if( fgets(p_c,sizeof(float),fp) == -1 )
{
printf("读文件失败。\n");
}
printf("c=%d\n",c);
// 结构体类型
struct stu s;
memset(&s,0,sizeof(struct stu));
struct stu *p_s = &s;
if( fgets(p_s,sizeof(struct stu),fp) == -1 )
{
printf("读文件失败。\n");
}
printf("s.name=%s,s.age=%d\n",s.name,s.age);
fclose(fp);
return 0;
}
测试结果:
写的代码有问题,fgets() 读取数据的时候,是读取 size-1 个字符,在代码中至少要size+1;还有fgets() 会在后面补一个结束符 ‘\0’ 。所以说我们写进去的内容,用 fgets() 的话,会被加上结束符 ‘\0’ 导致数据出错。所以说还是不要混着用了,容易出错,不好控制。
(2)fread() 读文件
测试代码:
struct stu
{
char name[31];
int age;
};
int main()
{
// 1.以读的方式打开文件
FILE *fp = fopen("/oracle/heima/CBasics/day10/e.txt","r");
if( fp == NULL )
{
printf("打开文件失败。\n");
return -1;
}
// 2.将数据读取出来
int a=0;
int *p_a = &a;
if( fread(p_a,sizeof(int),1,fp) == -1 )
{
printf("读文件失败。\n");
}
printf("a=%d\n",a);
double b = 0;
double *p_b = &b;
if( fread(p_b,sizeof(double),1,fp) == -1 )
{
printf("读文件失败。\n");
}
printf("b=%d\n",b);
float c = 0;
float *p_c = &c;
if( fread(p_c,sizeof(float),1,fp) == -1 )
{
printf("读文件失败。\n");
}
printf("c=%d\n",c);
// 结构体类型
struct stu s;
memset(&s,0,sizeof(struct stu));
struct stu *p_s = &s;
if( fread(p_s,sizeof(struct stu),1,fp) == -1 )
{
printf("读文件失败。\n");
}
printf("s.name=%s,s.age=%d\n",s.name,s.age);
fclose(fp);
return 0;
}
测试结果:
(3)结果分析
哎呦,我去,竟然都读失败了。为啥呢,想法错了吗,将这几个函数混合用,有空再来分析吧。看来还是不要混合用了,容易出错,读写文本文件尽量用 fputs() 和 fgets();读二进制文件就尽量用 fwrite() 和 fread();不要混合着用了,容易出错在写程序的时候。
四、文件的随机读写
1.移动文件光标的读写位置 fseek()
#include <stdio.h>
int fseek(FILE *stream, long offset, int whence);
功能:移动文件流(文件光标)的读写位置。
参数:
stream:已经打开的文件指针
offset:根据whence来移动的位移数(偏移量),可以是正数,也可以负数,如果正数,则相对于whence往右移动,如果是负数,则相对于whence往左移动。如果向前移动的字节数超过了文件开头则出错返回,如果向后移动的字节数超过了文件末尾,再次写入时将增大文件尺寸。
whence:其取值如下:
SEEK_SET:从文件开头移动offset个字节
SEEK_CUR:从当前位置移动offset个字节
SEEK_END:从文件末尾移动offset个字节
返回值:
成功:0
失败:-1
2.获取文件光标的读写位置 ftell()
#include <stdio.h>
long ftell(FILE *stream);
功能:获取文件流(文件光标)的读写位置。
参数:
stream:已经打开的文件指针
返回值:
成功:当前文件流(文件光标)的读写位置
失败:-1
#include <stdio.h>
void rewind(FILE *stream);
功能:把文件流(文件光标)的读写位置移动到文件开头。
参数:
stream:已经打开的文件指针
返回值:
无返回值
3.测试
typedef struct Stu
{
char name[50];
int id;
}Stu;
//假如已经往文件写入3个结构体
//fwrite(s, sizeof(Stu), 3, fp);
Stu s[3];
Stu tmp;
int ret = 0;
//文件光标读写位置从开头往右移动2个结构体的位置
fseek(fp, 2 * sizeof(Stu), SEEK_SET);
//读第3个结构体
ret = fread(&tmp, sizeof(Stu), 1, fp);
if (ret == 1)
{
printf("[tmp]%s, %d\n", tmp.name, tmp.id);
}
//把文件光标移动到文件开头
//fseek(fp, 0, SEEK_SET);
rewind(fp);
ret = fread(s, sizeof(Stu), 3, fp);
printf("ret = %d\n", ret);
int i = 0;
for (i = 0; i < 3; i++)
{
printf("s === %s, %d\n", s[i].name, s[i].id);
}
五、Windows和Linux文本文件区别
b是二进制模式的意思,b只是在Windows有效,在Linux用r和rb的结果是一样的。
Unix和Linux下所有的文本文件行都是\n结尾,而Windows所有的文本文件行都是\r\n结尾
在Windows平台下,以“文本”方式打开文件,不加b:
当读取文件的时候,系统会将所有的 “\r\n” 转换成 “\n”
当写入文件的时候,系统会将 “\n” 转换成 “\r\n” 写入
以"二进制"方式打开文件,则读\写都不会进行这样的转换
在Unix/Linux平台下,“文本”与“二进制”模式没有区别,"\r\n" 作为两个字符原样输入输出。
测试:判断文本文件是Linux格式还是Windows格式
#include<stdio.h>
int main(int argc, char **args)
{
if (argc < 2)
return 0;
FILE *p = fopen(args[1], "rb");
if (!p)
return 0;
char a[1024] = { 0 };
fgets(a, sizeof(a), p);
int len = 0;
while (a[len])
{
if (a[len] == '\n')
{
if (a[len - 1] == '\r')
{
printf("windows file\n");
}
else
{
printf("linux file\n");
}
}
len++;
}
fclose(p);
return 0;
}
六、获取文件状态
#include <sys/types.h>
#include <sys/stat.h>
int stat(const char *path, struct stat *buf);
功能:获取文件状态信息
参数:
path:文件名
buf:保存文件信息的结构体
返回值:
成功:0
失败-1
struct stat {
dev_t st_dev; //文件的设备编号
ino_t st_ino; //节点
mode_t st_mode; //文件的类型和存取的权限
nlink_t st_nlink; //连到该文件的硬连接数目,刚建立的文件值为1
uid_t st_uid; //用户ID
gid_t st_gid; //组ID
dev_t st_rdev; //(设备类型)若此文件为设备文件,则为其设备编号
off_t st_size; //文件字节数(文件大小)
unsigned long st_blksize; //块大小(文件系统的I/O 缓冲区大小)
unsigned long st_blocks; //块数
time_t st_atime; //最后一次访问时间
time_t st_mtime; //最后一次修改时间
time_t st_ctime; //最后一次改变时间(指属性)
};
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
int main(int argc, char **args)
{
if (argc < 2)
return 0;
struct stat st = { 0 };
stat(args[1], &st);
int size = st.st_size;//得到结构体中的成员变量
printf("%d\n", size);
return 0;
}
七、删除文件、重命名文件名
#include <stdio.h>
int remove(const char *pathname);
功能:删除文件
参数:
pathname:文件名
返回值:
成功:0
失败:-1
#include <stdio.h>
int rename(const char *oldpath, const char *newpath);
功能:把oldpath的文件名改为newpath
参数:
oldpath:旧文件名
newpath:新文件名
返回值:
成功:0
失败: - 1
八、文件缓冲区
1. 文件缓冲区概述
ANSI C标准采用“缓冲文件系统”处理数据文件。
所谓缓冲文件系统是指系统自动地在内存区为程序中每一个正在使用的文件开辟一个文件缓冲区从内存向磁盘输出数据必须先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘去。
如果从磁盘向计算机读入数据,则一次从磁盘文件将一批数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(给程序变量) 。
2.磁盘文件的存取
磁盘文件,一般保存在硬盘、U盘等掉电不丢失的磁盘设备中,在需要时调入内存
在内存中对文件进行编辑处理后,保存到磁盘中
程序与磁盘之间交互,不是立即完成,系统或程序可根据需要设置缓冲区,以提高存取效率
3.更新缓冲区
#include <stdio.h>
int fflush(FILE *stream);
功能:更新缓冲区,让缓冲区的数据立马写到文件中。
参数:
stream:文件指针
返回值:
成功:0
失败:-1