c语言的文件操作与文件缓冲区

简介: 如果没有文件,我们写的程序的数据是存储在电脑的内存中,如果程序退出,内存回收,数据就丢失了,等再次运行程序,是看不到上次程序的数据的,如果要将数据进行持久化的保存,我们可以使用文件。磁盘(硬盘)上的文件是文件。但是在程序设计中,我们⼀般谈的⽂件有两种:程序文件、数据文件(从文件功能的角度来分类 的)。就比如说我们电脑中以.txt为后缀的就是文件的一种,他就是数据文件。.exe为后缀的就为程序文件。函数名功能适用范围fgetc字符输入函数所有输入流fputc字符输出函数所有输出流fgets。


目录

C语言文件操作函数汇总
首先先给出所有函数与其简单介绍。方便已经学过的进行快速的回忆。

简单介绍文件
为什么使用文件
如果没有文件,我们写的程序的数据是存储在电脑的内存中,如果程序退出,内存回收,数据就丢失了,等再次运行程序,是看不到上次程序的数据的,如果要将数据进行持久化的保存,我们可以使用文件。

什么是文件
磁盘(硬盘)上的文件是文件。

但是在程序设计中,我们⼀般谈的⽂件有两种:程序文件、数据文件(从文件功能的角度来分类 的)。

就比如说我们电脑中以.txt为后缀的就是文件的一种,他就是数据文件。.exe为后缀的就为程序文件。

文件名
⼀个文件要有⼀个唯一的文件标识,以便用户识别和引用。

文件名包含三个部分分:文件路径+文件名主干+文件后缀

其中c:\code\就为文件路径test为文件名主干.txt为文件后缀

但是一般来说为了方便起见,文件标识常被称为文件名。

二进制文件和文本文件
根据数据的组织形式,数据文件被称为文本文件或者⼆进制文件。

计算机在内存中存储数据时,所有数据最终都以二进制(0和1)形式存储。无论是数字、字符、图像、音频,还是程序指令,它们在内存中的底层存储形式都是由二进制位(bit) 组成的。

所有如果不加转换的输出到外存的文件中,就是二进制文件。

如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件。

⼀个数据在文件中是怎么存储的呢?

对于字符⼀律以ASCII形式存储,但对于整数值型数据既可以用ASCII形式存储,又可以用二进制形式来进行存储。

如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符⼀个字节),而⼆进制形式输出,则在磁盘上只占4个字节。

下面给出一段代码来进行验证。

define _CRT_SECURE_NO_WARNINGS

include

int main()
{
int a = 10000;
FILE* pf = fopen("test.txt", "wb");
fwrite(&a, 4, 1, pf);//二进制的形式写到文件中
fclose(pf);
pf = NULL;
return 0;
}
在VS上打开二进制文件:

简单解释:

在这个例子中:

a=10000,在内存中以 4字节存储。
在小端序(Little-Endian)架构上(如 x86/x64 系统),10000 的二进制表示是:
0x00002710
存储在内存中的字节顺序为:

0x10 0x27 0x00 0x00
使用 fwrite 将这4个字节按二进制原样写入文件。

文件内容 (小端序示例)就会是显示

10 27 00 00
在大端下就为

00 00 27 10
那么这时候就有人问了,那前面的那8个0是什么意思?

当你在Visual Studio (VS) 或者一些十六进制编辑器中打开二进制文件时,发现文件开头有额外的8个字节的 0,这是由于文件头(File Header)或者缓冲区(Buffer Padding)造成的。这个无需关心,如果在外面打开test.txt他前面是没有那8个0的。

流和标准流

我们程序的数据需要输出到各种外部设备,也需要从外部设备获取数据,不同的外部设备的输入输出操作各不相同,为了方便程序员对各种设备进行方柏霓的操作,我们抽象出了流的概念,我们可以把流想象成流淌着字符的河。
C程序针对文件、画面、键盘等的数据输入输出操作都是通过流操作的。
流表示数据在程序和外部设备(如文件、网络、键盘、显示器)之间传输的通道。
⼀般情况下,我们要想向流里写数据,或者从流中读取数据,都是要打开流,然后操作。
上面的一些定义,不免有点抽象,就拿上上面对于一些文件操作的函数来简单联系一下。

