🤡博客主页:醉竺
🥰本文专栏:《C语言深度解剖》
😻欢迎关注:感谢大家的点赞评论+关注,祝您学有所成!
✨✨💜💛想要学习更多C语言深度解剖点击专栏链接查看💛💜✨✨
目录
1. 为什么使用文件
程序执行起来后,称之为进程,进程运行过程中的数据,均在内存中,当我们需要把运算后的数据存储下来时,就需要文件。 做到数据的持久化。
2. 文件概述
2.1 什么是流
在C语言中,流是一个用于输入输出操作的抽象概念。它代表了一个数据的流动,从源头(比如键盘、文件、网络等)到目的地(比如屏幕、文件、网络等)。流可以看作是一个数据的序列,它按照一定的顺序被读取或写入。
C语言的标准库提供了一套文件输入输出函数,这些函数可以对流进行操作。在C中,所有的输入输出操作都是通过流来完成的。标准输入输出库 <stdio.h> 定义了流的概念,并提供了相关的函数。
以下是C语言中几个重要的流:
- stdin - 标准输入流,通常指键盘输入。
- stdout - 标准输出流,通常指屏幕输出。
- stderr - 标准错误流,用于输出错误信息,通常也是输出到屏幕,但与stdout分离,以便于错误信息不会与普通输出混淆。
流在C语言中是通过文件指针来引用的,文件指针是一个指向FILE结构的指针,FILE结构包含了流的状态信息。例如,当你打开一个文件时,C语言的fopen函数会返回一个FILE指针,通过这个指针,你可以对文件进行读或写操作。
FILE* fp; // 声明一个文件指针
fp = fopen("example.txt", "r"); // 打开一个文件,返回一个指向该文件的流
if (fp == NULL) {
// 错误处理
}
// ... 使用fp进行文件操作
fclose(fp); // 关闭流
2.2 什么是文件
文件:文件指存储在外部介质(如磁盘磁带)上数据的集合。操作系统(windows,Linux, Mac 均是)是以文件为单位对数据进行管理的。
在C语言中,所有的设备都被视为文件,它们的输入输出操作都可以通过文件流来进行。这使得C语言的输入输出模型非常统一和灵活。
磁盘上的文件是文件。 但是在程序设计中,我们一般谈的文件有两种:程序文件、数据文件(从文件功能的角度来分类的)。
- 程序文件
包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境 后缀为.exe)。
- 数据文件
文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件, 或者输出内容的文件。
本章讨论的是数据文件。
在以前各章所处理数据的输入输出都是以终端为对象的,即从终端的键盘输入数据,运行结果显示到显示器上。其实有时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理 的就是磁盘上文件。
2.3 文件名
3. 文件的打开和关闭
3.1 文件指针
缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名 字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是有系统 声明的,取名FILE.
打开文件后我们得到 FILE*类型的文件指针,通过该文件指针对文件进行操作,FILE 是 一个结构体类型,那么首先让我们来看下它里边都有什么呢?
不同的C编译器的FILE类型包含的内容不完全相同,但是大同小异。
每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息, 使用者不必关心细节。
一般都是通过一个 FILE 的指针来维护 FILE 结构的变量。
下面我们可以创建一个FILE*的指针变量:
FILE* pf;//文件指针变量
定义 pf 是一个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够找到与它关联的文件。
比如:
3.2 文件的打开和关闭
文件在读写之前应该先打开文件,在使用结束之后应该关闭文件。
在编写程序的时候,在打开文件的同时,都会返回一个FILE*的指针变量指向该文件,也相当于建立了指针和文件的关系。
ANSIC 规定使用fopen函数来打开文件,fclose来关闭文件。
- 当无法打开文件时,fopen函数会返回空指针。这可能是因为文件不存在,也可能是因为文件的位置不对,还可能是因为我们没有打开文件的权限。
- 函数 fclose()关闭给出的文件流, 释放已关联到流的所有缓冲区。fclose()执行成功 时返回 0,否则返回 EOF(-1).
打开方式如下:
实例代码:
要区分标准的输入输出,和文件操作中的输入输出:
4. 文件的顺序读写
上述简单了解需要用的时候查看一下即可,下面我会为上述每个函数举一个例子:
🍑fgetc
int fgetc(FILE *stream);
fgetc 常用于从文件中逐个字符地读取数据,通常在循环中使用,通过判断返回值是否为EOF来确定是否读取到文件末尾。
- 成功时返回读取的字符(类型为unsigned char),并将其转换为int。
- 如果到达文件末尾或发生读取错误,返回特殊的EOF(通常定义为-1)
#include <stdio.h>
int main() {
FILE *file = fopen("example.txt", "r");
if (file == NULL) {
perror("Failed to open file");
return 1;
}
int ch;
while ((ch = fgetc(file)) != EOF) {
putchar(ch); // 输出到标准输出
}
if (ferror(file)) {
perror("Error reading from file");
}
fclose(file);
return 0;
}
🍑fputc
int fputc(int c, FILE *stream);
fputc用于将一个字符写入到文件中。
#include <stdio.h>
int main() {
FILE *file = fopen("output.txt", "w");
if (file == NULL) {
perror("Failed to open file");
return 1;
}
const char *str = "Hello, World!";
while (*str) {
if (fputc(*str++, file) == EOF) {
perror("Error writing to file");
fclose(file);
return 1;
}
}
fclose(file);
return 0;
}
🍑fgets
fgets通常用于从文件或标准输入读取一行文本。
char *fgets(char *str, int n, FILE *stream);
参数:
- str:一个指向字符数组的指针,用于存储读取的内容。
- n:指定最多读取的字符数,包括结尾的空字符('\0')。因此,fgets最多读取n-1个字符。
- stream:指向FILE对象的指针,表示要读取的输入流,通常通过fopen函数获得。
返回值:
- 成功时,返回指向str的指针。
- 如果发生错误或者到达文件末尾且没有读取到任何字符,则返回NULL。
特性:
- fgets读取到换行符('\n')时会停止,并将换行符包括在返回的字符串中。
- 如果一行文本超过n-1个字符,fgets只读取前n-1个字符,并在结尾添加空字符。
#include <stdio.h>
int main() {
FILE* file = fopen("example.txt", "r");
if (file == NULL) {
perror("Failed to open file");
return 1;
}
char buffer[5]; // 用于存储读取的行
while (fgets(buffer, sizeof(buffer), file) != NULL) {
printf("%s", buffer); // 输出读取的行
}
if (feof(file)) {
printf("Reached end of file.\n");
}
else if (ferror(file)) {
perror("Error reading from file");
}
fclose(file);
return 0;
}
🍑fputs
int fputs(const char* str, FILE* stream);
fputs 用于将字符串写入到文件中。它的功能与 fputc 类似,但 fputs 处理的是字符串而不是单个字符。
#include <stdio.h>
int main() {
FILE *file = fopen("output.txt", "w");
if (file == NULL) {
perror("Failed to open file");
return 1;
}
const char *str = "Hello, World!\n";
if (fputs(str, file) == EOF) {
perror("Error writing to file");
fclose(file);
return 1;
}
fclose(file);
return 0;
}
#include <stdio.h>
int main() {
FILE* file = fopen("output.txt", "w"); // 打开文件进行写入
if (file == NULL) {
perror("Error opening file");
return 1;
}
// 写入字符串到文件
fputs("Hello, World!", file); // 写入字符串 "Hello, World!"
// 写入换行符
fputc('\n', file);
if (fclose(file) != 0) { // 检查文件是否成功关闭
perror("Error closing file");
}
return 0;
}
特性:
- fputs 不会自动在字符串的末尾添加换行符(\n),如果需要添加换行符,必须在字符串中包含换行符或者在调用 fputs 后显式地写入换行符。
- 它直接将字符串的内容写入到指定文件流中。
🍑fscanf
int fscanf(FILE* stream, const char* format, ...);
fscanf 用于从文件中读取格式化输入。它的工作原理类似于 scanf 函数,但 fscanf 是从给定的文件流中读取数据。
特性:
- fscanf 根据格式化字符串提取输入数据并存储在相应的变量中。
- 它可以处理多种数据类型,包括整数、浮点数、字符串等。
返回值:
- 成功匹配并赋值的输入项数。如果达到文件末尾或发生读取错误,将返回一个负值。
假设我们有一个文本文件 data.txt,其中包含以下数据:
42 3.14 Hello
我们希望从中提取一个整数、一个浮点数和一个字符串。
#include <stdio.h>
int main() {
FILE *file = fopen("data.txt", "r");
if (file == NULL) {
perror("Failed to open file");
return 1;
}
int integer;
float float_num;
char str[50];
if (fscanf(file, "%d %f %49s", &integer, &float_num, str) != 3) {
perror("Error reading from file");
fclose(file);
return 1;
}
printf("Integer: %d\n", integer);
printf("Float: %f\n", float_num);
printf("String: %s\n", str);
fclose(file);
return 0;
}
🍑fprintf
int fprintf(FILE* stream, const char* format, ...);
fprintf 用于将格式化输出写入到文件中。它与 printf 类似,但 fprintf 是将格式化的字符串输出到指定的文件流,而不是输出到标准输出(控制台)。
返回值:
- 成功时,返回写入的字符数(不包括结尾的空字符)。
- 如果发生输出错误,返回一个负值。
特性:
- fprintf 根据格式化字符串将数据写入文件。
- 支持多种数据类型的格式化输出,包括整数、浮点数、字符串等。
#include <stdio.h>
int main() {
FILE *file = fopen("output.txt", "w");
if (file == NULL) {
perror("Failed to open file");
return 1;
}
int integer = 42;
float float_num = 3.14;
const char *str = "Hello";
if (fprintf(file, "Integer: %d\nFloat: %.2f\nString: %s\n", integer, float_num, str) < 0) {
perror("Error writing to file");
fclose(file);
return 1;
}
fclose(file);
return 0;
}
🍑fread
size_t fread(void* ptr, size_t size, size_t nmemb, FILE* stream);
fread 用于从文件中读取数据块。它的设计目标是高效地从文件中读取二进制数据。
参数:
- ptr:指向要存储读取数据的内存块的指针。
- size:每个数据单元的大小(以字节为单位)。
- nmemb:要读取的单元数。
- stream:指向要从中读取数据的 FILE 对象的指针。
返回值:
- 返回实际读取的完整数据单元个数(nmemb)。如果返回值小于 nmemb,可能是因为到达文件尾或发生读取错误。
特性:
- 适用于二进制文件读取,也可以用于文本文件的原始读取。
- fread 不会在读取数据时自动添加终止符(如空字符 \0)。
fread 常用于从文件中读取大量二进制数据,如结构体数组、图像、音频数据等。以下是一个简单的示例,说明如何使用 fread 从文件中读取数据:假设我们有一个二进制文件 data.bin,其中存储了一些整数。
#include <stdio.h>
#include <stdlib.h>
int main() {
FILE *file = fopen("data.bin", "rb");
if (file == NULL) {
perror("Failed to open file");
return 1;
}
int buffer[10];
size_t read_count = fread(buffer, sizeof(int), 10, file);
if (read_count < 10) {
if (feof(file))
printf("End of file reached.\n");
else if (ferror(file))
perror("Error reading from file");
}
for (size_t i = 0; i < read_count; ++i) {
printf("Integer %zu: %d\n", i, buffer[i]);
}
fclose(file);
return 0;
}
🍑fwrite
size_t fwrite(const void* ptr, size_t size, size_t nmemb, FILE* stream);
fwrite 用于将数据块写入文件。它通常用于处理二进制文件写操作,但也可以用于文本文件的原始写入。
参数:
- ptr:指向要写入数据的内存块的指针。
- size:每个数据单元的大小(以字节为单位)。
- nmemb:要写入的单元数。
- stream:指向要写入数据的 FILE 对象的指针。
返回值:
- 返回实际写入的完整数据单元个数(nmemb)。如果返回值小于 nmemb,可能是因为发生了写入错误。
特性:
- 适合用于二进制文件写入,也可以用于文本文件的原始写入。
- fwrite 不会自动添加终止符,也不会转换数据格式。
#include <stdio.h>
#include <stdlib.h>
int main() {
FILE *file = fopen("data.bin", "wb");
if (file == NULL) {
perror("Failed to open file");
return 1;
}
int buffer[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
size_t write_count = fwrite(buffer, sizeof(int), 10, file);
if (write_count < 10) {
perror("Error writing to file");
} else {
printf("Successfully wrote %zu items to file.\n", write_count);
}
fclose(file);
return 0;
}
对比一组函数:
scanf 和 printf 我们都很熟悉了,fscanf 和 fprintf 我们上面已经介绍,下面讲解 sscanf 和 sprintf的用法
🍑sprintf
int sprintf(char *str, const char *format, ...);
sprintf 用于将格式化输出写入字符串。它的功能类似于 printf,但 sprintf 是将格式化的字符串写入到一个字符数组(字符串)中,而不是输出到控制台。
返回值:
- 返回写入到 str 中的字符数(不包括结尾的空字符)。
- 如果发生错误,则返回一个负值。
#include <stdio.h>
int main() {
char buffer[100];
int integer = 42;
double float_num = 3.14159;
const char *str = "Hello, World!";
int n = sprintf(buffer, "Integer: %d\nFloat: %.2f\nString: %s\n", integer, float_num, str);
if (n < 0) {
printf("Error formatting string.\n");
} else {
printf("Formatted string:\n%s", buffer);
}
return 0;
}
🍑sscanf
int sscanf(const char *str, const char *format, ...);
sscanf 是 C 标准库中的一个函数,用于从字符串中读取格式化数据。它的功能与 scanf 类似,但 sscanf 是从给定的字符串中读取数据,而不是从标准输入流读取。
sscanf 用于从字符串中提取数据,根据格式字符串将其解析并赋值给提供的变量。
返回值:
- 返回成功匹配并赋值的输入项数。
- 如果没有成功匹配任何项,则返回零。
- 如果发生读取错误或格式错误,则返回 EOF。
用法:
- sscanf 用于从字符串中提取数据,根据格式字符串将其解析并赋值给提供的变量。
#include <stdio.h>
int main() {
const char *input = "42 3.14 Hello";
int integer;
float float_num;
char str[50];
int n = sscanf(input, "%d %f %s", &integer, &float_num, str);
if (n == 3) {
printf("Parsed values:\n");
printf("Integer: %d\n", integer);
printf("Float: %.2f\n", float_num);
printf("String: %s\n", str);
} else {
printf("Failed to parse all values. Parsed %d values.\n", n);
}
return 0;
}
5. 文件的随机读写
5.1 fssek
fseek 常用于调整文件指针以便在文件中读取或写入特定位置的数据。 即根据文件指针的位置和偏移量来定位文件指针。
int fseek(FILE *stream, long int offset, int origin);
参数说明:
- stream:指向 FILE 对象的指针,该对象标识了要设置位置的文件流。
- offset:要移动的字节数,可以是正数、负数或零。
- origin:指定移动的起点,可以是以下常量之一:
- SEEK_SET:从文件流的开始位置开始移动。
- SEEK_CUR:从文件流的当前位置开始移动。
- SEEK_END:从文件流的结束位置开始移动。
#include <stdio.h>
int main() {
FILE *file = fopen("example.txt", "r");
if (file == NULL) {
perror("Failed to open file");
return 1;
}
// 将文件指针移动到文件开头偏移量 10 字节的位置
if (fseek(file, 10, SEEK_SET) != 0) {
perror("fseek error");
fclose(file);
return 1;
}
// 读取并输出接下来的字符
int c = fgetc(file);
if (c != EOF) {
printf("Character at offset 10: %c\n", c);
}
fclose(file);
return 0;
}
5.2 ftell
ftell 用于获取文件流中的当前文件位置指针的位置。它与 fseek 函数相关联,用于确定当前文件指针相对于文件开头的偏移量。
long int ftell(FILE* stream);
返回值:
- 成功时返回文件指针相对于文件开头的偏移量(以字节为单位)。该偏移量可以用作 fseek 函数的 offset 参数。
- 失败时返回 -1L,并设置相应的错误标志。
用法:
- ftell 常用于获取当前文件读取或写入位置,以便在以后恢复这个位置。
#include <stdio.h>
int main() {
FILE *file = fopen("example.txt", "r");
if (file == NULL) {
perror("Failed to open file");
return 1;
}
// 读取第一个字符
int c = fgetc(file);
if (c != EOF) {
printf("First character: %c\n", c);
}
// 获取当前文件指针的位置
long position = ftell(file);
if (position != -1L) {
printf("Current position: %ld\n", position);
} else {
perror("ftell error");
}
fclose(file);
return 0;
}
5.3 rewind
rewind 用于将文件指针重新设置到文件的开头。它还会清除与该文件流相关的错误和文件结束标志。常用于在文件中重新开始读取或写入操作。
void rewind ( FILE * stream );
#include <stdio.h>
int main() {
FILE *file = fopen("example.txt", "r");
if (file == NULL) {
perror("Failed to open file");
return 1;
}
// 读取并输出第一个字符
int c = fgetc(file);
if (c != EOF) {
printf("First character: %c\n", c);
}
// 将文件指针重置到文件开头
rewind(file);
// 再次读取并输出第一个字符
c = fgetc(file);
if (c != EOF) {
printf("First character after rewind: %c\n", c);
}
fclose(file);
return 0;
}
注意事项:
- 错误状态清除:rewind 会清除与文件流相关的错误状态和文件结束标志,因此在调用 rewind 后,之前的错误状态将不再有效。
- 文件开头定位:它等价于调用 fseek(stream, 0L, SEEK_SET);,但 rewind 还会清除错误和文件结束标志。
缓冲区刷新:对于输出流,rewind 会刷新缓冲区,以确保任何未写入的缓冲区内容得到处理。
6. 文本文件和二进制文件
根据数据的组织形式,数据文件被称为文本文件或者二进制文件。
数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件。如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件件。
一个数据在内存中是怎么存储的呢?
字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。
如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符一个字节),而
二进制形式输出,则在磁盘上只占4个字节(VS2013测试)。
测试代码:
#include <stdio.h>
int main()
{
int a = 10000;
FILE* pf = fopen("test.txt", "wb");
fwrite(&a, 4, 1, pf);//二进制的形式写到文件中
fclose(pf);
pf = NULL;
return 0;
}
7. 文件读取结束的判定
7.1 文件的分类
- 从用户观点:
特殊文件(标准输入输出文件或标准设备文件)。
普通文件(磁盘文件)。
- 从操作系统的角度看,每一个与主机相连的输入、输出设备看作是一个文件。
例:
输入文件:终端键盘
输出文件:显示屏和打印机
- 按数据的组织形式:
ASCII 文件(文本文件):每一个字节放一个 ASCII 代码。在文本文件(textfile)中,字节表示字符,这使人们可以检查或编辑文件。例如,C程序源代码是存储在文本文件中的。
二进制文件:把内存中的数据按其在内存中的存储形式原样输出到磁盘上存放。 在二进制文件(binaryfile)中,字节不一定表示字符;字节组还可以表示其他类型的数据,比如整数和浮点数。如果试图查看可执行C程序的内容,你会立刻意识到它是存储在二进制文件中的。
7.2 被错误使用的feof
int feof(FILE *stream);
易错点:
在文件读取过程中,不能用feof函数的返回值直接用来判断文件的是否结束。
而是应用于当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束。
返回值:
- 如果文件指针已到达文件末尾,feof 返回一个非零值(通常为 1)。
- 如果文件指针未到达文件末尾,返回 0。
注意事项:
- EOF 状态:feof 检测的是文件流的 EOF 状态标志,该标志只有在尝试读取文件末尾之后才会被设置。因此,feof 通常在失败的读取操作之后才有意义。
- 结合读取操作:通常你不应在循环条件中直接使用 feof,而是结合读取操作来判断文件读取状态。例如,使用 fgetc 或其他读取函数,然后在循环之外检查 feof。
- 错误与 EOF 区别:EOF 状态与读取错误不同。feof 仅指示已到达文件末尾,而不指示是否发生读错误。要检查读取错误,应使用 ferror 函数。
- 缓冲区刷新:在某些库实现中,文件的缓冲区可能影响 EOF 状态的检测。
正确的使用:
文本文件的例子:
二进制文件的例子:
8. 文件缓冲区
C语言对文件的处理方法:
缓冲文件系统:
非缓冲文件系统:
系统不自动开辟确定大小的缓冲区,而由程序为每个文件设定缓冲区。用非缓冲文件系统进行的输入输出又称为低级输入输出系统。
int fflush(FILE *stream);
void setbuf(FILE *stream, char *buffer);
int setvbuf(FILE *stream, char *buffer, int mode, size_t size);
向磁盘驱动器传入数据或者从磁盘驱动器传出数据都是相对较慢的操作。因此,在每次程序想读或写字符时都直接访问磁盘文件是不可行的。获得较好性能的诀窍就是缓冲(buffering):把写入流的数据存储在内存的缓冲区域内;当缓冲区满了(或者关闭流)时,对缓冲区进行“清洗”(写入实际的输出设备)。输入流可以用类似的方法进行缓冲:缓冲区包含来自输入设备的数据,从缓冲区读数据而不是从设备本身读数据。缓冲在效率上可以取得巨大的收益,因为从缓冲区读字符或者在缓冲区内存储字符几乎不花什么时间。当然,把缓冲区的内容传递给磁盘,或者从磁盘传递给缓冲区是需要花时间的,但是一次大的“块移动”比多次小字节移动要快很多。
当程序向文件中写输出时,数据通常先放入缓冲区中。当缓冲区满了或者关闭文件时,缓冲区会自动清洗。然而,通过调用fflush函数,程序可以按我们所希望的频率来清洗文件的缓冲区。
- 调用 fflush(fp);为和fp相关联的文件清晰了缓冲区。
- 调用 fflush(NULL); 清洗了全部输出流。如果调用成功,fflush函数会返回零;如果发生错误,则返回EOF。
#include <stdio.h>
#include <windows.h>
//VS2013 WIN10环境测试
int main()
{
FILE* pf = fopen("test.txt", "w");
fputs("abcdef", pf);//先将代码放在输出缓冲区
printf("睡眠10秒-已经写数据了,打开test.txt文件,发现文件没有内容\n");
Sleep(10000);
printf("刷新缓冲区\n");
fflush(pf);//刷新缓冲区时,才将输出缓冲区的数据写到文件(磁盘)
//注:fflush 在高版本的VS上不能使用了
printf("再睡眠10秒-此时,再次打开test.txt文件,文件有内容了\n");
Sleep(10000);
fclose(pf);
//注:fclose在关闭文件的时候,也会刷新缓冲区
pf = NULL;
return 0;
}
这里可以得出一个结论:
因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。 如果不做,可能导致读写文件的问题。
本篇文章到此结束,这篇文章学透,对于C语言中的文件操作就会行云流水~麻烦点个赞评论支持一下吧!