C语言学习之文件操作

发布于:2025-05-15 ⋅ 阅读:(16) ⋅ 点赞:(0)

        经过前面的学习,我们已经基本掌握了如何去写一个C语言的代码了。但是在实际的项目中,我们不可能不需要文件去操作。因为如果没有文件,我们写的程序是存储在电脑的内存中的。如果程序推出,内存回收数据就随之丢失了。如果我们要对数据进行永久性的保存,我们就需要使用文件。

        接下来我们就文件的操作进行学习

目录

什么是文件

      程序文件

      数据文件   

        文件名

二进制文件和文本文件

科普:绝对路径和相对路径:

1. 绝对路径

2. 相对路径

3. 关键区别对比

4. 实际编程注意事项

5. 示例场景

文件的打开与关闭

        流和标准流

        流

        标准流

        文件指针

        文件的打开与关闭 

        文件的顺序读写

对比两组函数scanf/fscanf/sscanf和printf/fprintf/sprintf

对比表格

1. 功能与数据流方向

3. 安全性问题

2. 参数差异

4. 典型应用场景

5.典型应用场景

文件的随机读写

        fseek函数

        ftell函数

        rewind函数

   文件读取结束的判定

        错误使用的feof     

        文件缓冲区


什么是文件

        文件是磁盘(磁盘)的文件

        但是在程序设计上,我们一般谈及的文件分为两种:程序文件、数据文件(文件功能角度来区分)。

      程序文件

        程序文件包括程序文件(后缀为.c),目标文件(Windows环境下后缀为.obj),可执行程序(Windows环境下后缀为.exe)

      数据文件   

        文件内容不一定是程序,而是程序运行时读写的数据,如程序运行时需要从中读取数据的文件,或者输出内容的文件
        本篇中讨论的是数据⽂件。

        在之前的学习中所处理数据的输⼊输出都是以终端为对象的,即从终端的键盘输⼊数据,运⾏结果显⽰到显⽰器上。其实有时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使⽤,这⾥处理的就是磁盘上⽂件。

        文件名

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

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

        如:D:\VS2022\c-language-practice\test.c

        为了方便起见,文件标识就是文件名

二进制文件和文本文件

        根据数据的组织形式,数据⽂件被称为⽂本⽂件和⼆进制⽂件。

        数据在内存中以⼆进制的形式存储,如果不加转换的输出到外存的⽂件中,就是⼆进制⽂件。

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

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

        字符⼀律以ASCII形式存储数值型数据既可以⽤ASCII形式存储,也可以使⽤⼆进制形式存储。如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占⽤5个字节(每个字符⼀个字节),⽽⼆进制形式输出,则在磁盘上只占4个字节。

        测试代码:

#include<stdio.h>
int main()
{
	int a = 100;
	FILE* fp = fopen("test.txt", "wb");
	fwrite(&a,4,1,fp);//二进制写入文件中
	fclose(fp);
	fp = NULL;
	return 0;
}

运行之后是这个样子,也就是什么都没有

通过这里打开可以发现

        

生成了一个txt的文本文件

 这个文件在VS内是无法打开的

        但是我们可以在VS中打开它(虽然实际意义不是很大)

显示结果为:

科普:绝对路径和相对路径:

1. 绝对路径

定义

根目录开始的完整路径,明确指向文件或目录的位置。

  • Windows示例C:\\Users\\Name\\Documents\\file.txt

  • Linux/macOS示例/home/name/Documents/file.txt

特点

FILE* fp = fopen("data/input.csv", "r");  // 当前目录下的data子目录
// 或
FILE* fp = fopen("../logs/error.log", "w"); // 上级目录的logs子目录

2. 相对路径

定义

相对于程序当前工作目录的路径,不包含根目录。

  • 示例

    • data/file.txt:当前目录下的data子目录中的文件。

    • ../config/settings.ini:上级目录的config子目录中的文件。

特点

  • 灵活性:路径随程序的工作目录变化而变化。

  • 简洁性:路径较短,便于维护。

  • 符号说明

    • .:当前目录(可省略)。

    • ..:上级目录。