流(Stream) 是一个抽象概念,表示数据在输入/输出时的传输路径。

FILE* 是标准 I/O 库对流的封装,提供了:

缓冲
格式化读写
错误和 EOF 检测
open 和 write 直接操作文件描述符,不属于流操作,更接近底层系统调用。

fopen/fwrite:操作的是 文件流(FILE)。
open/write:操作的是 文件描述符(int),直接对文件进行原始字节流的读写。
我们对文件使用fopen函数就是对流进行操作,其对应就是打开一个流(也就是打开一个抽象的数据传输通道)。

标准流
那为什么我们从键盘输入数据,向屏幕上输出数据,并没有打开流呢?

那是因为C语言程序在启动的时候,默认打开了3个流:

用于从外部输入设备(通常是键盘)获取数据。
在 C 语言中,scanf 等函数会从标准输入流中读取用户输入的数据。

用于将程序的正常输出发送到外部显示设备(通常是屏幕)。
在 C 语言中,printf 等函数会将信息输出到标准输出流。

用于将程序的错误信息发送到外部显示设备(通常是屏幕)。
标准错误流与标准输出流是独立的,确保错误信息能够即刻显示,而不受输出缓冲的影响。
这是在默认下打开了这三个流,我们使用scanf、printf等函数就可以直接进行输入输出操作的。

stdin、stdout、stderr 三个流的类型是:FILE * 通常称为文件指针。

在C语言中,就是通过 FILE * 的文件指针来维护流的各种的操作的。

文件指针
在缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。

每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件的状态及文件当前的位置等)。这些信息是保存在⼀个结构体变量中的。该结构体类型是由系统声明的,取名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;
不同的C编译器的FILE类型包含的内容不完全相同,但本质还是不变的,大同小异而已。

所以每当我们打开一个文件的时候,这时候系统就会根据文件的情况自动创建⼀个FILE结构的变量,并将其中的数据填充好,我们作为使用者来说,就对系统来说只要做个乖宝宝,正确使用他就可以了,而且系统是十分聪明的,他让我们使用起来十分的方便。

下面我们可以创建⼀个FILE*的指针变量:

FILE* pf; // 文件指针类型
此时pf做为一指向FILE类型数据的指针变量,但我们只能说只是定义了一个文件指针类型,并没有让他指向一个对象。那么我们可以使pf指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件。也就是说,可以通过该文件指针变量就可以间接找到与它相关联的文件。

比如:

文件的打开和关闭
对于我们日常的认识来说,要使用冰箱,我们是先打开冰箱,然后在使用,最后再关闭。而且打开与关闭这个过程二者不可缺一,你不能说只打开使用,忘记关了,这就很浪费资源。那么对于文件来说也是如此,我们再使用文件的时候也是先要打开,并且使用完后必须要关闭,这个过程很重要!!!一定要记得关闭!!!

在编写程序的时候,在打开文件的同时,都会返回⼀个FILE*的指针变量指向该文件,也相当于建立了指针和文件的关系。

ANSI C 规定使用fopen来打开文件,使用fclose来关闭文件。

// 打开文件

FILE fopen(const char filename, const char* mode);

// 关闭文件

int fclose(FILE* stream);
mode表示文件的打开模式,下面都是文件的打开模式:

模式 含义 文件不存在时的行为
"r" 只读 如果文件不存在,报错
"w" 只写 创建新文件或覆盖已有文件
"a" 追加 创建新文件或在已有文件尾部追加
"rb" 只读(二进制) 如果文件不存在,报错
"wb" 只写(二进制) 创建新文件或覆盖已有文件
"ab" 追加(二进制) 创建新文件或在已有文件尾部追加
"r+" 读写 如果文件不存在,报错
"w+" 读写 创建新文件或覆盖已有文件
"a+" 读写 创建新文件或在已有文件尾部追加
"rb+" 读写(二进制) 如果文件不存在,报错
"wb+" 读写(二进制) 创建新文件或覆盖已有文件
"ab+" 读写(二进制) 创建新文件或在已有文件尾部追加
代码实操举例:

include

int main()
{
FILE* pf; // 定义一个文件指针
// 打开文件
pf = fopen("test.txt", "w");
if (pf == NULL)
{
// 打开失败
printf("fopen fail\n");
exit(1);
}
fclose(pf);
pf = NULL;
return 0;
}
文件的顺序读写
顺序读写函数介绍
函数名 功能 适用范围
fgetc 字符输入函数 所有输入流
fputc 字符输出函数 所有输出流
fgets 文本行输入函数 所有输入流
fwrite 二进制输出函数 文件输出流
fputs 文本行输出函数 所有输出流
fscanf 格式化输入函数 所有输入流
fprintf 格式化输出函数 所有输出流
fread 二进制输入函数 文件输入流

下面是对每一个函数进行一个代码使用案例

读取单个字符:

include

int main()
{
FILE *file = fopen("example.txt", "r");
if (file == NULL)
{
printf("文件打开失败!\n");
return 1;
}

char ch = fgetc(file);  // 读取单个字符
while (ch != EOF) 
{
    printf("%c", ch);
    ch = fgetc(file);
}

fclose(file);
return 0;

}

写入单个字符:

include

int main() {
FILE *file = fopen("example.txt", "w");
if (file == NULL) {
printf("文件打开失败!\n");
return 1;
}

fputc('A', file);  // 写入单个字符

fclose(file);
return 0;

}

读取一行文本:

include

int main() {
FILE *file = fopen("example.txt", "r");
if (file == NULL) {
printf("文件打开失败!\n");
return 1;
}

char line[100];
fgets(line, sizeof(line), file);  // 读取一行文本
printf("读取的行: %s\n", line);

fclose(file);
return 0;

}

写入二进制数据:

include

int main() {
FILE *file = fopen("example.bin", "wb");
if (file == NULL) {
printf("文件打开失败!\n");
return 1;
}

int data = 12345;
fwrite(&data, sizeof(int), 1, file);  // 写入二进制数据

fclose(file);
return 0;

}

输出一行文本:

include

int main() {
FILE *file = fopen("example.txt", "w");
if (file == NULL) {
printf("文件打开失败!\n");
return 1;
}

fputs("Hello, World!\n", file);  // 写入一行文本

fclose(file);
return 0;

}

读取格式化的数据:

include

int main() {
FILE *file = fopen("example.txt", "r");
if (file == NULL) {
printf("文件打开失败!\n");
return 1;
}

int num;
fscanf(file, "%d", &num);  // 读取整数
printf("读取的数字: %d\n", num);

fclose(file);
return 0;

}

输出格式化的数据:

include

int main() {
FILE *file = fopen("example.txt", "w");
if (file == NULL) {
printf("文件打开失败!\n");
return 1;
}

fprintf(file, "Hello, %s!\n", "World");  // 格式化输出字符串

fclose(file);
return 0;

}

读取二进制数据:

include

int main() {
FILE *file = fopen("example.bin", "rb");
if (file == NULL) {
printf("文件打开失败!\n");
return 1;
}

int data;
fread(&data, sizeof(int), 1, file);  // 读取二进制数据
printf("读取的数字: %d\n", data);

fclose(file);
return 0;

}
文件的随机读写
在上述文件操作函数中,顺序读写的特点是所有的输入操作都是从文件的开头开始,所有的输出操作同样也是从文件的开头开始。

对于输入操作来说,这种顺序访问方式通常不会引起太大的问题,因为我们通常是依次读取文件内容。然而,对于输出操作来说,这种顺序访问方式可能会带来一个明显的问题:每次从新打开然后写入都会从文件的开头开始,导致原有内容被覆盖。

为了解决这个问题,就需要引入文件的随机读写机制。随机读写允许程序在文件的任意位置进行读写操作,而不是局限于从头开始。这种机制通过文件指针的灵活移动来实现,可以有效避免数据被意外覆盖的问题,同时也大大提高了文件操作的灵活性和效率。

