掌握C语言文件操作:从理论到实战指南

news2025/5/19 11:02:43

        文件操作是C语言编程中不可或缺的一部分,它使得程序能够持久化存储数据,并在需要时高效读写。本文将从基础概念到实战技巧,系统讲解C语言文件操作的核心知识点,并结合代码示例帮助读者深入理解。

一. 为什么需要文件操作?

        程序运行时,数据存储在内存中,一旦程序结束,内存数据就会被释放。文件操作解决了数据的持久化问题,例如:

        保存用户配置:如游戏的存档和设置。

        处理大规模数据:如日志文件或数据库的读写。

        跨进程通信:通过文件共享数据。

        二. 文件类型

        先补充文件和文件名的概念:

        文件:磁盘(硬盘)上的文件是文件。

        文件名:也称文件标识,例如C:\code\test.txt。由文件路径、文件名主干、文件后缀三部分组成,以便用户识别和引用。 

        在程序设计中,我们一般讲两种文件:程序文件数据文件(从文件功能的角度分类)。

1. 程序文件

        程序文件包含三种:

        源文件(.c):开发者编写的代码文件。

        目标文件(.obj):编译后的中间文件。

        可执行文件(.exe):链接后直接运行的程序。 

        2. 数据文件 

        文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。这就是数据文件,也是本章讨论的重点。

        数据文件包含两种:

        文本文件:如.txt、.csv,内容为ASCII字符。

        二进制文件:如图片、音频,内容为二进制数据。

        不同数据存储的数据文件类型不同,字符型数据一律以ASCII形式存储,数值型数据可以用ASCII形式存储,也可以使用二进制形式存储。

        例如,十进制整型10000,二进制为00000000 00000000 00100111 00010000。如果以ASCII码的形式输出到磁盘(文本文件),为“1” “0” “0” “0” “0”,磁盘中占用5个字节(每个字符1个字节);如果以二进制形式输出(二进制文件),为0x10,0x27,0x00,0x00,则在磁盘中只占4个字节

        测试代码:

#include <stdio.h>


int main()
{
    int a = 10000;
    FILE* pf = fopen("test.txt", "wb");//wb - write binary
    fwrite(&a, 4, 1, pf);//二进制的形式写到文件中

    fclose(pf);
    pf = NULL;

    return 0;
}

        这段代码创建了一个新文件,并将整数10000以二进制形式写入文件中。运行后调试控制台没有任何结果,但是编译器已经创建了一个二进制文件(.txt),可以通过添加现有项找到该二进制文件。

         我们通过选择打开方式为二进制编译器就能打开该二进制文件。

        文件内容:

        三. 流和标准流 

        程序的数据需要输出到各种外部设备,也需要从外部设备获取数据,不同的外部设备的输入输出操作各不相同。为了方便程序员对各种设备进行方便的操作,我们抽象出了流的概念。 

        1. 什么是流?

        在C语言中,流(stream)是一个抽象概念,表示程序与外部设备(如键盘、显示器、文件、网络等)之间数据传输的通道。

        可以将流想象成一条“数据河流”,数据在这条河中单向流动,因此有输入流也有输出流:

        输入流:数据从外部设备(如键盘、文件)流向程序。

        输出流:数据从程序流向外部设备(如显示器、文件)。

        2. 流的抽象意义 

        流的抽象意义有两点: 

        1. 统一接口:不同设备的操作方式差异巨大(例如键盘输入和文件读取),但流通过统一接口(如fgetc、fprintf)屏蔽了底层细节,程序员无需关心设备的具体实现。
        2. 缓冲机制:流通常与缓冲区(Buffer)结合使用。例如,数据从内存写入磁盘时,先暂存到缓冲区,缓冲区满后一次性写入,提升IO效率(计算机系统在进行输入/输出操作时的性能表现)。 

        3. 标准流 

        但是我们从键盘输入数据,向屏幕上输出,并没有打开流。是因为C语言程序启动时,默认打开三个预定义的流,称为标准流:

        1. stdin - 标准输入流,通常关联键盘输入,scanf函数就是从标准输入流中读取数据。

        2. stdout - 标准输出流,通常关联显示器输出,printf函数就是将信息输出到标准输出流中。

        3. stderr - 标准错误流,专用于输出错误信息,默认也关联显示器。

         标准流的特点: 

        1. 无需手动打开和关闭:程序启动时自动创建,结束时自动释放。

        2. 数据类型为FILE*(称为文件指针):C语言中,就是通过FILE*的文件指针来维护流的各种操作。 

         四. 文件指针

         每个被使用的文件,都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息,如文件的名字,文件状态以及文件当前的位置等。

        这些信息被保存在一个名为FILE的结构体变量中。该结构体是由系统声明的,如VS2013编译环境下提供的stdio.h头文件中有以下的文件类型声明:

struct _iobuf
{
    char *_ptr;
    int  _cnt;
    char *_base;
    int  _flag;
    int  _file;
    int  _charbuf;
    int  _bufsize;
    char *_tmpfname;
};

typedef struct _iobuf FILE;

        不同的编译器的FILE类型包含的内容不完全相同,但是大同小异。每当打开⼀个文件的时候,系统会根据文件的情况自动创建⼀个FILE结构的变量,并填充其中的信息,使用者不必关心细节。

        ⼀般都是通过⼀个FILE的指针来维护这个FILE结构的变量,这样使用起来更加方便。下面我们可以创建⼀个FILE*的指针变量: 

FILE* pf;//文件指针变量

        定义pf是⼀个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区(是⼀个结构体变量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够间接找到与它关联的文件。

        示意图:

        五. 文件操作核心函数 

        1. 打开与关闭文件 

        文件在读写之前应该先打开文件,在使用结束后应该关闭文件。ANSI C规定使用fopen函数来打开文件,fclose函数来关闭文件。函数原型如下:

        fopen打开文件,需指定文件路径(文件名,filename)和模式(mode)。打开文件失败时,会返回NULL

        比如:

#include <stdio.h>

int main()
{
    FILE* pf = fopen("test.txt", "r");//用只读的方式打开文件

    if (pf == NULL)
    {
        perror("fopen");
        return 1;
    }

    fclose(pf);
    pf = NULL;

    return 0;
}

        这里并不存在文件名为test.txt的文件,所以pf为NULL。运行结果:

        文件的打开模式有以下几种:

模式含义文件不存在时的行为
"r"(只读)为了读取数据,打开一个已经存在的文本文件出错
"w"(只写)为了写入数据,打开一个文本文件(写入会覆盖原有内容)建立一个新文件
"a"(追加)向文本文件尾添加数据建立一个新文件
"rb"(只读)为了读取数据,打开一个已经存在的二进制文件出错
"wb"(只写)为了写入数据,打开一个二进制文件建立一个新文件
"ab"(追加)向二进制文件尾添加数据建立一个新文件
"r+"(读写)为了读和写,打开一个文本文件出错
"w+"(读写)为了读和写,建立一个新的文本文件建立一个新文件
"a+"(读写)打开一个文本文件,在文件尾进行读写 建立一个新文件
"rb+"(读写)为了读和写,打开一个二进制文件出错
"wb+"(读写)为了读和写,建立一个新的二进制文件建立一个新文件
"ab+"(读写)打开一个二进制文件,在文件尾进行读写 建立一个新文件

        比如:

#include <stdio.h>

int main()
{
    int a = 10000;
    FILE* pf = fopen("test.txt", "w");//用只写的方式打开文件

    if (pf == NULL)
    {
        perror("fopen");
        return 1;
    }

    fclose(pf);
    pf = NULL;

    return 0;
}

        运行后就会发现,程序文件相同目录下生成了一个新文件,名为test.txt。

        默认在当前目录下读或写,也可以通过绝对路径和相对路径让代码按照指定路径读/写文件:

        绝对路径:从根目录开始,完整描述文件或目录位置的路径,以“ / ”或“ \ ” 分隔。例如,C:\User\Username\Documents\file.txt。

        相对路径:相对于当前工作目录或文件位置的路径。例如,当前路径是C:\User\Username\Documents,那么file.txt的相对路径可以是..\Picture\file.jpg,表示文件file.jpg位于当前目录的上一级目录Picture中( . 表示当前目录,.. 表示上一级目录,以此类推)。

        注意:代码中要连用两个反斜杠,表示一个反斜杠,防止它被解释为一个转义序列符。

        2. 顺序读写函数 

        当我们掌握了打开和关闭文件,就要来学习如何读写文件。

        顺序读写函数有以下几种:

函数名功能适用于
fgetc字符输入函数(读取单个字符)所有输入流
fputc字符输出函数(写入单个字符)所有输出流
fgets文本行输入函数(读取一行文本)所有输入流
fputs文本行输出函数(写入一行文本)所有输出流
fscanf格式化输入函数所有输入流
fprintf格式化输出函数所有输出流
fread二进制输入文件输入流
fwrite二进制输出文件输出流

        上面说的适用于所有输出/入流,一般指适用于标准输出/入流和其他输出/入流(如文件输出/入流) 。前六个函数是针对文本数据进行文件的输出和输出,最后两个是针对二进制数据进行文件的输出和出入。

        2.1 fputc和fgetc

       fputc函数的原型:

         作用:fputc函数将字符character(传递参数是字符的ACSII码值),写入stream流(指向的对应文件信息区的指针),并前进位置指示器(即光标)。

        返回值:如果写入成功,会返回该字符的ACSII码值;如果写入失败,会返回EOF

        例如:

#include <stdio.h>

int main()
{
	FILE* pf = fopen("data.txt", "w");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//写文件
	fputc('a', pf);	
	fputc('b', pf);
	fputc('c', pf);
	fputc('d', pf);
	//关闭文件
	fclose(pf);
	pf = NULL;
	return 0;
}

        这段代码将字符a、b、c、d写入data.txt中。运行之后就会发现,程序文件相同路径下生成了一个data.txt文本文件,打开后会有如下内容:

        当写入第一个字符a时,光标就会移动到a的后面,随后写入字符b,再次移动光标,以此类推。 

       fgetc函数的原型:

        作用:fgetc函数从流中获取字符,并前进光标。

        返回值:如果获取成功,会返回该字符的ACSII码值(int类型);如果获取字符失败,会返回EOF。 

        例如:

#include <stdio.h>

int main()
{
	FILE* pf = fopen("data.txt", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//读文件(文件原有字符串“abcd”)
	int ch = fgetc(pf);
	printf("%c\n", ch);
	ch = fgetc(pf);
	printf("%c\n", ch);
	ch = fgetc(pf);
	printf("%c\n", ch);
	ch = fgetc(pf);
	printf("%c\n", ch);

    //也可以通过fgetc的返回值读取文件中所有字符:
    //while ((int ch = fgetc(pf)) != EOF)
    //{
    //	printf("%c", ch);
    //}
	//关闭文件
	fclose(pf);
	pf = NULL;
	return 0;
}

        这段代码从data.txt中读取了前四个字符,并依次打印。运行结果:

         当读取第一个字符a时,光标就会移动到a的后面,随后写读取字符b,再次移动光标,以此类推。 

        再来看一个例子:

#include <stdio.h>

int main()
{
	int ch = fgetc(stdin);
	putchar(ch);//打印一个字符,相当于printf("%c", )
    //getchar -- 读取一个字符,相当于scanf("%c", )
	return 0;
}

        fgetc函数从标准输入流stdin中获取字符,putchar函数再将字符输出。运行后会发现,控制台窗口没有输出任何数据,光标停在首位。因为此时标准输入流中没有数据,我们可以通过键盘输入字符,这个字符就会进入标准输入流,并被获取和打印。运行结果:

        这说明,fgetc函数适用于所有输入流,同样的也可以证明fputc函数适用于所有输出流,代码如下:

#include <stdio.h>

int main()
{
	int ch = fgetc(stdin);
	fputc(ch, stdout);

	return 0;
}

        我们通过键盘输入一个字符,对应的就会打印这个字符。运行结果:

         2.2 fputs和fgets

        fputs函数的原型:

        作用:fputs函数将指针str指向的字符串写入流中。

        返回值:如果写入成功,会返回一个非负值(non-negative value);如果写入失败,会返回EOF。 fputs函数将字符串写入流中时,遇到“ \0 ”结束写入。

        例如:

#include <stdio.h>

int main()
{
	FILE* pf = fopen("data.txt", "w");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//写文件
	//写入一行字符
	fputs("How are you?\n", pf);
	fputs("abcdefg\n", pf);
	//关闭文件
	fclose(pf);
	pf = NULL;
	return 0;
}

        这里就会发现,data.txt多出了很多内容:

        注意:

        1. 写入字符串含“\n”,写入文件时就会换行。

        2. 我们会发现,原本的“abcd”已经不见了,这就是“w”只写模式的特点:写入会覆盖原有内容。 

        fgets函数的原型:

        作用:从流中读取字符,并将其作为字符串储存到str中,直到读取(num-1)个字符或者到达换行符或文件结束符(end-of-file)为止(以先发生的为准),并移动光标至读取字符的后面 。当换行符使fgets停止读取时,换行符仍被函数认为是一个有效字符,并包含在复制到str的字符串中。

        返回值:如果成功,该函数返回str;如果发生读取失败,则返回的指针是空指针

        例如:

#include <stdio.h>

int main()
{
	FILE* pf = fopen("data.txt", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//读文件,data.txt中有字符串“abcdefghijk”
	//读取多个字符
	char ch[10];
	fgets(ch, 10, pf);

	//关闭文件
	fclose(pf);
	pf = NULL;
	return 0;
}

        调试并打开监视:

        这里说明,函数参数num为10时,实际上是只读取了9个有效字符,和一个“\0”。把数组大小和num改为20,再调试并打开监视: 

        fgets读取完所有字符后就不会再读取。

        再举一个例子:

#include <stdio.h>

int main()
{
	FILE* pf = fopen("data.txt", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//读文件
	//data.txt中有:
	//hello world
	//hahaha
	char ch[20];
	fgets(ch, 20, pf);
	printf("%s", ch);
	//关闭文件
	fclose(pf);
	pf = NULL;
	return 0;
}

        运行结果:

        并没有读取到hahaha。但是调试并打开监视:

        这里ch储存了转行符,所以只要再读取一次就能打印hahaha:

        2.3 fprintf和fscanf

        fprintf函数的原型:

 

        fprintf函数将具有一定格式的数据写入流中。函数参数后面有省略号,这被称为可变参数。printf函数的参数中也存在可变参数,例如:

printf("%d",10);
printf("hello");
printf("%d %s",10,:"hello");

        参数的类型和数量都不同,所以函数参数用可变参数代替。fprintf的使用和printf非常相似,只是fprintf的函数参数比printf多了一个文件指针类型的流,因此使用fprintf函数并不困难,例如:

#include <stdio.h>

int main()
{
	int age = 18;
	char name[20] = "zhangsan";
	double grades = 95.5;

	FILE* pf = fopen("data.txt", "w");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//写文件
	fprintf(pf, "%d %s %.2lf", age, name, grades);
	//关闭文件
	fclose(pf);
	pf = NULL;
	return 0;
}

        运行结果:

        能够按一定格式输出,也就能按一定格式输入,fscanf函数就能从文件中获取数据。

        fscanf函数的原型如下:

        fscanf函数和scanf函数的参数相似(fscanf函数的参数多了“FILE* stream”),使用方法也很相似,例如:

#include <stdio.h>

int main()
{
	int age;
	char name[20];
	double grades;

	FILE* pf = fopen("data.txt", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//读文件
    //文件原有:
    //18 zhangsan 99.50
    //scanf("%d %s %lf", &age, name, &grades);--再加上流就是fscanf函数:
	fscanf(pf, "%d %s %lf", &age, name, &grades);
	//关闭文件
	fclose(pf);
	pf = NULL;
	return 0;
}

        运行结果:

         2.4 fwrite和fread

        fwrite函数的原型:

    

        fwrite函数将数据以二进制形式写入流中。该函数有以下参数:

        1. const void* ptr: ptr指向被写的数据。

        2. size_t size:被写的数据中一个元素的长度(单位是字节)。

        3. size_t count:元素的个数。

        4. FILE* stream:写入数据到stream流。

        例如,将一个整型数组数据以二进制形式写入data.txt文件中,代码如下:

#include <stdio.h>

int main()
{
	FILE* pf = fopen("data.txt", "wb");//以二进制写的方式打开文件
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//写文件
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };

	fwrite(arr, sizeof(int), 10, pf);
	//关闭文件
	fclose(pf);
	pf = NULL;
	return 0;
}

        运行后打开data.txt,会发现写入了数据,但是并不能看出来是10个数字:

        用文本文件的方式打开二进制文件,数据就会变为乱码,但实际上数组中的十个整型数据已经写入了data.txt。 

        我们可以用fread函数读取数据验证想法,fread函数的原型如下:

        fread函数从流中读取二进制数据。该函数的四个参数和fwrite函数一样。例如:

#include <stdio.h>

int main()
{
	FILE* pf = fopen("data.txt", "rb");//以二进制只读的方式打开文件
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//写文件
	int arr[10] = {0};
	fread(arr, sizeof(int), 10, pf);

	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", arr[i]);
	}
	//关闭文件
	fclose(pf);
	pf = NULL;
	return 0;
}

        运行结果:

        说明fread函数成功从data.txt中读取这十个数字,原本文件中确实储存着这十个数字。 

        如果让fread函数读取文本文件,就不会读取到正确数据。例如,我们在data.txt中写入十进制整型1到10,再用相同代码读取数据,运行结果:

        读取数据明显不正确。所以要注意文本文件和二进制文件的区别,以及对应的数据之间的区别。

        3. sscanf和sprintf

        这里补充sscanfsprintf函数。

        我们介绍过printf、fprintf、scanf和fscanf函数:

        scanf -- 针对stdin的格式化的输入函数

        printf -- 针对stdout的格式化的输出函数

        fscanf -- 针对所有输入流 格式化的输入函数

        fprintf -- 针对所有输出流的格式化的输出函数

        sscanf和sprintf函数能够实现有格式的数据与字符串之间的转换

        sscanf -- 从字符串中,按照格式提取格式化的数据

        sprintf -- 将带有格式的数据,按照格式转化成字符串

        sprintf函数的原型:

        sprintf函数写格式化的数据到str指向的字符串中,也就是将格式化的数据转换成字符串。例如:

#include <stdio.h>

int main()
{
	int age = 18;
	char name[20] = "zhangsan";
	double grades = 95.5;

	char buf[120] = { 0 };
    //printf("%d %s %.1lf", age, name, grades)
	sprintf(buf, "%d %s %.1lf", age, name, grades);
	printf("%s\n", buf);
	return 0;
}

        运行结果:

        sprintf函数将age、name和grades这三个不同类型的变量转换成了字符串并写入buf指向的字符串中。 

        sscanf函数的原型:

        sscanf函数和sprintf函数的作用相反,sscanf函数从字符串中读取有格式的数据。例如:

#include <stdio.h>

int main()
{
	int age = 18;
	char name[20] = "zhangsan";
	double grades = 95.5;

	char buf[120] = { 0 };
	sprintf(buf, "%d %s %.1f", age, name, grades);

	int age2 = 0;
	char name2[20] = "";
	double grades2 = 0;
	
	//scanf("%d %s %lf", &age2, &name2, &grades2);
	sscanf(buf, "%d %s %lf", &age2, &name2, &grades2);
	printf("%d\n", age2);
	printf("%s\n", name2);
	printf("%.1lf\n", grades2);
	return 0;

        运行结果:

        sscanf函数从buf指向的字符串中,按%d %s %lf的格式顺序读取数据,并分别存储在变量中。 

4. 文件的随机读写

        文件的读写,既支持顺序读写,也支持随机读写。比如文件中有“abcdef”,假设此时光标默认在f的右边,我们可以通过一些函数让光标移动到e的左侧,以此为起始位置进行读写,这就是文件的随机读写。 

        4.1 fseek 

        fseek函数的原型:

        fseek函数可以根据文件指针的位置和偏移量来重新定位文件指针(即文件内容的光标)。该函数有三个参数:

        1. FILE* stream:指向文件的流。

        2. long int offset:相对于起始位置origin的偏移量(单位字节),向右偏移是正数,向左偏移是负数。

        3. int origin:origin有三种情况:SEEK_SET(文件的起始处),SEEK_CUR(光标的当前位置),SEEK_END(文件的末尾处)。

        例如,文件中有“abcdefghi” :

int ch = fgetc(pf);
printf("%c\n",ch);
ch = fgetc(pf);
printf("%c\n",ch);

        这样就会打印a和b两个字符。如果想要读取a后立即读取e,就可以用fseek函数重新定位光标(光标应该在e的左侧):

fseek(pf,4,SEEK_SET);//从文件起始处,偏移量为4的地方
fseek(pf,3,SEEK_CUR);//从光标的当前位置(即a的右侧),偏移量为3的地方
fseek(pf,-5,SEEK_END);//从文件末尾处,偏移量为-5(向左偏移,为负数)的地方

        这三种写法都是定位光标在e的左侧。完整代码:

#include <stdio.h>

int main()
{
	int age;
	char name[20];
	double grades;

	FILE* pf = fopen("data.txt", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//读文件
	int ch = fgetc(pf);
	printf("%c\n", ch);

	fseek(pf, 4, SEEK_SET);
	//fseek(pf, 3, SEEK_CUR);
	//fseek(pf, -5, SEEK_END);

	ch = fgetc(pf);
	printf("%c\n", ch);
	//关闭文件
	fclose(pf);
	pf = NULL;
	return 0;
}

        运行结果:

        4.2 ftell 

        ftell函数的原型:

        ftell函数会返回文件指针相对于起始位置的偏移量(返回类型为long int)。 

        例如:

#include <stdio.h>

int main()
{
	int age;
	char name[20];
	double grades;

	FILE* pf = fopen("data.txt", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//读文件
    //文件原有内容:abcdefghi
	int ch = fgetc(pf);
	printf("%c\n", ch);

	fseek(pf, 4, SEEK_SET);

	ch = fgetc(pf);
	printf("%c\n", ch);

	printf("%ld\n", ftell(pf));
	//关闭文件
	fclose(pf);
	pf = NULL;
	return 0;
}

        当第二次fgetc函数运行后,光标指在e和f之间,与起始位置的偏移量为5。 

        运行结果:

4.3 rewind 

        rewind函数的原型:

        rewind函数可以让文件指针的位置回到文件的起始位置

        例如:

#include <stdio.h>

int main()
{
	int age;
	char name[20];
	double grades;

	FILE* pf = fopen("data.txt", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}

	//读文件
	int ch = fgetc(pf);
	printf("%c\n", ch);

	fseek(pf, 4, SEEK_SET);

	ch = fgetc(pf);
	printf("%c\n", ch);

	rewind(pf);//光标再次回到起始位置

	ch = fgetc(pf);//此时读取的就是字符a
	printf("%c\n", ch);
	//关闭文件
	fclose(pf);
	pf = NULL;
	return 0;
}

        第二次fgetc后,rewind函数让光标再次指向了初始位置,则第三次fgetc函数读取到的字符仍然是字符a。

运行结果:

        以上就是文件的随机读写中最重要的三个函数。 

六. 文件读取结束的判定

        在读取文件时,我们可以利用“文件是否读取结束”这一信息判断是否继续读取文件。

        例如:

#include <stdio.h>

int main()
{
	int age;
	char name[20];
	double grades;

	FILE* pf = fopen("data.txt", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}

	//读文件
    //文件有:"abcdefghi"
	int ch = 0;
	while ((ch = fgetc(pf)) != EOF)
	{
		printf("%c\n", ch);
	}
	//关闭文件
	fclose(pf);
	pf = NULL;
	return 0;
}

        运行结果:

        这里就是循环判断fgetc函数的返回值是否为EOF(文件结束标志),如果返回值不是EOF,说明文件还可以正常读取,代码就会继续读取并打印数据;如果返回值是EOF,说明文件已经读取结束,跳出循环。

        判定文件读取结束,一般都是判断函数的返回值:

        1.文本文件(EOF/NULL):

                 fgetc(函数返回数据的ASCII码值):判断返回值是否为EOF

                 fgets(函数返回str):判断返回值是否为NULL

        2. 二进制文件(返回值是否小于实际要读的个数):

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

        文件读取结束有两种情况:

        1. 正常读取:遇到文件末尾而结束。

        2. 异常读取:发生读取错误而结束。

        想要知道是哪种情况导致文件读取结束,就需要用到feof函数和ferror函数:

        int feof ( FILE * stream ):如果文件读取时遇到文件末尾,则返回非0的整型。

        int ferror ( FILE * stream ):如果文件读取时发生错误,则返回非0的整型。

        例如(文本文件):

int main(void)
{
    	FILE* pf = fopen("data.txt", "r");
    	if (pf == NULL)
    	{
    		perror("fopen");
    		return 1;
    	}
    //fgetc 当读取失败的时候或者遇到文件结束的时候,都会返回EOF
    int c; // 注意:int,非char,要求处理EOF
    while ((c = fgetc(pf)) != EOF) //I/O读取文件循环
    {
        putchar(c);
    }
    //判断是什么原因结束的
    if (ferror(pf))
        puts("I/O error when reading");
    else if (feof(pf))
        puts("End of file reached successfully");

    fclose(pf);
    pf = NULL;
}

        运行结果:

        根据返回结果,文件读取结束是因为到达文件末尾。

        例如(二进制文件): 

#include <stdio.h>

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); // 写 double 的数组
	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 { // error handling
		if (feof(fp))
			printf("Error reading test.bin: unexpected end of file\n");
		else if (ferror(fp)) {
			perror("Error reading test.bin");
		}
	}
	
	fclose(fp);
}

        这里就是判断fread函数的返回值(ret_code)是否小于实际要读的个数,如果不小于,代码接着打印数据, 如果小于,代码就不会再打印数据,而是用feof函数和ferror函数检测错误情况。

        运行结果:

        由此可知,在文件读取过程中,不能用feof函数和ferror函数的返回值直接来判断文件是否结束,正确方法应该是判断各种文件顺序写函数的返回值

七. 文件缓冲区

        ANSI C 标准采用“缓冲文件系统” 处理数据文件。

        缓冲文件系统是指系统自动地在内存中为程序中每⼀个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才⼀起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。

        缓冲区的大小根据C编译系统决定的。 

        示意图:

        例如:

#include <stdio.h>
#include <windows.h>
//VS2022 WIN11环境测试
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;
}

        根据代码指示在不同时间段打开text.txt文件,就会发现文件内容的变化。

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

八. 总结

        文件操作是C语言中实现数据持久化的核心技能。通过本文的学习,我们了解到文件的打开、读写、随机访问及错误处理等关键操作。 

        掌握文件操作,让你的程序真正“记住”数据!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2332819.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

在 Linux 上安装 MongoDB Shell

1. 下载 MongoDB Shell Download | MongoDB wget https://downloads.mongodb.com/compass/mongosh-2.5.0-linux-x64.tgz 2. tar -zxvf mongosh-2.5.0-linux-x64.tgz 3. copy 命令 sudo cp mongosh /usr/local/bin/ sudo cp mongosh_crypt_v1.so /usr/local/lib/ 4. …

数据结构-复杂度详解

前言&#xff1a;大家好&#xff01;本文带来的是数据结构-复杂度的讲解&#xff0c;一起来看看吧&#xff01; 1.算法的时间复杂度和空间复杂度 1.1算法的效率 复杂度&#xff1a;衡量一个算法的好坏&#xff08;效率&#xff09;&#xff0c;从两个维度衡量&#xff0c;时…

安宝特新闻丨Vuzix Core™波导助力AR,视角可调、高效传输,优化开发流程

Vuzix Core™ 光波导技术 近期&#xff0c;Vuzix Core™光波导技术赋能AR新视界&#xff01;该系列镜片支持定制化宽高比调节及20至40视场角范围&#xff0c;可灵活适配各类显示引擎。通过创新的衍射光波导架构&#xff0c;Vuzix Core™实现了光学传输效率与图像质量的双重突破…

【SQL】常见SQL 行列转换的方法汇总 - 精华版

【SQL】常见SQL 行列转换的方法汇总 - 精华版 一、引言二、SQL常见的行列转换对比1. 行转列 Pivoting1.1 ​​CASE WHEN 聚合函数​​1.2 ​​IF 聚合函数​​1.3 ​​PIVOT操作符​​ 2.列转行 Unpivoting2.1 UNION ALL​​2.2 ​​EXPLODE函数&#xff08;Hive/Spark&#…

【原创】vue-element-admin-plus完成确认密码功能,并实时获取Form中表单字段中的值

前言 我第一句就想说&#xff1a;vue-element-admin-plus真是个大坑货&#xff01;就一个确认密码功能都值得我单开一页博客来讲这么一个简单的功能 布局和代码 布局如图所示&#xff0c;我需要密码和确认密码&#xff0c;确认密码需要和密码中的内容一致&#xff0c;不然会返…

MySQL---数据库基础

1.数据库概念 文件保存数据有以下几个缺点&#xff1a; 文件的安全性问题 文件不利于数据查询和管理 文件不利于存储海量数据 文件在程序中控制不方便 数据库存储介质&#xff1a; 1.磁盘 2.内存 为了解决上述问题&#xff0c;设计出更加利于管理数据的东西 —— 数据库。…

leetcode68.左右文本对齐

思路源自 leetcode-字符串篇 68题 文本左右对齐 难度高的模拟类型题目&#xff0c;关键点在于事先知道有多少单词要放在本行并且还要知道本行是不是最后一行&#xff08;最后一行需要全部单空格右对齐&#xff0c;不是最后一行就空格均摊&#xff09;&#xff0c;非最后一行的空…

若依微服务集成Flowable仿钉钉工作流

项目简介 本项目工作流模块集成在若依项目单独一个模块&#xff0c;可实现单独运行部署&#xff0c; 前端采用微前端&#xff0c;嵌入在若依的前端项目中。因博主是后端开发&#xff0c;对前端不是太属性&#xff0c;没将工作流模块前端代码移到若依前端。下面贴上代码工程结构…

MySQL 架构设计:数据库的“城市规划指南“

就像一座完美城市需要精心的规划才能高效运行&#xff0c;一个优秀的 MySQL 系统也需要精心的架构设计才能支撑业务的发展…让我们一起探索 MySQL 的"城市规划"&#xff0c;学习如何设计一个既高效又稳定的数据库王国&#xff01; 什么是 MySQL 架构设计&#xff1f…

【从0到1学MybatisPlus】MybatisPlus入门

Mybatis-Plus 使用场景 大家在日常开发中应该能发现&#xff0c;单表的CRUD功能代码重复度很高&#xff0c;也没有什么难度。而这部分代码量往往比较大&#xff0c;开发起来比较费时。 因此&#xff0c;目前企业中都会使用一些组件来简化或省略单表的CRUD开发工作。目前在国…

依靠视频设备轨迹回放平台EasyCVR构建视频监控,为幼教连锁园区安全护航

一、项目背景 幼教行业连锁化发展态势越发明显。在此趋势下&#xff0c;幼儿园管理者对于深入了解园内日常教学与生活情况的需求愈发紧迫&#xff0c;将这些数据作为提升管理水平、优化教育服务的重要依据。同时&#xff0c;安装监控系统不仅有效缓解家长对孩子在校安全与生活…

HOW - React Developer Tools 调试器

目录 React Developer Tools使用Components 功能特性1. 查看和编辑 props/state/hooks2. 查找组件3. 检查组件树4. 打印组件信息5. 检查子组件 Profiler 功能特性Commit ChartFlame Chart 火焰图Ranked Chart 排名图 why-did-you-render 参考文档&#xff1a; React调试利器&a…

STM32F103C8T6单片机开发:简单说说单片机的外部GPIO中断(标准库)

目录 前言 如何使用STM32F1系列的标准库完成外部中断的抽象 初始化我们的GPIO为输入的一个模式 初识GPIO复用&#xff0c;开启GPIO的复用功能时钟 GPIO_EXTILineConfig和EXTI_Init配置外部中断参数 插入一个小知识——如何正确的配置结构体&#xff1f; 初始化中断&#…

docker的安装使用0废话版本自学软硬件工程师778天

见字如面&#xff0c; 这里是AIGC创意人_竹相左边 上一篇 因为 自己开发客户系统&#xff0c;为了解决一键启动 前端后端&#xff0c;涉及到了docker-compose 在新的电脑上安装docker 有各种问题这里再次记录下&#xff0c;既是笔记也是分享。 我先用自己的话说一遍&#xff0…

探秘 Svelte+Vite+TS+Melt - UI 框架搭建,开启高效开发

框架太“重”了&#xff1a;通常一个小型项目只由少数几个简单页面构成&#xff0c;如果使用 Vue 或者 React 这些框架来研发的话&#xff0c;有点“大材小用”了。构建的产物中包含了不少框架运行时代码(虚拟 DOM、响应式、状态管理等)&#xff0c;这些代码对于小型项目而言是…

vscode 连不上 Ubuntu 18 server 的解决方案

下载 vscode 历史版本 18.5&#xff08;windows请装在 系统盘 C 盘&#xff09; 打开 vdcode&#xff0c;将 自动更新 设置为 None &#xff08;很关键&#xff0c;否则容易前功尽弃&#xff09; 重命名&#xff08;删除&#xff09; 服务器上的 .vscode-server 文件夹 重新…

云原生运维在 2025 年的发展蓝图

随着云计算技术的不断发展和普及&#xff0c;云原生已经成为了现代应用开发和运维的主流趋势。云原生运维是指在云原生环境下&#xff0c;对应用进行部署、监控、管理和优化的过程。在 2025 年&#xff0c;云原生运维将迎来更加广阔的发展前景&#xff0c;同时也将面临着一系列…

Redis进阶--哨兵

目录 一、引言 二、介绍 三、哨兵的核心功能 四、使用docker进行单个主机redis主从复制哨兵操作 五、哨兵重新选取主节点的流程 1.主观下线 2.客观下线 3.主节点挂了 4.挑选新主节点 六、总结 一、引言 如果主从复制中&#xff0c;主节点挂了&#xff0c;从节点会迷茫…

Franka 机器人x Dexterity Gen引领遥操作精细任务新时代

教授机器人工具灵活操作难题 在教授机器人灵活使用工具方面&#xff0c;目前主要有两种策略&#xff1a;一是人类遥控&#xff08;用于模仿学习&#xff09;&#xff0c;二是模拟到现实的强化学习。然而&#xff0c;这两种方法均存在明显的局限性。 1、人类遥控&#xff08;用…

专题|MATLAB-R语言Logistic逻辑回归增长模型在互联网金融共生及移动通信客户流失分析实例合集

全文链接&#xff1a;https://tecdat.cn/?p41343 作为数据科学家&#xff0c;我们始终关注如何通过模型创新揭示复杂系统的动态规律。本专题合集聚焦两大核心应用场景&#xff0c;通过 Logistic 增长模型与逻辑回归技术&#xff0c;为互联网金融共生演化与移动通信客户流失预警…