【C语言】文件操作全解析

文章目录

在C语言编程中,文件操作是实现数据持久化存储的关键技术。无论是保存用户数据、配置信息还是处理大量数据,都离不开文件操作。本文将详细讲解C语言文件操作的方方面面,从基本概念到实际应用,帮助你全面掌握这一重要技能。

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

在程序运行过程中,我们处理的数据通常存储在内存中。然而,内存具有临时性的特点------当程序退出或计算机断电时,内存中的数据会全部丢失。如果我们希望数据能够长期保存,以便下次运行程序时继续使用,就必须使用文件将数据存储到磁盘等外部存储设备中。

简单来说,文件操作让程序的数据拥有了"记忆"能力,是实现数据持久化的核心手段。

二、认识文件:不止是磁盘上的存储

在C语言中,我们通常从功能角度将文件分为两类:

2.1 程序文件

程序文件是用于构成程序本身的文件,包括:

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

2.2 数据文件

数据文件是程序运行时读写的数据载体,比如:

  • 程序需要读取的配置文件
  • 程序输出的日志文件
  • 存储用户信息的数据库文件

本文重点讨论的是数据文件的操作。

2.3 文件名的构成

一个完整的文件名包含三个部分:

  • 文件路径:指示文件在磁盘中的位置
  • 文件名主干:文件的核心标识
  • 文件后缀:指示文件类型

例如:c:\code\test.txt中,c:\code\是路径,test是文件名主干,.txt是后缀。

三、文本文件与二进制文件:数据的两种形态

根据数据的存储形式,数据文件可分为文本文件和二进制文件,它们的区别如下:

3.1 存储方式差异

  • 二进制文件:数据在内存中以二进制形式存储,不加转换直接输出到外存
  • 文本文件:数据在存储前被转换为ASCII码形式,以字符形式存储

3.2 实例对比:整数10000的存储

以整数10000为例:

  • 文本文件存储:占用5个字节(每个字符一个字节),存储内容是'1','0','0','0','0'的ASCII码
  • 二进制文件存储:在VS2019环境下仅占用4个字节,直接存储其二进制形式00000000 00000000 00100111 00010000

3.3 二进制文件操作示例

c 复制代码
#include <stdio.h>
int main()
{
    int a = 10000;
    FILE* pf = fopen("test.txt", "wb");  // 以二进制写入模式打开文件
    fwrite(&a, 4, 1, pf);  // 将a以二进制形式写入文件
    fclose(pf);
    pf = NULL;
    return 0;
}

在VS中查看二进制文件时,需要使用二进制编辑器,10000会显示为10270000

四、文件的打开与关闭:操作的开始与结束

4.1 流的概念

C语言通过"流"(stream)来操作各种外部设备(包括文件)。流可以想象成"流淌着字符的河",程序通过流与外部设备进行数据交换。

C程序启动时会默认打开3个标准流:

  • stdin:标准输入流(通常对应键盘)
  • stdout:标准输出流(通常对应显示器)
  • stderr:标准错误流(通常对应显示器)

这就是为什么我们可以直接使用scanfprintf函数进行输入输出,而无需手动打开流。

4.2 文件指针

缓冲文件系统中,通过文件指针(FILE*类型)来管理文件。每个被打开的文件在内存中都有一个对应的文件信息区(结构体),存储文件名、状态、当前位置等信息,文件指针就指向这个结构体。

定义文件指针的方式:

c 复制代码
FILE* pf;  // 声明一个文件指针变量

4.3 打开与关闭函数

  • 打开文件 :使用fopen函数

    c 复制代码
    FILE * fopen ( const char * filename, const char * mode );
  • 关闭文件 :使用fclose函数

    c 复制代码
    int fclose ( FILE * stream );

4.4 文件打开模式

文件打开模式决定了对文件的操作权限,常见模式如下:

