当前位置:首页 > 技术 > 正文内容

C语言文件操作全面指南

访客 技术 2026年9月12日 12

在C语言程序设计中,文件操作是实现数据持久化存储的核心功能。它允许程序与外部存储设备(如硬盘、U盘等)进行数据交换,从而保存程序运行状态、用户数据或配置信息。C语言的文件操作功能主要通过标准库<stdio.h>中定义的FILE结构体及一系列函数来实现。

文件与流的概念

在C语言的视角下,文件被抽象为字节的有序序列。无论是文本、图像还是其他二进制数据,它们在文件系统中都以字节流的形式存在。程序通过"文件流"(或简称"流")与文件进行交互,这意味着数据从文件流入程序(读取)或从程序流向文件(写入)。

文件类型划分

根据文件内容的组织形式和访问方式,文件通常分为以下两类:

1. 文本文件(Text Files)

  • 特性: 由可打印字符(如ASCII码或其扩展字符集)组成,易于人类阅读和编辑。不同操作系统对文本文件中的行结束符有不同约定(例如Windows使用CRLF,Unix/Linux使用LF)。
  • 操作方式: 通常使用fprintf、fscanf进行格式化读写,或通过fputc、fgetc处理单个字符,以及fputs、fgets处理字符串。
  • 缓冲机制: 默认采用行缓冲,即在遇到换行符或缓冲区满时才进行实际的I/O操作。
  • 优势: 兼容性好,便于不同系统之间的数据交换。
  • 局限性: 不适合存储原始二进制数据,如图片或音频,因为这些数据在写入时可能会被转换。

2. 二进制文件(Binary Files)

  • 特性: 以原始字节序列存储数据,不进行任何字符转换。文件内容通常不可直接被文本编辑器解读。例如,程序的可执行文件、图片、视频、压缩包等。
  • 操作方式: 主要使用fread和fwrite函数进行块数据读写,效率较高。
  • 缓冲机制: 默认采用块缓冲,适合处理大量连续数据。
  • 优势: 存储效率高,能够精确保存原始数据,适用于各种非文本数据。
  • 局限性: 不具备人类可读性,需要专门的程序或工具才能解析。

文件操作流程概述

在C语言中,对文件进行操作通常遵循以下基本步骤:

  1. 声明文件指针: 定义一个FILE *类型的变量,作为文件与程序之间的连接桥梁。
  2. 开启文件: 使用fopen函数打开指定文件,并设置其操作模式(读、写、追加等)。
  3. 数据交换: 根据需求选择合适的函数(如fprintf、fscanf、fread、fwrite等)进行数据的读取或写入。
  4. 关闭文件: 操作完成后,务必调用fclose函数关闭文件,释放系统资源。

文件指针详解

文件指针(类型为FILE*)是进行文件操作的关键。它是一个指向FILE结构体的指针,而FILE结构体包含了文件缓冲区、文件位置指针、文件状态标志等与文件相关的所有信息。程序通过文件指针间接控制和访问文件。

FILE *myFilePointer; // 声明一个文件指针

文件开启函数:fopen

fopen函数用于打开一个文件,并建立一个文件流。其函数原型如下:

FILE *fopen(const char *filename, const char *mode);
  • filename:指向一个字符串,包含要打开的文件路径和名称。
  • mode:指向一个字符串,指定文件打开模式。

常用文件打开模式:

  • "r":只读模式。 文件必须存在,否则fopen返回NULL。
  • "w":只写模式。 如果文件不存在则创建;如果文件已存在,则截断(清空)其内容。
  • "a":追加模式。 如果文件不存在则创建;如果文件已存在,新写入的数据将添加到文件末尾。
  • "r+":读写模式(更新)。 文件必须存在,允许读写操作。
  • "w+":读写模式(更新)。 如果文件不存在则创建;如果文件已存在,则截断其内容,然后允许读写。
  • "a+":读写模式(更新)。 如果文件不存在则创建;如果文件已存在,读操作从文件开头开始,写操作则追加到文件末尾。

所有模式都可选择性地添加'b'字符,如"rb"、"wb",以指示按二进制模式打开文件。在某些系统上,文本模式和二进制模式的处理方式可能不同。

