望大佬多多关注,内容持续更新!
在此电脑中的c盘和d盘中的文件夹就是文件,也就是磁盘上的文件就是文件。在程序设计中,我们需要谈论的是程序文件和数据文件。
源程序文件(后缀.c)、目标文件(windows环境后缀.obj)、可执行程序(后缀.exe)
存放数据的文件
我们在学习c语言程序的时候用到的printf、scanf输入输出数据的都是以终端为对象,即从终端的键盘输入数据,运行结果显示到显
示器上。其实也可以把信息放在磁盘上,需要的时候从磁盘上把数据读到内存中使用。
文件名包括三个部分:文件路径+文件名主干+文件后缀
例如:D:\App\DingDing
我们平时写的代码在运行时数据放在内存中,当程序退出的时候,数据自然就不存在了,占用的内存空间返回给了操作系统,这样当我们使用的时候就必须运行程序重新录入数据,这样使用就很麻烦。所以有不有方法让数据保留下来,我们不需要时再删除,需要的时候直接拿出来用呢?方法是有的,把数据存放在磁盘文件和数据库等方式都是可行的。这里使用磁盘文件就是把数据放在电脑磁盘上,使得数据持久化。
每个被使用的文件其实都在内存中开辟了一个文件信息区,用来存放文件信息。这些信息都是保存在一个结构体变量中,该结构体类型时系统声明的,类型是FILE。
那么用一张图来解释一下这个文件信息区:
在Visual Studio 2013编译环境中合格文件结构体是这样的:
struct _iobuf {
char* _ptr;
int _cnt;
char* _base;
int _flag;
int _file;
int _charbuf;
int _bufsiz;
char* _tmpfname;
};
typedef struct _iobuf FILE;
这个struct _iobuf结构体类型重新定位为FILE,每当我们打开文件,系统就会根据文件的情况自动创建一个FILE结构体类型,填充文件信息,我们不需要关心FILE结构体中是怎么用的,怎么实现的,我们只需要知道打开文件就会出现一个对应的文件信息区,这个文件信息区是存放在内存中的,就相当于这个文件结构体类型,所以使用的时候通过文件指针找到这个文件结构体,也就是文件信息区,这里就是文件指针的概念。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够找到与它关联的文件。
FILE* fopen(const char* filename, const char* mode);
filename:包含要打开的文件名的C字符串(它的值应该遵循运行环境的文件名规范,可以包含路径)
mode: 包含文件访问模式的C字符串
- 返回的指针可以通过调用fclose或freopen从文件中解关联。所有打开的文件在正常程序终止时自动关闭。
- 如果文件被成功打开,该函数返回一个指向file对象的指针,可用于未来操作中识别流,否则返回NULL指针。
int fclose(FILE* stream);
stream: 指向FILE对象的指针,该对象指定要关闭的流。
如果流成功关闭,则返回零值。如果失败,则返回EOF。
int main()
{
//打开文件
//打开c盘下的test文件夹中的test.txt时,在内存中映射了一块有关此文件的文件信息区,
//这个文件信息区的类型是FILE结构体类型,然后把地址给到pf文件指针,就可以通过pf指针来访问这块文件信息区。
FILE* pf = fopen("c:\\test\\test.txt", "w");
if (pf == NULL)
{
perror("fopen():");
}
//关闭文件
//关闭文件就是指通过这个文件指针关闭这个文件信息区,进而关闭此文件
fclose(pf);
pf = NULL;
return 0;
}
int fputc ( int character, FILE * stream );
作用:将字符写入流
character:要写的字符的int提升。写入时,该值在内部转换为无符号字符
sream: 指向标识输出流的FILE对象的指针
- 如果成功,则返回所写的字符。
- 如果发生写错误,则返回EOF并设置错误指示符
#include
int main()
{
//打开文件
//打开c盘下的test文件夹中的test.txt时,在内存中映射了一块有关此文件的文件信息区,
//这个文件信息区的类型是FILE结构体类型,然后把地址给到pf文件指针,就可以通过pf指针来访问这块文件信息区。
FILE* pf = fopen("c:\\test\\test.txt", "w");
if (pf == NULL)
{
perror("fopen():");
return 1;
}
//写入数据
int i = 0;
for (i = 0; i < 26; i++)
{
fputc('a' + i,pf);
}
//关闭文件
//关闭文件就是指通过这个文件指针关闭这个文件信息区,进而关闭此文件
fclose(pf);
pf = NULL;
return 0;
}
运行结果:
int fgetc ( FILE * stream );
作用:从流中获取字符
stream:指向标识输入流的FILE对象的指针
- 如果成功,则返回已读取的字符
- 如果调用时流位于文件末尾,则函数返回EOF并为流设置文件末尾指示符,如果发生读错误,该函数返回EOF并为流设置错误指示符。
#include
int main()
{
//打开文件
//打开c盘下的test文件夹中的test.txt时,在内存中映射了一块有关此文件的文件信息区,
//这个文件信息区的类型是FILE结构体类型,然后把地址给到pf文件指针,就可以通过pf指针来访问这块文件信息区。
FILE* pf = fopen("c:\\test\\test.txt", "r");
if (pf == NULL)
{
perror("fopen():");
return 1;
}
//读取数据
int ch = 0;
while ((ch = fgetc(pf)) != EOF)
{
printf("%c ", ch);
}
//关闭文件
//关闭文件就是指通过这个文件指针关闭这个文件信息区,进而关闭此文件
fclose(pf);
pf = NULL;
return 0;
}
运行结果:
int fputs ( const char * str, FILE * stream );
作用:将字符串写入流
str: 包含要写入流的内容的C字符串。
stream: 指向标识输入流的FILE对象的指针
1.如果成功,则返回非负值
2.如果发生错误,该函数返回EOF并设置错误指示符(ferror)
#include
int main()
{
//打开文件
//打开c盘下的test文件夹中的test.txt时,在内存中映射了一块有关此文件的文件信息区,
//这个文件信息区的类型是FILE结构体类型,然后把地址给到pf文件指针,就可以通过pf指针来访问这块文件信息区。
FILE* pf = fopen("c:\\test\\test.txt", "w");
if (pf == NULL)
{
perror("fopen():");
return 1;
}
//写入数据
fputs("hello world!\n", pf);
char arr[] = "abcdef";
fputs(arr, pf);
//关闭文件
//关闭文件就是指通过这个文件指针关闭这个文件信息区,进而关闭此文件
fclose(pf);
pf = NULL;
return 0;
}
运行结果:
char * fgets ( char * str, int num, FILE * stream );
作用:从流中获取字符串
str: 指针插入到一个字符数组中,读取的字符串在其中被复制
num: 要复制到str的最大字符数(包括终止的空字符)。
stream: 指向标识输入流的FILE对象的指针。
- 如果成功,函数返回str。
- 如果读取字符时遇到文件结束符,则设置eof指示符。如果在读取任何字符之前发生这种情况,则返回的指针是空指针(并且str的内容保持不变)。如果发生读错误,则设置错误指示符(ferror)并返回空指针(但str所指向的内容可能已更改)
int main()
{
//打开文件
//打开c盘下的test文件夹中的test.txt时,在内存中映射了一块有关此文件的文件信息区,
//这个文件信息区的类型是FILE结构体类型,然后把地址给到pf文件指针,就可以通过pf指针来访问这块文件信息区。
FILE* pf = fopen("c:\\test\\test.txt", "r");
if (pf == NULL)
{
perror("fopen():");
return 1;
}
//读取数据一行一行读
char arr[20] = "#############";
fgets(arr, 20, pf);
printf("%s\n", arr);
fgets(arr, 20, pf);
printf("%s\n", arr);
//关闭文件
//关闭文件就是指通过这个文件指针关闭这个文件信息区,进而关闭此文件
fclose(pf);
pf = NULL;
return 0;
}
运行结果:
为什么会出现这样呢?原因很简单,不管你从流中拿多好,它每次都是从一行里面拿,如果超出了这一行的元素个数,它依旧是从这一行中拿数据,不可能从第二行拿。
另外还要注意num:
通过这个图我们看得出来num是指从pf指向的流中拿到num个字符,其中还包括’\0’结束标志字符。
int fprintf ( FILE * stream, const char * format, … );
作用:将格式化的数据写入流
stream: 指向标识输出流的FILE对象的指针。
format:C字符串,包含要写入流的文本。它可以有选择地包含嵌入的格式说明符,这些说明符将被后续附加参数中指定的值替换并按要求格式化。
- 如果成功,则返回写入的字符总数。
- 如果发生写错误,则设置错误指示符(ferror)并返回负数。如果在写入宽字符时发生多字节字符编码错误,则errno设置为EILSEQ并返回负数
#include
struct S
{
char name[20];
int age;
float weight;
};
int main()
{
struct S s = { "lisi",20,123.5f };
FILE* pf = fopen("c:\\test\\test.txt", "w");
if (NULL == pf)
{
perror("fopen():");
return 1;
}
fprintf(pf, "%s %d %f", s.name, s.age, s.weight);
fclose(pf);
pf = NULL;
return 0;
}
运行结果:
int fscanf ( FILE * stream, const char * format, … );
作用:从流读取格式化的数据
stream: 指向标识输入流的FILE对象的指针。
format:C字符串,包含一个字符序列,该字符序列控制如何处理从流中提取的字符
1.成功时,函数返回成功填充的参数列表的项数
2.如果在读取过程中发生读取错误或到达文件末尾,则设置适当的指示符。并且,如果在成功读取任何数据之前发生任何一种情况,则返回EOF。
struct S
{
char name[20];
int age;
float weight;
};
int main()
{
struct S s = { "lisi",20,123.5f };
FILE* pf = fopen("c:\\test\\test.txt", "r");
if (NULL == pf)
{
perror("fopen():");
return 1;
}
fscanf(pf, "%s %d %f", s.name, &s.age, &s.weight);
printf("%s %d %f", s.name, s.age, s.weight);
fclose(pf);
pf = NULL;
return 0;
}
运行结果:
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
作用:向流写入数据块
ptr: 指向要写入的元素数组的指针,转换为const void*。
size: 要写入的每个元素的大小(以字节为单位)。
count: 元素数量,每个元素的大小为size字节。
stream: 指向指定输出流的FILE对象的指针。
- 返回成功写入的元素总数。
- 如果此数字与count参数不同,则写入错误阻止函数完成。在这种情况下,将为流设置错误指示符,如果size或count为零,则函数返回零,并且错误指示符保持不变。
#include
struct S
{
char name[20];
int age;
float score;
};
int main()
{
struct S s = { "zhangsan", 20, 95.5f };
FILE*pf = fopen("c:\\test\\test.txt", "wb");
if (NULL == pf)
{
perror("fopen");
return 1;
}
fwrite(&s, sizeof(s), 1, pf);
fclose(pf);
pf = NULL;
return 0;
}
运行结果:
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
作用:从流中读取数据块
ptr:指向内存块的指针,其大小至少为(sizecount)个字节,转换为void。
size:要读取的每个元素的大小(以字节为单位)。
count:元素数量,每个元素的大小为size字节。
stream:指向指定输入流的FILE对象的指针。
- 返回成功读取的元素总数。
- 如果该数字与count参数不同,则要么发生了读取错误,要么在读取时已到达文件结束。在这两种情况下,都设置了适当的指标,可以分别用ferror和feof进行检查。如果size或count为零,则函数返回零,并且流状态和ptr所指向的内容保持不变。
#include
struct S
{
char name[20];
int age;
float score;
};
int main()
{
struct S s = {0};
FILE* pf = fopen("test.txt", "rb");
if (NULL == pf)
{
perror("fopen");
return 1;
}
fread(&s, sizeof(s), 1, pf);
printf("%s %d %f\n", s.name, s.age, s.score);
fclose(pf);
pf = NULL;
return 0;
}
运行结果:
scanf/fscanf/sscanf
printf/fprintf/sprintf
scanf:按照一定的格式从键盘输入数
printf:按照一定的格式把数据打印到屏幕上
这组函数适用于标准输入/输出的格式化的输入/输出语句
fscanf:按照一定的格式从输入流(文件/stdin)输入数据
fprintf:按照一定的格式从输出流(文件/stdout)输出数据
这组函数适用于所有的输入/输出流的格式化输入/输出语句
sscanf:从字符串中按照一定的格式读取出格式化的数据
sprintf:把格式化的数据按照银锭的格式换成字符串
提一下sscanf和sprintf函数用法:
sprintf:
int sprintf ( char * str, const char * format, … );
作用:写入格式化数据到字符串str中
返回值情况:如果成功,则返回写入的字符总数。此计数不包括自动附加在字符串末尾的额外空字符。如果失败,则返回负数。
sscanf:
int sscanf ( const char * s, const char * format, …);
作用:从字符串s中读取格式化数据
返回值情况:成功时,函数返回参数列表中成功填充的项数。在匹配失败的情况下,该计数可以匹配预期的项目数量,也可以更少(甚至为零)。如果在成功解释任何数据之前出现输入失败,则返回EOF。
#include
struct S
{
char name[10];
int age;
float score;
};
int main()
{
char buf[100] = {0};
struct S tmp = { 0 };
struct S s = { "zhangsan", 20, 95.5f };
//从结构体s中写入数据到buf
sprintf(buf, "%s %d %f", s.name, s.age, s.score);//以字符串的形式打印
printf("%s\n", buf);
//从buf字符串中读取数据
sscanf(buf, "%s %d %f", tmp.name, &(tmp.age), &(tmp.score));
printf("%s %d %f\n", tmp.name, tmp.age, tmp.score);//以结构体的形式打印
return 0;
}
前面我们介绍的是顺序读写,这里我们来介绍一下随机读写。
int fseek ( FILE * stream, long int offset, int origin );
作用:将与流关联的位置指示器设置为一个新位置(意思就是根据文件指针的位置和偏移量来定位文件指针
)
参数说明:
stream:指向标识流的FILE对象的指针。
offset:二进制文件:要从原点偏移的字节数。
origin:作为偏移量参考的位置。SEEK_SET:文件的开始;SEEK_CUR:文件指针的当前位置;SEEK_END:文件的末尾。
实例:
这里就列举了一个SEEK_SET的例子,其他的很容易就懂了。
long int ftell ( FILE * stream );
作用:返回文件指针相对于起始位置的偏移量
这里就不再举例说明了,很容易。
void rewind ( FILE * stream );
作用:让文件指针的位置回到文件的起始位置。
在文件读取过程中,不能用feof函数的返回值直接用来判断文件的是否结束,而是应用于当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束。
观察上述我们顺序读取的函数就可以清晰知道文件结束是不同的。这个是要重点注意的,因为我们使用的时候要知道是读取失败结束还是遇到文件结尾结束,不然bug就很难找到。
ANSIC 标准采用“缓冲文件系统”处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定的(VS2022缓冲区大小是512字节)。
总结:
因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。如果不做,可能导致读写文件的问题。