C语言中的使用

FILE* fp = fopen("data/input.csv", "r");  // 当前目录下的data子目录
// 或
FILE* fp = fopen("../logs/error.log", "w"); // 上级目录的logs子目录

适用场景

  • 项目内部资源文件(如配置文件、数据文件)。

  • 需要跨平台移植的代码。

缺点

  • 依赖工作目录:若程序运行时工作目录改变,路径可能失效。

  • 调试困难:路径错误可能导致文件找不到,需检查当前目录。

3. 关键区别对比

对比项 绝对路径 相对路径
路径起点 根目录(如 C:\\ 或 / 程序当前工作目录
唯一性 唯一确定文件位置 随工作目录变化
可移植性 低(依赖系统目录结构) 高(路径与项目结构相关)
典型使用场景 系统级文件访问(如日志、全局配置) 项目内部资源访问(如数据文件、模块配置)
路径长度
安全性 可能暴露敏感路径 更安全(隐藏具体系统路径)

4. 实际编程注意事项

(1) 路径分隔符

  • Windows:默认使用反斜杠 \,但在C字符串中需转义为 \\,或统一使用正斜杠 /(兼容)。

  • Linux/macOS:使用正斜杠 /

  • 跨平台写法

    // 统一使用正斜杠
    FILE* fp = fopen("data/files/image.png", "rb");

    (2) 获取当前工作目录

  • C标准库函数getcwd(需包含 <unistd.h> 或 <direct.h>)。

    #include <unistd.h>
    char cwd[256];
    getcwd(cwd, sizeof(cwd)); // 获取当前工作目录
    printf("当前目录:%s\n", cwd);

    (3) 路径拼接

  • 手动拼接(不推荐):

    char path[256];
    snprintf(path, sizeof(path), "%s/%s", base_dir, filename);
  • 使用库函数(推荐):

    • C17引入的 std::filesystem(C++),但C语言需依赖平台API(如Windows的 PathCombine)。

(4) 错误处理

  • 检查文件是否成功打开

FILE* fp = fopen("data.txt", "r");
if (fp == NULL) {
    perror("文件打开失败"); // 输出:文件打开失败: No such file or directory
    return 1;
}

5. 示例场景

场景1:读取项目内的配置文件

  • 相对路径config/settings.ini

    FILE* fp = fopen("config/settings.ini", "r");

    场景2:写入系统日志文件

  • 绝对路径(Linux):

    FILE* fp = fopen("/var/log/myapp/error.log", "a");

    总结

  • 绝对路径:精准但不可移植,适合访问系统级固定文件。

  • 相对路径:灵活且可移植,适合项目内部资源管理。

  • 编程建议:优先使用相对路径,结合环境变量或配置文件动态设置路径,提升代码可维护性和跨平台能力。

    文件的打开与关闭

            流和标准流

            流

            我们程序的数据需要输出到各种外部设备,也需要从外部设备获取数据,不同的外部设备的输⼊输出操作各不相同,为了⽅便程序员对各种设备进⾏⽅便的操作,我们抽象出了流的概念,我们可以把流想象成流淌着字符的河。

            C程序针对⽂件、画⾯、键盘等的数据输⼊输出操作都是通过流操作的。

            ⼀般情况下,我们要想向流⾥写数据,或者从流中读取数据,都是要打开流,然后操作。

            标准流

            那为什么我们从键盘输⼊数据,向屏幕上输出数据,并没有打开流呢?那是因为C语⾔程序在启动的时候,默认打开了3个流:

    • stdin - 标准输⼊流,在⼤多数的环境中从键盘输⼊,scanf函数就是从标准输⼊流中读取数据。

    • stdout - 标准输出流,⼤多数的环境中输出⾄显⽰器界⾯,printf函数就是将信息输出到标准输出流中。

    • stderr - 标准错误流,⼤多数环境中输出到显⽰器界⾯。

            这是默认打开了这三个流,我们使⽤scanf、printf等函数就可以直接进⾏输⼊输出操作的。stdin、stdout、stderr 三个流的类型是: FILE * ,通常称为⽂件指针。

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

            那么什么是文件指针呢,接下来我们来学习以下

            文件指针

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

            每个被使⽤的⽂件都在内存中开辟了⼀个相应的⽂件信息区,⽤来存放⽂件的相关信息(如⽂件的名字,⽂件状态及⽂件当前的位置等)。这些信息是保存在⼀个结构体变量中的。该结构体类型是由系统声明的,取名 FILE.

            不同C编译器的FILE类型包含的内容不完全相同,但是大同小异

            每当打开⼀个⽂件的时候,系统会根据⽂件的情况⾃动创建⼀个FILE结构的变量,并填充其中的信息,使⽤者不必关⼼细节。

            ⼀般都是通过⼀个FILE的指针来维护这个FILE结构的变量,这样使⽤起来更加⽅便。

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

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

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

            ⽐如:

            文件的打开与关闭 

            ⽂件在读写之前应该先打开⽂件,在使⽤结束之后应该关闭⽂件

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

            ANSI C 规定使⽤ fopen 函数来打开⽂件, fclose函数 来关闭⽂件。

    //打开文件
    FILE*fopen(const char *filename,const char*mode);
    //关闭文件
    FILE*fclose(FILE*stream);

            mode表示文件打开模式,下面的是所有的文件打开模式

    文件打开模式 含义 文件不存在时的行为
    "r" 只读模式打开文本文件 返回NULL,打开失败
    "rb" 只读模式打开二进制文件 返回NULL,打开失败
    "w" 写入模式打开文本文件,清空文件内容(若存在) 创建新文件
    "wb" 写入模式打开二进制文件,清空文件内容(若存在) 创建新文件
    "a" 追加模式打开文本文件,写入从文件末尾开始 创建新文件
    "ab" 追加模式打开二进制文件,写入从文件末尾开始 创建新文件
    "r+" 读写模式打开文本文件(文件必须存在) 返回NULL,打开失败
    "rb+" / "r+b" 读写模式打开二进制文件(文件必须存在) 返回NULL,打开失败
    "w+" 读写模式打开文本文件,清空文件内容(若存在) 创建新文件
    "wb+" / "w+b" 读写模式打开二进制文件,清空文件内容(若存在) 创建新文件
    "a+" 读写模式打开文本文件,写入从文件末尾开始,读取从头开始 创建新文件
    "ab+" / "a+b" 读写模式打开二进制文件,写入从文件末尾开始,读取从头开始 创建新文件
    "wx" (C11) 独占写入模式打开文本文件(若文件存在则失败) 创建新文件(若存在则失败,返回NULL
    "wbx" (C11) 独占写入模式打开二进制文件(若文件存在则失败) 创建新文件(若存在则失败,返回NULL

            举例说明:

    #include<stdio.h>
    int main()
    {
    	//fopen()函数用于打开文件,返回一个FILE*指针
    	// 如果成功打开文件,则返回文件信息区的指针
    	// 如果打开失败,则返回NULL。
    	FILE* fp=fopen("D:\\VS2022\\c-language-practice\\文件操作学习\\test.txt","r");
    	if(fp==NULL)
    	{
    		perror("fopen");
    		return 1;
    	}
    	else
    	{
    		printf("文件打开成功\n");
    	}
    	return 0;
    }

            文件的顺序读写

            顺序读写函数的介绍

    函数名 功能 适用场景
    fgetc 从文件流中读取一个字符 逐个字符读取文本文件(如解析配置文件、简单文本处理)
    fputc 向文件流中写入一个字符 逐个字符写入文本文件(如生成日志、构建文本内容)
    fgets 从文件流中读取一行或指定长度的字符串(包含换行符,自动添加\0 按行读取文本文件(如读取CSV、配置文件)
    fputs 向文件流中写入一个字符串(不自动添加换行符) 写入字符串到文本文件(如保存用户输入、导出数据)
    fscanf 从文件流中按格式读取数据(类比 scanf 读取结构化文本数据(如读取"Name: %s, Age: %d" 格式的记录)
    fprintf 向文件流中按格式写入数据(类比 printf 写入结构化文本数据(如生成格式化日志、保存表格数据)
    fread 从文件流中读取二进制数据块(按字节数和项数) 读取二进制结构体或数组(如加载图片、音视频文件或程序状态快照)
    fwrite 向文件流中写入二进制数据块(按字节数和项数) 写入二进制结构体或数组(如保存游戏进度、序列化数据)
    getc 与 fgetc 功能相同,但可能通过宏实现(性能略高) 同 fgetc,常用于高频字符读取场景
    putc 与 fputc 功能相同,但可能通过宏实现(性能略高) 同 fputc,常用于高频字符写入场景
    ungetc 将字符退回输入流,使下次读取可重新获取该字符 解析需要“向前看”的场景(如词法分析器中回退字符)

            上⾯说的适⽤于所有输⼊流⼀般指适⽤于标准输⼊流和其他输⼊流(如⽂件输⼊流);所有输出流⼀般指适⽤于标准输出流和其他输出流(如⽂件输出流)。

    关键说明

    1. 文本模式 vs 二进制模式

      • 文本模式函数(fgetcfputs等)会自动处理换行符转换(如Windows的\r\n ↔ \n)。

      • 二进制模式函数(freadfwrite)直接操作原始字节,无格式转换。

    2. 顺序读写特性

      • 所有函数按文件指针顺序操作,每次读写后指针自动后移。

      • 与随机访问函数(fseekftell)互补,不可混用。

    3. 典型应用场景

      • 文本处理fgets + fputs(逐行读写),fscanf + fprintf(格式化数据)。

      • 二进制处理fread + fwrite(结构体/数组持久化)。

      • 低层操作fgetc + fputc(自定义解析或生成。

    举例说明:写入一个字符

    fputc函数的结构:

    #include<stdio.h>
    int main()
    {
    	FILE* fp=fopen("test.txt","w");
    	if(fp==NULL)
    	{
    		perror("fopen");
    		return 1;
    	}
    	//写文件
    	fputc('a', fp);
    	fputc('b', fp);
    	fputc('c', fp);
    	fputc('d', fp);
    	fputc('e', fp);
    	fputc('f', fp);
    	fputc('g', fp);
    	fputc('h', fp);
    	fputc('i', fp);
    	fputc('j', fp);
    	fputc('k', fp);
    	//关闭文件
    	fclose(fp);
    	fp = NULL;
    	return 0;
    }

     代码为0 ,代表运行成功

    成果展示:

    读字符

    #include<stdio.h>
    int main()
    {
    	FILE* fp=fopen("test.txt","r");
    	if(fp==NULL)
    	{
    		perror("fopen");
    		return 1;
    	}
    	//写文件
    	fputc('a', fp);
    	fputc('b', fp);
    	fputc('c', fp);
    	fputc('d', fp);
    	fputc('e', fp);
    	fputc('f', fp);
    	fputc('g', fp);
    	fputc('h', fp);
    	fputc('i', fp);
    	fputc('j', fp);
    	fputc('k', fp);
    	//读文件
    	int ch;
    	while((ch=fgetc(fp))!=EOF)
    	{
    		printf("%c ", ch);
    	}
    	//关闭文件
    	fclose(fp);
    	fp = NULL;
    	return 0;
    }

    对比两组函数scanf/fscanf/sscanf和printf/fprintf/sprintf

    对比表格

    对比项 scanf 系列 (scanf/fscanf/sscanf) printf 系列 (printf/fprintf/sprintf)
    核心功能 从输入源按格式解析数据并存储到变量。 将数据按格式格式化输出到目标输出源。
    函数分类 scanf:标准输入(键盘)
    fscanf:文件流
    sscanf:字符串
    printf:标准输出(屏幕)
    fprintf:文件流
    sprintf:字符串
    参数顺序 scanf(format, &var1, &var2...)
    fscanf(stream, format, &var...)
    printf(format, val1, val2...)
    fprintf(stream, format, val...)
    输入/输出源 输入源:键盘(stdin)、文件、字符串 输出目标:屏幕(stdout)、文件、字符串
    返回值 返回成功解析的项数(失败返回EOF 返回成功输出的字符数(失败返回负值)
    安全性风险 缓冲区溢出(如%s无长度限制) sprintf存在缓冲区溢出风险,建议用snprintf
    典型应用场景 读取用户输入、解析文件数据、提取字符串中的结构化信息 打印日志、生成格式化字符串、写入文件数据

    详细对比说明

    1. 功能与数据流方向

    3. 安全性问题

    • scanf系列

      • 从输入源(键盘、文件、字符串)读取数据,按格式解析后存入变量。

    • 示例

      int age;
      char name[32];
      sscanf("Name: Alice, Age: 25", "Name: %s, Age: %d", name, &age);

      printf系列

    • 将数据按格式转换为字符串,输出到目标(屏幕、文件、字符串)。

    • 示例

      char buffer[100];
      sprintf(buffer, "Result: %d", 42); // 输出到字符串

      2. 参数差异

    • scanf系列

      • scanf:直接从 stdin 读取,无需指定输入源。

      • fscanf:需传入文件指针(如 FILE* fp)。

      • sscanf:需传入源字符串。

      • 变量参数:必须传递变量的地址(如 &num)。

    • printf系列

      • printf:直接输出到 stdout

      • fprintf:需传入文件指针。

      • sprintf:需传入目标字符数组。

      • 变量参数:直接传递变量值(非指针)。

    • scanf系列

      • %s 和 %[ 格式符可能导致缓冲区溢出。

      • 改进方法:指定最大宽度(如 %31s 限制长度为31字符)。

    • printf系列

      • sprintf 可能因目标缓冲区过小导致溢出。

      • 改进方法:使用 snprintf 限制写入长度。

    char buf[10];
    snprintf(buf, sizeof(buf), "%s", "Too long string"); // 安全截断

    4. 典型应用场景

    函数 应用场景
    scanf 交互式程序读取用户输入(需配合输入验证)
    fscanf 读取配置文件、日志文件等结构化文本文件
    sscanf 解析字符串中的数字或字段(如解析HTTP响应头)
    printf 输出调试信息、程序运行结果到控制台
    fprintf 写入日志文件、生成报告文件
    sprintf 动态生成格式化字符串(需注意缓冲区大小,优先用 snprintf

    5.典型应用场景

     输入函数组

    // 从键盘读取输入
    int num;
    scanf("%d", &num); 
    
    // 从文件读取
    FILE* fp = fopen("data.txt", "r");
    fscanf(fp, "Value: %d", &num);
    
    // 从字符串解析
    char str[] = "2023-10-01";
    int year, month, day;
    sscanf(str, "%d-%d-%d", &year, &month, &day);

    输出字符组

    // 输出到屏幕
    printf("Result: %d\n", 42);
    
    // 输出到文件
    FILE* fp = fopen("log.txt", "w");
    fprintf(fp, "Error: %s\n", "File not found");
    
    // 输出到字符串
    char buffer[64];
    sprintf(buffer, "Date: %04d-%02d-%02d", year, month, day);

    文件的随机读写

            fseek函数

            根据文件指针的位置和偏移量来定位文件的指针(文件内容的光标)

    int fseek(FILE*stream,long int offset,int origin);

            例子:

    #include<stdio.h>
    int main()
    {
    	FILE* pfile;
    	pfile = fopen("test.txt", "wb");
    	fputs("Hello, world!", pfile);
    	fseek(pfile,9,SEEK_SET);
    	fputs("  IM THE WORLD", pfile);
    	fclose(pfile);
    	return 0;
    }

            结果:

            ftell函数

            返回文件指针相对起始位置的偏移量

    #include<stdio.h>
    int main()
    {
    	FILE* pfile;
    	long size;
    	pfile = fopen("test.txt", "rb");
    	if (pfile == NULL)
    	{
    		perror("打开失败");
    		return 1;
    	}
    	else
    	{
    		fseek(pfile, 0, SEEK_END);
    		size=ftell(pfile);
    		fclose(pfile);
    		printf("文件大小为:%ld\n", size);
    	}
    	return 0;
    }

            rewind函数

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

    void rewind (FILE*stream);

    #include<stdio.h>
    int main()
    {
    	int n;
    	FILE* pfile;
    	char str[27];
    	pfile = fopen("test.txt", "w+");
    	for (n='A';n<= 'Z';n++)
    	{
    		fputc(n,pfile);
    	}
    	rewind(pfile);
    	fread(str, 1, 26, pfile);
    	fclose(pfile);
    	str[26] = '\0';
    	printf("%s\n", str);
    	return 0;
    }

    结果为: 

       文件读取结束的判定

            错误使用的feof     

            牢记:在⽂件读取过程中,不能⽤ feof 函数的返回值直接来判断⽂件的是否结束。feof 的作⽤是:当⽂件读取结束的时候,判断读取结束的原因是否是:遇到⽂件尾结束。

            1. ⽂本⽂件读取是否结束,判断返回值是否为 EOF ( fgetc ),或者 NULL ( fgets )例如:• fgetc 判断是否为 EOF .• fgets 判断返回值是否为 NULL .

            2. ⼆进制⽂件的读取结束判断,判断返回值是否⼩于实际要读的个数。例如:• fread判断返回值是否⼩于实际要读的个数。

            文本文件的例子:

    #include<stdio.h>
    #include<stdlib.h>
    int main()
    {
    	int c;//要处理EOF,必须为int型变量
    	FILE* pfile=fopen("test.txt", "r");
    	if (!pfile)
    	{
    		perror("打开失败");
    		return 1;
    	}
    	//输入失败的时候会返回EOF
    	while ((c = fgetc(pfile)) != EOF)
    	{
    		putchar(c);
    	}
    	//判断原因
    	if (ferror(pfile))
    	{
    		puts("读取失败");
    	}
    	else if(feof(pfile))
    	{
    		puts("文件结束");
    	}
    	fclose(pfile);
    	return 0;
    }

            二进制文件的例子:

    #include<stdio.h>
    enum {SIZE=5};
    int main()
    {
    	double a[SIZE] = {1.1, 2.2, 3.3, 4.4, 5.5};
    	FILE* pfile = fopen("test.txt", "wb");//以二进制方式打开文件
    	fwrite(a, sizeof(double), SIZE, pfile);//将数组a写入文件
    	fclose(pfile);//关闭文件
    
    	double b[SIZE];
    	pfile =fopen("test.txt", "rb");//以二进制方式打开文件
    	size_t ret_code = fread(b, sizeof(double), SIZE, pfile);//从文件中读取数组b
    	if (ret_code==SIZE)
    	{
    		puts("数组读取内容为:");
    		for (int n = 0;n < SIZE;n++)
    		{
    			printf("%.2f ",b[n]);
    		}
    		putchar('\n');
    	}
    	else
    	{
    		if (feof(pfile))
    		{
    			puts("文件读取结束");
    		}
    		else if (ferror(pfile))
    		{
    			puts("文件读取失败");
    		}
    	}
    	fclose(pfile);//关闭文件
    	return 0;
    }

            文件缓冲区

            ANSI C 标准采⽤“缓冲⽂件系统” 处理数据⽂件的,所谓缓冲⽂件系统是指系统⾃动地在内存中为程序中每⼀个正在使⽤的⽂件开辟⼀块“⽂件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才⼀起送到磁盘上。如果从磁盘向计算机读⼊数据,则从磁盘⽂件中读取数据输⼊到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的⼤⼩根据C编译系统决定的。

            

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

            以上就是本期C语言的内容了,希望各位读者朋友点个赞,谢谢