// 示例:尝试以只读模式打开文件并进行错误检查
FILE *dataSource = fopen("data.txt", "r");
if (dataSource == NULL) {
    perror("无法打开文件 data.txt"); // 打印系统错误信息
    // 可以在这里进行错误处理,例如退出程序
    // exit(EXIT_FAILURE);
}
// 成功打开文件,可以进行后续操作...

文件关闭函数:fclose

文件操作完成后,必须使用fclose函数关闭文件流,释放与该文件关联的所有系统资源和缓冲区。这是一个良好的编程习惯,可以防止数据丢失和资源泄露。

int fclose(FILE *stream);

该函数成功时返回0,失败时返回EOF。

// 示例
// 假设 dataSource 已成功打开
// FILE *dataSource = fopen("data.txt", "r");
if (dataSource != NULL) {
    if (fclose(dataSource) == EOF) {
        perror("关闭文件失败");
    }
}

文件数据的读写

C语言提供了多种函数来处理文件数据的读写,主要分为字符、字符串和格式化I/O。

文本文件读写方法

1. 单字符读写:fgetc / fputc

  • fgetc: 从指定文件流中读取一个字符。
  • int fgetc(FILE *stream);

    该函数返回读取到的字符(以int类型表示),如果到达文件末尾或发生错误则返回EOF。

    // 示例:逐字符读取文件内容并打印
    FILE *readerFile = fopen("input.txt", "r");
    if (readerFile == NULL) {
        perror("打开 input.txt 失败");
        // return 1;
    }
    int c;
    printf("文件内容:\n");
    while ((c = fgetc(readerFile)) != EOF) {
        putchar(c); // 将字符输出到标准输出
    }
    fclose(readerFile);
    
  • fputc: 将一个字符写入到指定文件流中。
  • int fputc(int character, FILE *stream);

    成功时返回写入的字符,失败时返回EOF。

    // 示例:向文件写入单个字符
    FILE *writerFile = fopen("output.txt", "w");
    if (writerFile == NULL) {
        perror("打开 output.txt 失败");
        // return 1;
    }
    fputc('H', writerFile);
    fputc('e', writerFile);
    fputc('l', writerFile);
    fputc('l', writerFile);
    fputc('o', writerFile);
    fclose(writerFile);
    

2. 字符串读写:fgets / fputs

  • fgets: 从文件流中读取一行字符串。
  • char *fgets(char *buffer, int max_len, FILE *stream);

    读取最多max_len-1个字符,直到遇到换行符或文件末尾。换行符如果被读取到也会存储到缓冲区中。成功时返回buffer,失败或文件末尾时返回NULL。

    // 示例:逐行读取文件内容
    FILE *lineReader = fopen("lines.txt", "r");
    if (lineReader == NULL) {
        perror("打开 lines.txt 失败");
        // return 1;
    }
    char lineBuffer[256];
    printf("按行读取内容:\n");
    while (fgets(lineBuffer, sizeof(lineBuffer), lineReader) != NULL) {
        printf("%s", lineBuffer); // fgets会保留换行符
    }
    fclose(lineReader);
    
  • fputs: 将一个字符串写入到文件流中。
  • int fputs(const char *str, FILE *stream);

    成功时返回非负值,失败时返回EOF。fputs不会自动添加换行符。

    // 示例:向文件写入字符串
    FILE *lineWriter = fopen("output_lines.txt", "w");
    if (lineWriter == NULL) {
        perror("打开 output_lines.txt 失败");
        // return 1;
    }
    fputs("第一行文本。\n", lineWriter);
    fputs("第二行文本。\n", lineWriter);
    fclose(lineWriter);
    

3. 格式化读写:fscanf / fprintf

  • fscanf: 从文件中按照指定格式读取数据。
  • int fscanf(FILE *stream, const char *format, ...);

    功能类似于scanf,但从文件流中读取。返回成功匹配并赋值的输入项数量。

    // 示例:从文件读取格式化数据
    FILE *formatReader = fopen("data.log", "r");
    if (formatReader == NULL) {
        perror("打开 data.log 失败");
        // return 1;
    }
    int id_val;
    char item_name[50];
    double item_price;
    printf("从 data.log 读取数据:\n");
    while (fscanf(formatReader, "%d %s %lf", &id_val, item_name, &item_price) == 3) {
        printf("ID: %d, Item: %s, Price: %.2f\n", id_val, item_name, item_price);
    }
    fclose(formatReader);
    
  • fprintf: 按照指定格式向文件中写入数据。
  • int fprintf(FILE *stream, const char *format, ...);

    功能类似于printf,但将输出写入文件流。返回写入的字符数,发生错误时返回负值。

    // 示例:向文件写入格式化数据
    FILE *formatWriter = fopen("report.txt", "w");
    if (formatWriter == NULL) {
        perror("打开 report.txt 失败");
        // return 1;
    }
    int product_id = 101;
    const char *product_name = "C Programming Book";
    float price = 59.90f;
    fprintf(formatWriter, "Product ID: %d\n", product_id);
    fprintf(formatWriter, "Product Name: %s\n", product_name);
    fprintf(formatWriter, "Price: %.2f\n", price);
    fclose(formatWriter);
    