随机读写的函数(如 fseek 和 ftell)使得程序员可以精确地控制文件指针的位置,从而在文件的任意位置进行读写。这种机制对于需要频繁修改文件特定部分内容的应用场景尤为重要。

"w" 模式:指针从文件开头开始。文件内容被清空。
"a" 模式:指针直接移动到文件末尾,无论之前内容如何。
"r" 模式:指针从文件开头开始。

fwrite / fprintf / fputc:每次写入后,文件指针自动向后移动,停留在最后写入的位置。
如果你不手动移动文件指针,下一次写入会接着上一次的写入位置继续。

fread / fscanf / fgetc:每次读取后,文件指针自动向后移动,停留在读取结束的位置。

文件打开时:指针位置取决于文件打开模式("w" 开头、"a" 末尾、"r" 开头)。
文件操作时:指针的位置由最近一次的操作决定(读取/写入后指针自动移动)。
手动控制:使用 fseek 或 rewind 来显式控制文件指针的位置。
动态跟踪:使用 ftell 查看当前文件指针的位置。
fseek
根据文件指针的位置和偏移量来定位文件指针(文件内容的光标)。

函数原型:

int fseek ( FILE * stream, long int offset, int origin );
对于第三个参数的解释:

origin:起始位置,决定偏移量从哪里开始计算。

第三个参数 origin 的取值及含义

SEEK_SET 从文件的开头开始移动 文件的开头 偏移量从文件开头开始计算(offset 通常为正数)
SEEK_CUR 从当前位置开始移动 文件指针当前的位置 偏移量从当前指针位置开始计算(可正可负)
SEEK_END 从文件的末尾开始移动 文件末尾 偏移量从文件末尾开始计算(通常为负数)
代码举例:

include

int main()
{
FILE* pf;
pf = fopen("test.txt", "wb");
fputs("This is an apple.", pf);
fseek(pf, 9, SEEK_SET);
fputs(" sam", pf);
// 这样在输出就是在第九个字符后开始输出
// 变为:This is a sample.
fclose(pf);
pf = NULL;
return 0;
}
ftell
返回文件指针相对于起始位置的偏移量。

函数原型:

long int ftell ( FILE * stream );
代码举例:

include

int main()
{
FILE* pf;
pf = fopen("test.txt", "wb");
fputs("This is an apple.", pf);
fseek(pf, 9, SEEK_SET);
// 现在已经文件指针相对于起始位置的偏移量设置为了9
printf("%d\n", ftell(pf));
fputs(" sam", pf);
// 这样在输出就是在第九个字符后开始输出
// 变为:This is a sample.
fclose(pf);
pf = NULL;
return 0;
}
代码运行结果:

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

函数原型:

void rewind ( FILE * stream );
代码举例:

include

int main()
{
FILE* pf;
pf = fopen("test.txt", "wb");
fputs("This is an apple.", pf);
fseek(pf, 9, SEEK_SET);
// 现在已经文件指针相对于起始位置的偏移量设置为了9
printf("%d\n", ftell(pf));
// 偏移量变为0
rewind(pf);
printf("%d\n", ftell(pf));
fclose(pf);
pf = NULL;
return 0;
}
代码运行结果:

文件读取结束的判定
我们在读取一个文件的时候,不免要知道上面时候截至,所以就需要调用一些函数,来判断当前的文件指针是否到尾了,所以这时候EOF与NULL在这里的特殊。

但是要注意的是:牢记在文件读取过程中,不能用feof函数的返回值直接来判断文件的是否结束。

feof 的作用是:当文件读取结束的时候,判断是读取结束的原因是否是:遇到文件尾结束。

文本文件读取是否结束,判断返回值是否为EOF(fgetc),或者NULL(fgets)

例如:

fgetc 判断是否为 EOF。
fgets 判断返回值是否为 NULL。
二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。

例如:fread判断返回值是否小于实际要读的个数。

文本文件的例子:

include

include