文件使用方式 含义 如果指定文件不存在
"r"(只读) 打开文本文件用于输入 出错
"w"(只写) 打开文本文件用于输出 创建新文件
"a"(追加) 向文本文件末尾添加数据 创建新文件
"rb"(只读) 打开二进制文件用于输入 出错
"wb"(只写) 打开二进制文件用于输出 创建新文件
"ab"(追加) 向二进制文件末尾添加数据 创建新文件
"r+"(读写) 打开文本文件用于读写 出错
"w+"(读写) 创建文本文件用于读写 创建新文件
"a+"(读写) 打开文本文件用于读写,从末尾开始 创建新文件

4.5 示例代码

c 复制代码
#include <stdio.h>
int main ()
{
    FILE * pFile;
    // 打开文件
    pFile = fopen ("myfile.txt","w");
    // 文件操作
    if (pFile!=NULL)
    {
        fputs ("fopen example",pFile);
        // 关闭文件
        fclose (pFile);
    }
    return 0;
}

五、文件的顺序读写:按部就班的数据处理

顺序读写是指从文件的开头到结尾依次进行读写操作,C语言提供了一系列函数实现这一功能:

5.1 常用顺序读写函数

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

5.2 函数对比

  • 输入函数家族

    • scanf:从标准输入流读取格式化数据
    • fscanf:从指定输入流读取格式化数据
    • sscanf:从字符串读取格式化数据
  • 输出函数家族

    • printf:向标准输出流输出格式化数据
    • fprintf:向指定输出流输出格式化数据
    • sprintf:将格式化数据输出到字符串

六、文件的随机读写:自由定位数据位置

随机读写允许程序直接定位到文件的任意位置进行操作,主要通过以下函数实现:

6.1 fseek函数:定位文件指针

根据文件指针的当前位置和偏移量来定位指针:

c 复制代码
int fseek ( FILE * stream, long int offset, int origin );

参数origin可以是:

  • SEEK_SET:从文件开头开始计算
  • SEEK_CUR:从当前位置开始计算
  • SEEK_END:从文件末尾开始计算

示例:

c 复制代码
#include <stdio.h>
int main ()
{
    FILE * pFile;
    pFile = fopen ( "example.txt" , "wb" );
    fputs ( "This is an apple." , pFile );
    fseek ( pFile , 9 , SEEK_SET );  // 定位到第10个字符位置
    fputs ( " sam" , pFile );  // 从该位置开始写入
    fclose ( pFile );
    return 0;
}
// 最终文件内容为:"This is a sample."

6.2 ftell函数:获取当前位置偏移量

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

c 复制代码
long int ftell ( FILE * stream );

示例:计算文件大小

c 复制代码
#include <stdio.h>
int main ()
{
    FILE * pFile;
    long size;
    pFile = fopen ("myfile.txt","rb");
    if (pFile==NULL) 
        perror ("Error opening file");
    else
    {
        fseek (pFile, 0, SEEK_END);  // 定位到文件末尾
        size=ftell (pFile);  // 获取偏移量,即文件大小
        fclose (pFile);
        printf ("Size of myfile.txt: %ld bytes.\n",size);
    }
    return 0;
}

6.3 rewind函数:重置文件指针

将文件指针重新定位到文件开头:

c 复制代码
void rewind ( FILE * stream );

示例:

c 复制代码
#include <stdio.h>
int main ()
{
    int n;
    FILE * pFile;
    char buffer [27];
    
    pFile = fopen ("myfile.txt","w+");
    for ( n='A' ; n<='Z' ; n++)
        fputc ( n, pFile);
    rewind (pFile);  // 回到文件开头
    
    fread (buffer,1,26,pFile);
    fclose (pFile);
    
    buffer[26]='\0';
    printf(buffer);  // 输出ABCDEFGHIJKLMNOPQRSTUVWXYZ
    return 0;
}

七、文件读取结束的判定:正确识别结束条件

很多初学者会错误地使用feof函数来判断文件是否结束,这是需要避免的。

7.1 feof函数的正确用法

feof函数的作用是:当文件读取结束时,判断结束的原因是否是"遇到文件尾"。它并不能直接用来判断文件是否结束。