二进制文件读写:fread / fwrite

对于二进制文件的操作,fread和fwrite函数以块为单位读写数据,效率高且不进行任何数据转换。

  • fread: 从文件流中读取指定数量的数据块。
  • size_t fread(void *ptr, size_t size, size_t count, FILE *stream);
    • ptr:指向存储读取数据的内存块。
    • size:每个数据块的字节大小。
    • count:要读取的数据块数量。
    • stream:文件指针。

    返回成功读取的数据块数量。

  • fwrite: 将指定数量的数据块写入文件流。
  • size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);
    • ptr:指向要写入数据的内存块。
    • size:每个数据块的字节大小。
    • count:要写入的数据块数量。
    • stream:文件指针。

    返回成功写入的数据块数量。

// 示例:二进制文件读写结构体数据
typedef struct {
    int record_id;
    float measurement_val;
    char description[30];
} DataRecord;

void write_binary_data(const char* filename) {
    FILE *binFile = fopen(filename, "wb");
    if (binFile == NULL) {
        perror("写入二进制文件失败");
        return;
    }
    DataRecord dr1 = {101, 123.45f, "Sensor_A Data"};
    DataRecord dr2 = {102, 67.89f, "Sensor_B Data"};
    fwrite(&dr1, sizeof(DataRecord), 1, binFile);
    fwrite(&dr2, sizeof(DataRecord), 1, binFile);
    fclose(binFile);
    printf("二进制数据写入 '%s' 完成。\n", filename);
}

void read_binary_data(const char* filename) {
    FILE *binFile = fopen(filename, "rb");
    if (binFile == NULL) {
        perror("读取二进制文件失败");
        return;
    }
    DataRecord temp_dr;
    printf("读取二进制数据从 '%s':\n", filename);
    while (fread(&temp_dr, sizeof(DataRecord), 1, binFile) == 1) {
        printf("ID: %d, Value: %.2f, Desc: %s\n", temp_dr.record_id, temp_dr.measurement_val, temp_dr.description);
    }
    fclose(binFile);
}

// 主函数中调用示例:
// write_binary_data("sensor_data.bin");
// read_binary_data("sensor_data.bin");

文件位置管理与随机访问

C语言提供了一系列函数来控制文件指针的位置,从而实现对文件内容的随机访问。

  • fseek: 移动文件指针到指定位置。
  • int fseek(FILE *stream, long offset, int origin);
    • offset:偏移量。
    • origin:起始位置,可以是SEEK_SET(文件开头)、SEEK_CUR(当前位置)或SEEK_END(文件末尾)。

    成功时返回0,失败时返回非零值。

  • ftell: 获取当前文件指针的位置。
  • long ftell(FILE *stream);

    返回当前文件指针相对于文件开头的字节偏移量,失败时返回-1L。

  • rewind: 将文件指针重置到文件起始位置。
  • void rewind(FILE *stream);
// 示例:文件定位
FILE *randFile = fopen("random_access.bin", "r+b"); // 读写二进制模式
if (randFile == NULL) {
    perror("打开 random_access.bin 失败");
    // return 1;
}

// 写入一些初始数据
const char *initial_data = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
fputs(initial_data, randFile);
fflush(randFile); // 确保数据写入文件

// 获取当前位置(通常在写入数据后会位于文件末尾)
long current_offset = ftell(randFile);
printf("当前文件指针位置:%ld 字节\n", current_offset); // 应该等于 initial_data 的长度

// 移动文件指针到第10个字节处 (从0开始计数)
fseek(randFile, 10, SEEK_SET);
printf("移动指针到文件开头第10字节。\n");