int main(void) {
int c; // 使用 int 而不是 char 来存储 fgetc 返回值,能够正确处理 EOF
FILE* fp = fopen("test.txt", "r");
if (!fp) {
perror("File opening failed"); // 输出打开文件失败的原因
return EXIT_FAILURE;
}

// 使用 fgetc 读取文件内容
while ((c = fgetc(fp)) != EOF) { // 标准 C I/O 读取文件循环
    putchar(c); // 输出读取到的字符到标准输出
}

// 判断结束原因
if (ferror(fp)) {
    puts("I/O error when reading");
} else if (feof(fp)) {
    puts("\nEnd of file reached successfully");
}

fclose(fp); // 关闭文件
return EXIT_SUCCESS;

}
二进制文件的例子:

include

include

enum { SIZE = 5 };

int main(void) {
double a[SIZE] = {1., 2., 3., 4., 5.};
FILE fp = fopen("test.bin", "wb"); // 必须使用二进制模式
fwrite(a, sizeof
a, SIZE, fp);
fclose(fp);

double b[SIZE];
fp = fopen("test.bin", "rb");
size_t ret_code = fread(b, sizeof *b, SIZE, fp); // 读 double 的数组

if (ret_code == SIZE) {
    puts("Array read successfully, contents: ");
    for (int n = 0; n < SIZE; ++n)
        printf("%f ", b[n]);
    putchar('\n');
} else { // 错误处理
    if (feof(fp))
        printf("Error reading test.bin: unexpected end of file\n");
    else if (ferror(fp))
        perror("Error reading test.bin");
}
fclose(fp);
return 0;

}

putchar 是 C 语言标准库中的一个字符输出函数,用于向标准输出(通常是屏幕)输出一个字符。
函数原型:
// 函数原型
int putchar(int c);
// 参数:

// c:要输出的字符(作为 int 传入,但会转换为 unsigned char)

// 返回值:

// 成功时:返回写入的字符。
// 失败时:返回 EOF,并设置错误标志。
在调用 fgetc 函数时,文件指针会自动向后移动到下一个字符。这是 fgetc 函数的默认行为。
文件缓冲区
在 ANSI C 标准中,缓冲文件系统(Buffered I/O System)是一个重要的特性,旨在提高程序对文件的读写效率。其基本原理是通过缓冲区(通常是在内存中分配的一个区域)来缓存读写操作,减少磁盘操作次数,从而提升性能。

缓冲文件系统的工作流程:
输出(写入文件)

当程序想要将数据写入文件时,它首先将数据写入内存中的缓冲区。
数据不会立刻写入磁盘,而是先存储在内存中的缓冲区中,等待缓冲区满了以后,整个缓冲区的数据才会一次性写入磁盘。
这种方式减少了磁盘操作的次数,从而提高了程序的执行效率。
输入(读取文件):

当程序从文件中读取数据时,数据不是直接从磁盘读取到程序中,而是首先从磁盘读取到内存的缓冲区中。
一旦缓冲区被填满,程序再从缓冲区将数据逐一取出到程序的变量中。
这种方式避免了频繁的磁盘访问,提高了读取效率。
文件操作的特点

刷新缓冲区

虽然缓冲区提高了性能,但也可能引入一些问题,例如,程序在异常退出时可能导致缓冲区中的数据没有写入到磁盘。为了避免这种情况,可以使用 fflush() 函数手动将缓冲区的数据写入磁盘。这个操作确保缓冲区中的数据被立即写入文件。

缓冲区的大小
缓冲区的大小是由 C 编译器的实现来决定的。这个大小是固定的,并且通常是根据系统的配置来设定的。不同的编译器或操作系统可能会选择不同的缓冲区大小,这影响了读写操作的效率。

这里可以得出⼀个结论: 因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。如果不做,可能导致读写文件的问题。

当然如果想对缓冲区了解更多,可以学一下Linux操作系统,到那时候就会对缓冲区有更深的理解,在这里也只能说这么多了,再说就牵着到操作系统了。