7.2 正确的判断方式

  1. 文本文件

    • 使用fgetc时,判断返回值是否为EOF
    • 使用fgets时,判断返回值是否为NULL

    示例:

    c 复制代码
    #include <stdio.h>
    #include <stdlib.h>
    int main(void)
    {
        int c;  // 注意:必须是int类型,以处理EOF
        FILE* fp = fopen("test.txt", "r");
        if(!fp) {
            perror("File opening failed");
            return EXIT_FAILURE;
        }
        
        // fgetc读取失败或遇到文件结束时返回EOF
        while ((c = fgetc(fp)) != EOF)
            putchar(c);
        
        // 判断结束原因
        if (ferror(fp))
            puts("I/O error when reading");
        else if (feof(fp))
            puts("End of file reached successfully");
        
        fclose(fp);
    }
  2. 二进制文件

    • 使用fread时,判断返回值是否小于实际要读取的个数

    示例:

    c 复制代码
    #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);  // 写入数组
        fclose(fp);
        
        double b[SIZE];
        fp = fopen("test.bin","rb");
        size_t ret_code = fread(b, sizeof *b, SIZE, fp);  // 读取数组
        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);
    }

八、文件缓冲区:提升效率的中间层

ANSI C标准采用"缓冲文件系统",系统会自动为每个正在使用的文件在内存中开辟一块"文件缓冲区":

  • 输出数据时:先送到内存缓冲区,缓冲区满后再一起写入磁盘
  • 输入数据时:先从磁盘读取到缓冲区,再从缓冲区送到程序数据区

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

8.1 缓冲区演示

c 复制代码
#include <stdio.h>
#include <windows.h>  // VS2019 WIN11环境
int main()
{
    FILE* pf = fopen("test.txt", "w");
    fputs("abcdef", pf);  // 数据先存入输出缓冲区
    printf("睡眠10秒-此时打开test.txt,文件无内容\n");
    Sleep(10000);
    printf("刷新缓冲区\n");
    fflush(pf);  // 手动刷新缓冲区,数据写入磁盘
    printf("再睡眠10秒-此时打开test.txt,文件有内容\n");
    Sleep(10000);
    fclose(pf);  // 关闭文件时也会刷新缓冲区
    pf = NULL;
    return 0;
}

8.2 重要结论

由于缓冲区的存在,操作文件时必须注意:

  • 及时刷新缓冲区(使用fflush函数)
  • 操作结束后关闭文件(fclose会自动刷新缓冲区)

否则可能导致数据未能正确写入文件,造成数据丢失或不一致。

总结

文件操作是C语言编程中的重要技能,掌握它可以让你的程序具备数据持久化能力。本文介绍了文件的基本概念、类型划分、打开关闭、顺序读写、随机读写、结束判定以及缓冲区机制等内容。

在实际编程中,要注意以下几点:

  • 始终检查文件是否成功打开
  • 操作完成后及时关闭文件
  • 正确判断文件读取结束的条件
  • 理解并合理利用缓冲区机制
相关推荐
程序猿编码3 小时前
二进制签名查找器(Aho-Corasick 自动机):设计思路与实现原理(C/C++代码实现)
c语言·c++·网络安全·二进制·逆向工程·ac自动机
mailtolaozhao3 小时前
C#入门--Hello world
开发语言·c#
王维志3 小时前
C# 中的 DateTime
开发语言·c#·.net
歪歪1008 小时前
HTML 如何转 Markdown
开发语言·chrome·python·程序人生·html
小坏坏的大世界8 小时前
C++中多线程和互斥锁的基本使用
开发语言·c++
路由侠内网穿透8 小时前
本地部署 SQLite 数据库管理工具 SQLite Browser ( Web ) 并实现外部访问
运维·服务器·开发语言·前端·数据库·sqlite
王者鳜錸9 小时前
PYTHON从入门到实践-18Django模版渲染
开发语言·python·django
Hard but lovely9 小时前
C++ STL--> vector的模拟实现!
开发语言·c++
hweiyu009 小时前
IDEA搭建GO环境
开发语言·后端·golang·intellij-idea·idea·intellij idea