// 读取一个字符
char read_char_at_pos = fgetc(randFile);
printf("在当前位置读取到的字符是:%c\n", read_char_at_pos); // 应该是 'A'

// 再次移动指针,从当前位置向后偏移5个字节
fseek(randFile, 5, SEEK_CUR);
printf("从当前位置向后偏移5字节。\n");
read_char_at_pos = fgetc(randFile);
printf("再次读取到的字符是:%c\n", read_char_at_pos); // 应该是 'Q' (10+1+5+1=17, initial_data[16] is 'Q')

// 将文件指针重置到开头
rewind(randFile);
printf("文件指针已重置到开头。\n");

// 从头开始读取一个字符
read_char_at_pos = fgetc(randFile);
printf("重置后读取到的字符是:%c\n", read_char_at_pos); // 应该是 '0'

fclose(randFile);

文件操作错误检查

在文件操作过程中,错误是不可避免的。C语言提供了一套机制来检测和处理这些错误。

  • feof: 检测文件是否已达到末尾。
  • int feof(FILE *stream);

    如果文件结束符被设置,返回非零值。

  • ferror: 检测文件流是否发生过读写错误。
  • int ferror(FILE *stream);

    如果文件流的错误指示器被设置,返回非零值。

  • clearerr: 清除文件流的错误和文件结束指示器。
  • void clearerr(FILE *stream);
  • perror: 打印由系统错误码对应的错误消息。
  • void perror(const char *s);

    通常在fopen失败时使用,提供更具体的错误原因。

// 示例:文件错误处理
FILE *missingFile = fopen("non_existent_file.txt", "r");
if (missingFile == NULL) {
    perror("尝试打开文件时发生错误"); // 输出类似 "尝试打开文件时发生错误: No such file or directory"
    // exit(EXIT_FAILURE); // 退出程序
} else {
    // 假设文件存在并进行操作...
    fclose(missingFile);
}

// 模拟读写错误检测
// 注意:以下代码可能因系统权限而无法真正触发错误
FILE *protectedFile = fopen("/root/protected_file.txt", "w"); // 假设 /root 目录不可写
if (protectedFile == NULL) {
    perror("尝试在受保护目录创建文件");
} else {
    if (fputc('X', protectedFile) == EOF) {
        if (ferror(protectedFile)) {
            perror("写入文件时发生错误");
        }
    }
    fclose(protectedFile);
}

文件缓冲区管理

为了提高I/O效率,C语言的文件操作通常会利用缓冲区。数据并非每次读写都直接与物理存储设备交互,而是先暂存在内存缓冲区中。缓冲区的类型包括:

  • 全缓冲: 当缓冲区满时或程序明确调用fflush时才进行I/O操作(通常用于二进制文件)。
  • 行缓冲: 当遇到换行符时、缓冲区满时或程序明确调用fflush时进行I/O操作(通常用于文本文件)。
  • 无缓冲: 数据读写直接与物理设备交互,不经过缓冲区(效率最低,通常用于特殊情况)。

可以使用setvbuf函数手动设置文件流的缓冲模式和缓冲区大小。

int setvbuf(FILE *stream, char *buf, int mode, size_t size);
  • stream:文件流。
  • buf:指向用户提供的缓冲区。如果为NULL,则系统分配。
  • mode:缓冲模式,如_IOFBF(全缓冲)、_IOLBF(行缓冲)、_IONBF(无缓冲)。
  • size:缓冲区大小。

文件操作实践建议

  1. 及时关闭文件: 完成文件操作后,立即使用fclose关闭文件,释放资源,防止数据丢失和资源泄露。
  2. 全面错误检查: 每次fopen后都应检查其返回值是否为NULL。在读写操作中,也要利用ferror和feof检查错误状态。
  3. 避免使用不安全的函数: 例如,gets()函数不检查缓冲区边界,极易导致缓冲区溢出漏洞,应避免使用,改为fgets()。
  4. 适时刷新缓冲区: 对于写操作,如果需要确保数据立即写入物理存储(例如在程序崩溃前),应调用fflush()函数强制刷新缓冲区。
  5. 善用文件管理函数: remove()用于删除文件,rename()用于文件重命名。合理使用它们有助于程序的健壮性和灵活性。
  6. 选择合适的读写模式: 根据文件内容是文本还是二进制,以及操作需求(只读、只写、追加、读写),选择最合适的打开模式。

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。