目录
相关文章
|
5月前
|
人工智能 C语言
|
6月前
|
存储 小程序 C语言
【C语言程序设计——文件】文件操作(头歌实践教学平台习题)【合集】
本文介绍了C语言中的文件操作,分为两个关卡。第1关任务是将键盘输入的字符(以#结束)存入`file1.txt`并显示输出;第2关任务是从键盘输入若干行文本(每行不超过80个字符,用-1作为结束标志),写入`file2.txt`后再读取并显示。文中详细讲解了文件的打开、读取(使用`fgetc()`和`fgets()`)、写入(使用`fputc()`和`fputs()`)及关闭操作,并提供了示例代码和测试说明。
193 5
|
7月前
|
存储 程序员 C语言
【C语言】文件操作函数详解
C语言提供了一组标准库函数来处理文件操作,这些函数定义在 `<stdio.h>` 头文件中。文件操作包括文件的打开、读写、关闭以及文件属性的查询等。以下是常用文件操作函数的详细讲解,包括函数原型、参数说明、返回值说明、示例代码和表格汇总。
186 9
|
7月前
|
存储 数据管理 C语言
C 语言中的文件操作:数据持久化的关键桥梁
C语言中的文件操作是实现数据持久化的重要手段,通过 fopen、fclose、fread、fwrite 等函数,可以实现对文件的创建、读写和关闭,构建程序与外部数据存储之间的桥梁。
|
8月前
|
算法 C语言
C语言中的文件操作技巧,涵盖文件的打开与关闭、读取与写入、文件指针移动及注意事项
本文深入讲解了C语言中的文件操作技巧,涵盖文件的打开与关闭、读取与写入、文件指针移动及注意事项,通过实例演示了文件操作的基本流程,帮助读者掌握这一重要技能,提升程序开发能力。
512 3
|
C语言 C++ 编译器
C语言清空输入缓冲区
来源:http://blog.csdn.net/guanyasu/article/details/53153705 https://zhidao.baidu.com/question/5241738.html   C语言中如何清空输入输出缓冲区  上述描述似乎能够解决问题了,但是,fflush( )并不是标准C语言库函数,只是部分编译器自己实现的函数,是对标准C的扩展。
1650 0
|
7天前
|
安全 C语言
C语言中的字符、字符串及内存操作函数详细讲解
通过这些函数的正确使用,可以有效管理字符串和内存操作,它们是C语言编程中不可或缺的工具。
178 15
|
6月前
|
存储 算法 C语言
【C语言程序设计——函数】素数判定(头歌实践教学平台习题)【合集】
本内容介绍了编写一个判断素数的子函数的任务,涵盖循环控制与跳转语句、算术运算符(%)、以及素数的概念。任务要求在主函数中输入整数并输出是否为素数的信息。相关知识包括 `for` 和 `while` 循环、`break` 和 `continue` 语句、取余运算符 `%` 的使用及素数定义、分布规律和应用场景。编程要求根据提示补充代码,测试说明提供了输入输出示例,最后给出通关代码和测试结果。 任务核心:编写判断素数的子函数并在主函数中调用,涉及循环结构和条件判断。
320 23
|
5月前
|
人工智能 Java 程序员
一文彻底搞清楚C语言的函数
本文介绍C语言函数:函数是程序模块化的工具,由函数头和函数体组成,涵盖定义、调用、参数传递及声明等内容。值传递确保实参不受影响,函数声明增强代码可读性。君志所向,一往无前!
112 1
一文彻底搞清楚C语言的函数
|
6月前
|
算法 C语言
【C语言程序设计——函数】利用函数求解最大公约数和最小公倍数(头歌实践教学平台习题)【合集】
本文档介绍了如何编写两个子函数,分别求任意两个整数的最大公约数和最小公倍数。内容涵盖循环控制与跳转语句的使用、最大公约数的求法(包括辗转相除法和更相减损术),以及基于最大公约数求最小公倍数的方法。通过示例代码和测试说明,帮助读者理解和实现相关算法。最终提供了完整的通关代码及测试结果,确保编程任务的成功完成。
272 15
【C语言程序设计——函数】利用函数求解最大公约数和最小公倍数(头歌实践教学平台习题)【合集】