1.缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。
2.每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。3.这些信息是保存在一个结构体变量中的。该结构体类型是由系统
声明的,取名FILE.
1.不同的C编译器的FILE类型包含的内容不完全相同,但是大同小异。
2.每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息,使用者不必关心细节。
3.一般都是通过一个FILE的指针来维护这个FILE结构的变量,这样使用起来更加方便。
下面我们可以创建一个FILE* 的指针变量:
FILE* pf;//文件指针变量
4.定义pf是一个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区(是一个结构体变量)。
5.通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够找到与它关联的文件。
流:一个抽象的概念
引入流的原因:
因为计算机可以跟许许多多各种各样的外部设备进行交互,不断地输入与输出信息
所以程序员在写程序的时候就需要针对不同外部设备学习输入与输出的方式
为了简化程序员写代码的难度和过程,引入了流这样的概念
程序员只需要向流中输入数据,从流中读取数据
而无需关心流如何与外部设备之间的信息交互,比如信息如何到文件中去,如何到屏幕上面等等这些问题
这些问题是由C语言和操作系统共同完成的
而C语言程序,只要运行起来,默认就打开3个流
1.标准输入流 stdin FILE*
2.标准输出流 stdout FILE*
3.标准错误流 stderr FILE*
文件在读写之前应该先打开文件,在使用结束之后应该关闭文件。
在编写程序的时候,在打开文件的同时,都会返回一个FILE*的指针变量指向该文件,也相当于建立了指针和文件的关系。
打开文件
FILE* fopen(const char* filename, const char* mode);
关闭文件
int fclose(FILE* stream);
fopen:
1.
如果文件成功打开,该函数将返回指向 FILE 对象的指针,该对象可用于在将来的操作中标识流。
2.
否则,将返回空指针。
3.
在大多数库实现中,errno 变量在失败时也设置为特定于系统的错误代码。
fclose:
1.
如果流成功关闭,则返回零值。
失败时,将返回 EOF。
2.fclose具有刷新缓冲区的功能
3.在VS2022的<stdio.h>头文件中,EOF是这么被定义的,即宏定义为-1
#define EOF (-1)
文件使用方式 含义 如果指定文件不存在
“r”(只读) 为了输入数据,打开一个已经存在的文本文件 出错
“w”(只写) 为了输出数据,打开一个文本文件 建立一个新的文件
“a”(追加) 向文本文件尾添加数据 建立一个新的文件
“rb”(只读) 为了输入数据,打开一个二进制文件 出错
“wb”(只写) 为了输出数据,打开一个二进制文件 建立一个新的文件
“ab”(追加) 向一个二进制文件尾添加数据 建立一个新的文件
“r + ”(读写) 为了读和写,打开一个文本文件 出错
“w + ”(读写) 为了读和写,建立一个新的文件 建立一个新的文件
“a + ”(读写) 打开一个文件,在文件尾进行读写 建立一个新的文件
“rb + ”(读写) 为了读和写打开一个二进制文件 出错
“wb + ”(读写) 为了读和写,新建一个新的二进制文件 建立一个新的文件
“ab + ”(读写) 打开一个二进制文件,在文件尾进行读和写 建立一个新的文件
int main()
{
FILE* pf = fopen("data.txt", "r");
//打开同级文件路径下的data.txt文件
if (pf == NULL)//对有无文件进行判断
{
perror("fopen");//打印错误信息
return 1;
}
//关文件
fclose(pf);
pf = NULL;
return 0;
}
顺序读写:每次读写完一次之后文件指针就会后移
使用者无法指定文件指针的位置
功能 函数名 适用于
字符输入函数 fgetc 所有输入流 字符
字符输出函数 fputc 所有输出流 字符
文本行输入函数 fgets 所有输入流 字符串
文本行输出函数 fputs 所有输出流 字符串
格式化输入函数 fscanf 所有输入流 格式
格式化输出函数 fprintf 所有输出流 格式
二进制输入 fread 文件
二进制输出 fwrite 文件
两者被称为字符输入/输出函数.
仅能适用于字符型数据,
不过可以适用于所有输入流(文件,键盘等等),或者是可以适用于所有输出流(文件,屏幕打印等等)
fgetc
int fgetc(FILE * stream);
功能总述:从流中获取一个字符
1.
返回文件指针所读取到的字符(强转为int类型)
2.
然后文件指针向后偏移至下一个要读取的数据处
3.
当文件读取结束后,该函数返回EOF
下面我们看一下实例
我们提前就已经创建好了data.txt这个文件了
int main()
{
FILE* pf = fopen("data.txt", "r");//注意我们在这里要进行读取的操作,所以以"r"的方式打开
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读文件
int ch = 0;
while ((ch = fgetc(pf)) != EOF)
{
printf("%c\n", ch);
}
//关文件
fclose(pf);
pf = NULL;
return 0;
}
这是我自己提前录入到data.txt中的信息
fputc
int fputc(int character, FILE * stream);
功能总述:向流中写入一个数据
1.
把一个字符写入流中然后移动文件指针
2.
成功后,将返回所写字符。
如果发生写入错误,则返回 EOF并设置错误指示器(ferror)。
一开始,我们的data.txt中依然是存放的abcdefghi,然后我们进行写入操作
注意,写入操作会先清空原有数据,然后再进行写入!!!
int main()
{
FILE* pf = fopen("data.txt", "w");//写文件用"w"打开方式
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写文件
int i = 0;
for (i = 0; i < 26; i++)
{
fputc('a' + i, pf);//abcdefghijklmnopqrstuvwxyz
//打印到屏幕上
//fputc('a' + i, stdout);
//fputc('a' + i, stderr);
}
//关文件
fclose(pf);
pf = NULL;
return 0;
}
两者被称为文本行输入/输出函数.
1.仅适用于字符串类型的数据
2.不过可以适用于所有输入流(文件,键盘等等),或者是可以适用于所有输出流(文件,屏幕打印等等)3.注意:fputs可以一次写一行数据到流中,但是不会自动换行,如果想要换行,则需要自行添加’\n’换行符
fgets
char* fgets(char* str, int num, FILE * stream);
功能总述:从流中获取一行字符串
1.
把从流中读取到的num-1个字符存入str这个字符串中
1.1.读够num-1个后不读了
1.2.读到'\n'后不读了
2.
换行符使 fgets 停止读取,但它被函数视为有效字符,并包含在复制到 str 的字符串中。
返回值
1.成功后,函数返回 str。
2.如果在尝试读取字符时遇到文件末尾,则设置 eof 指示器 (feof)。
3.如果在读取任何字符之前发生这种情况,则返回的指针为空指针(str 的内容保持不变)。
此时文件已经提前被改回为abcdefghi
int main()
{
FILE* pf = fopen("data.txt", "r");
if (NULL == pf)
{
perror("fopen");
return 1;
}
//读文件-读一行
char arr[10] = { 0 };
fgets(arr, 10, pf);
printf("%s\n", arr);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
int fputs(const char* str, FILE * stream);
1.功能总述:向流当中写入一行字符串
2.
从str中读取数据写到流中
3.
该函数从指定的地址 (str) 开始复制,直到到达终止空字符 ('\0')。
此终止空字符不会复制到流中。
int main()
{
FILE* pf = fopen("data.txt", "w");
if (NULL == pf)
{
perror("fopen");
return 1;
}
//写文件,写一行
//fputs只负责写入那一行数据,不会给你换行,要是想换行,自己加'\n'
fputs("hello world", pf);
fputs("hello friend", pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
两者被称为格式化输入/输出函数.
1.可以针对于格式化的数据,例如:结构体
2.可以适用于所有输入流(文件,键盘等等),或者是可以适用于所有输出流(文件,屏幕打印等等)
fprintf
int fprintf ( FILE * stream, const char * format, ... );
1.功能总述:将格式化数据写入流
2.将按格式指向流的 C 字符串写入流。如果 format 包含格式占位符(以 % 开头的子序列),则格式后面的其他参数将被格式化并插入到生成的字符串中,替换其各自的占位符。
3.在 format 参数之后,函数至少需要与格式指定的一样多的其他参数。
例如:
struct S s1 = { 100,3.14f };
fprintf(pf, "%d %f\n", s1.a, s1.s);
在这里为了演示fprintf与fscanf对于格式化数据的处理,定义了一个结构体S
struct S
{
int a;
float s;
};
int main()
{
FILE* pf = fopen("data.txt", "w");
if (NULL == pf)
{
perror("fopen");
return 1;
}
//写文件
struct S s1 = { 100,3.14f };
fprintf(pf, "%d %f\n", s1.a, s1.s);
struct S s2 = { 100,3.14f };
fprintf(pf, "%d %f", s2.a, s2.s);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
fprintf跟fputs相似,也不会自动进行换行,必须自行加上换行符’\n’后才会换行
fscanf
int fscanf ( FILE * stream, const char * format, ... );
1.功能总述:从流中读取格式化数据写入对应的格式型数据类型的变量中
例子:
struct S s = {0};
fscanf(pf, "%d %f", &(s.a), &(s.s));
int main()
{
FILE* pf = fopen("data.txt", "r");
if (NULL == pf)
{
perror("fopen");
return 1;
}
//读文件
struct S s = {0};
fscanf(pf, "%d %f", &(s.a), &(s.s));
printf("%d %f", s.a, s.s);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
int sprintf ( char * str, const char * format, ... );
功能总述:
将格式化数据读取后存放到str所指向的区域中,如果有'\n'也会加入其中
例如:
sprintf(arr, "%d %f %s\n", s.a, s.s, s.q);
将从结构体变量s中读取到的对应的格式型数据存放到数组arr中
这里修改了一下结构体S
struct S
{
int a;
float s;
char q[10];
};
int main()
{
char arr[30] = { 0 };
struct S s = { 100,3.14f,"hello" };
sprintf(arr, "%d %f %s\n", s.a, s.s, s.q);
//将格式化数据读取后存放到arr数组中,如果有'\n'也会读入其中
printf("%s", arr);
printf("you can see '\\n'");//用\把'\n'中的\n转义了一下
}
int sscanf ( const char * s, const char * format, ...);\
功能总述:
1.从字符串中读取格式化数据
2.从 s 读取数据并根据参数格式将它们存储到附加参数给出的位置,就像使用 scanf 一样,但从 s 而不是标准输入 (stdin) 读取。
例如:
struct S tmp = { 0 };
sscanf(arr, "%d %f %s", &(tmp.a), &(tmp.s), tmp.q);
将从arr数组中读取到的格式化数据写入结构体变量tmp中
int main()
{
char arr[30] = { 0 };
struct S s = { 100,3.14f,"hello" };
sprintf(arr, "%d %f %s", s.a, s.s, s.q);
//将格式化数据读取后存放到arr数组中,如果有'\n'也会读入其中
struct S tmp = { 0 };
sscanf(arr, "%d %f %s", &(tmp.a), &(tmp.s), tmp.q);
//将从arr数组中读取到的格式化数据写入结构体变量tmp中
printf("%d %f %s", tmp.a, tmp.s, tmp.q);
}
两者被称为二进制输入/输出函数.
将读取到的数据以二进制的方式写入流中
针对的对象是文件
size_t fwrite(const void* ptr, size_t size, size_t count, FILE* stream);
功能总述:
从ptr指向的内存空间中读取count个大小为size的数据存放到stream流中
int main()
{
FILE* pf = fopen("data.txt", "wb");//二进制的写要加上'b'
if (NULL == pf)
{
perror("fopen");
return 1;
}
struct S s = { 4,2.5f,"hello"};
//写文件
fwrite(&s, sizeof(struct S), 1, pf);
//从s指向的内存空间中读取1个大小为sizeof(struct S)的数据存放到pf指向的流中
fclose(pf);
pf = NULL;
return 0;
}
因为写入的是二进制数据,所以以普通的文本文件打开方式打开会出现下面这种情况
也就是出现一堆乱码
而我们在VS中用二进制编辑器打开后就能很好地查看其中的数据了
我这台电脑是小端存储方式,关于大小端的问题,大家可以看我的另一篇博客:
C语言数据存储深度剖析
二进制读文件
读二进制文件存放到其他空间(ptr指向的空间)中
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
功能总述:
1.从流中读取count个大小为size的数据存放到ptr指向的空间中
2.这里用void* 指针类型便于接受任何类型的指针
3.返回实际读到的数据的个数
根据这个来决定还要不要往下继续读取
例如:
fread(&s, sizeof(struct S), 1, pf);
从流中读取count个大小为size的数据存放到s中
int main()
{
FILE* pf = fopen("data.txt", "rb");//二进制的写要加上'b'
if (NULL == pf)
{
perror("fopen");
return 1;
}
//struct S s = { 4,2.5f,"hello"};
//写文件
//fwrite(&s, sizeof(struct S), 1, pf);
//读文件
struct S s = { 0 };
fread(&s, sizeof(struct S), 1, pf);
//从pf指向的流中读取1个大小为sizeof(struct S)的数据存放到s中
printf("%d %f %s\n", s.a, s.s, s.q);
fclose(pf);
pf = NULL;
return 0;
}
scanf / fscanf / sscanf
printf / fprintf / sprintf
scanf:从标准输入流读取格式化的数据
printf:向标准输出流中写格式化的数据
fscanf:适用于所有输入流的格式化输入函数
fprintf:适用于所有输出流的格式化输出函数
sscanf:将格式化的数据,转换成字符串
sprintf:从字符串中读取格式化的数据
fseek
根据文件指针的位置和偏移量来定位文件指针
int fseek(FILE* stream, long int offset, int origin);
ftell
返回文件指针相对于起始位置的偏移量
long int ftell ( FILE * stream );
rewind
void rewind ( FILE * stream );
让文件指针的位置回到文件的起始位置
也可以用fseek函数
int fseek(FILE* stream, 0, SEEK_SET);
origin:用作偏移参考的位置,类似于物理中的参照物
使得我们所要填写得偏移量是以参照物的(起始地址/末尾地址)为原点,偏移offset个单位,offset可正可负
取值类型及含义:
SEEK_SET Beginning of file(文件的起始位置)(第一个数据的起始地址(即左侧))
SEEK_CUR Current position of the file pointer(文件指针目前所在的位置)
SEEK_END End of file(文件的末尾位置)(最后一个数据的末尾地址(即右侧))
例如:
我们目前文件中的数据为01234567
这么设置的好处是假设ftell返回的数值是point
那么就说明pf现在指向了point这个数据的左侧
例如:point==1
pf指向1的左侧
不过:如果指向7的右侧则point为8
起初:
文件指针pf指向0的左侧,即0这个数据的起始地址
我们想让pf改为指向4的左侧,即让它指向4的起始地址
1.fseek(pf,4,SEEK_SET)
offset的符号:
向右移动:+号(可省略)
向左移动:-号(不可省略)
2.fseek(pf,-4,SEEK_END)
注意:此时以文件末尾位置为参照物:
(文件的末尾位置)(最后一个数据的末尾地址(即右侧))
向左4个单位后指向4的左侧
3.假设此时pf指向4的左侧,我们想让pf指向1的右侧(即2的左侧)
fseek(pf,-2,SEEK_CUR)
移动文件指针即可
在此过程中,我们用ftell来看文件指针指向哪里,
注意:每次使用fseek定位文件指针时无需将其回复到文件的起始位置
最后使用rewind让文件指针的位置回到文件的起始位置(这里是为了介绍rewind这个函数)
int main()
{
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读文件
int point = ftell(pf);
printf("初始:pf:%d\n", point);
fseek(pf, 4, SEEK_SET);
point = ftell(pf);
printf("第一种方案移动后:pf:%d\n", point);
fseek(pf, -4, SEEK_END);
point = ftell(pf);
printf("第二种方案移动后:pf:%d\n", point);
fseek(pf, -2, SEEK_CUR);
point = ftell(pf);
printf("第三种方案移动后:pf:%d\n", point);
rewind(pf);
point = ftell(pf);
printf("使用rewind还原后:pf:%d\n", point);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
文本文件和二进制文件
根据数据的组织形式,数据文件被称为文本文件或者二进制文件。
1.数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件。
2.如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文
本文件。
一个数据在内存中是怎么存储的呢?
字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。
如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符一个字节),而
二进制形式输出,则在磁盘上只占4个字节(VS2013测试)。
文件读取结束的判定
被错误使用的feof
牢记:在文件读取过程中,不能用feof函数的返回值直接来判断文件的是否结束。
feof 的作用是:当文件读取结束的时候,判断是读取结束的原因是否是:遇到文件尾结束。
1. 文本文件读取是否结束,判断返回值是否为 EOF ( fgetc ),或者 NULL ( fgets )
例如:
fgetc 判断是否为 EOF .
fgets 判断返回值是否为 NULL .
2. 二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。
例如:
fread判断返回值是否小于实际要读的个数。
文件缓冲区
1.ANSIC 标准采用“缓冲文件系统”处理的数据文件的.
2.所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。
3.从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。
4.如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。
5.缓冲区的大小是根据C编译系统决定的。
6.这里可以得出一个结论:
因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文
件。
如果不做,可能导致读写文件的问题。
以上就是C语言文件操作详解的全部内容,希望能对大家有所帮助,谢谢大家