Linux C文件读写如何高效操作与避坑?

99ANYc3cd6
预计阅读时长 27 分钟
位置: 首页 C语言 正文
  1. 核心概念:文件描述符 vs. 文件指针
  2. C 标准库 I/O (高级接口,推荐日常使用)
    • 打开文件:fopen()
    • 关闭文件:fclose()
    • 写入数据:fprintf(), fputs(), fwrite()
    • 读取数据:fscanf(), fgets(), fread()
    • 定位文件指针:fseek(), ftell(), rewind()
    • 判断文件结束:feof()
    • 错误处理:ferror(), perror()
    • 刷新缓冲区:fflush()
  3. Linux 系统调用 I/O (底层接口,性能更高,更灵活)
    • 打开文件:open()
    • 创建文件:creat() (已不推荐,使用 open() 替代)
    • 关闭文件:close()
    • 写入数据:write()
    • 读取数据:read()
    • 定位文件指针:lseek()
  4. 综合示例与对比
  5. 最佳实践与注意事项

核心概念:文件描述符 vs. 文件指针

在开始之前,理解这两个概念至关重要。

linux c语言文件读写
(图片来源网络,侵删)
  • 文件指针

    • 来源:C 标准库 (stdio.h)。
    • 本质:一个指向 FILE 结构体的指针。FILE 结构体包含了文件缓冲区、当前读写位置、文件描述符等丰富的信息。
    • 特点:高级、易用、带缓冲,I/O 操作通过函数如 fread(), fwrite() 完成,这些函数会先将数据读写到内存缓冲区,再批量与磁盘交互,大大提高了 I/O 效率。
    • 示例FILE *fp;
  • 文件描述符

    • 来源:Linux/Unix 操作系统内核。
    • 本质:一个非负整数,是内核为了管理一个进程所打开的文件而创建的索引,它指向内核中维护的文件表。
    • 特点:底层、高效、无缓冲,直接与内核交互,没有标准库的额外开销,适合需要极致性能或精细控制的场景(如网络编程、设备驱动)。
    • 标准文件描述符
      • 0: 标准输入
      • 1: 标准输出
      • 2: 标准错误

关系:当你使用 fopen() 打开一个文件时,C 标准库会向内核请求一个文件描述符,并将其存放在 FILE 结构体中,你通过 FILE* 指针操作文件,实际上是 FILE 结构体内部的文件描述符在与内核通信。


C 标准库 I/O (高级接口)

这是最常用、最安全的方式,适用于绝大多数文件操作场景。

linux c语言文件读写
(图片来源网络,侵删)

1 打开文件:fopen()

#include <stdio.h>
FILE *fopen(const char *pathname, const char *mode);
  • pathname: 文件路径。

  • mode: 打开模式,常用模式如下:

    • "r": 只读,文件必须存在。
    • "w": 只写,如果文件存在,则清空内容;如果不存在,则创建新文件。
    • "a": 追加,如果文件存在,则在末尾写入;如果不存在,则创建新文件。
    • "r+": 读写,文件必须存在,读写指针在文件开头。
    • "w+": 读写,如果文件存在,则清空内容;如果不存在,则创建新文件。
    • "a+": 读写,如果文件存在,则在末尾写入;如果不存在,则创建新文件,读操作可以从任意位置开始,但写操作总是在末尾。
  • 返回值:成功返回 FILE* 指针,失败返回 NULL

2 关闭文件:fclose()

#include <stdio.h>
int fclose(FILE *stream);
  • stream: fopen() 返回的文件指针。
  • 返回值:成功返回 0,失败返回 EOF (通常是 -1)。

3 写入数据

  • fprintf(): 格式化写入,类似于 printf()
    fprintf(fp, "Name: %s, Age: %d\n", "Alice", 30);
  • fputs(): 写入一个字符串,不自动换行。
    fputs("Hello, World!\n", fp);
  • fwrite(): 以二进制块形式写入数据。
    int data[] = {10, 20, 30, 40, 50};
    size_t items_written = fwrite(data, sizeof(int), 5, fp); // 写入5个int

4 读取数据

  • fscanf(): 格式化读取,类似于 scanf()
    char name[50];
    int age;
    fscanf(fp, "Name: %s, Age: %d\n", name, &age);
  • fgets(): 从文件中读取一行,包括换行符 \n
    char buffer[256];
    if (fgets(buffer, sizeof(buffer), fp) != NULL) {
        // buffer 中包含一行内容
    }
  • fread(): 以二进制块形式读取数据。
    int data[5];
    size_t items_read = fread(data, sizeof(int), 5, fp); // 读取5个int

5 定位文件指针

  • fseek(): 将文件指针移动到指定位置。
    fseek(fp, 0, SEEK_SET); // 移动到文件开头 (SEEK_SET: 0)
    fseek(fp, 10, SEEK_CUR); // 从当前位置向后移动10字节 (SEEK_CUR: 1)
    fseek(fp, -10, SEEK_END); // 从文件末尾向前移动10字节 (SEEK_END: 2)
  • ftell(): 获取当前文件指针的位置(相对于文件开头)。
    long position = ftell(fp);
  • rewind(): 将文件指针重置到文件开头,相当于 fseek(fp, 0, SEEK_SET)

6 判断文件结束与错误

  • feof(): 检查是否到达文件结尾。
    while (!feof(fp)) {
        // 循环读取直到文件末尾
    }

    注意feof() 在读取到文件末尾的下一次调用时才会返回真,在循环条件中直接判断 fread()fgets() 的返回值更可靠。

    linux c语言文件读写
    (图片来源网络,侵删)
  • ferror(): 检查文件流是否发生错误。
    if (ferror(fp)) {
        perror("Error reading from file");
    }
  • perror(): 打印一条描述性错误信息到标准错误流 (stderr)。
    if (fp == NULL) {
        perror("fopen failed"); // 会打印 "fopen failed: [具体的系统错误信息,如 No such file or directory]"
    }

Linux 系统调用 I/O (底层接口)

当你需要最高性能或访问标准库不支持的文件类型(如 /dev/null)时,可以使用系统调用。

1 打开文件:open()

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int open(const char *pathname, int flags, mode_t mode);
  • pathname: 文件路径。
  • flags: 打开标志,常用标志(可通过 组合):
    • O_RDONLY: 只读
    • O_WRONLY: 只写
    • O_RDWR: 读写
    • O_CREAT: 如果文件不存在,则创建它,需要提供 mode 参数。
    • O_TRUNC: 如果文件存在且以写/读写方式打开,则清空文件。
    • O_APPEND: 每次写入都追加到文件末尾。
  • mode: 创建文件时的权限(仅当 O_CREAT 被设置时才有效)。0644 (rw-r--r--)。
  • 返回值:成功返回文件描述符(一个整数),失败返回 -1

2 关闭文件:close()

#include <unistd.h>
int close(int fd);
  • fd: open() 返回的文件描述符。
  • 返回值:成功返回 0,失败返回 -1

3 写入数据:write()

#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
  • fd: 文件描述符。
  • buf: 要写入数据的缓冲区指针。
  • count: 要写入的字节数。
  • 返回值:成功返回实际写入的字节数,失败返回 -1

4 读取数据:read()

#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
  • fd: 文件描述符。
  • buf: 存放读取数据的缓冲区指针。
  • count: 要读取的最大字节数。
  • 返回值:成功返回实际读取的字节数(可能小于 count),到达文件末尾返回 0,失败返回 -1

5 定位文件指针:lseek()

#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
  • fd: 文件描述符。
  • offset: 偏移量。
  • whence: 起始位置,与 fseek() 相同:SEEK_SET, SEEK_CUR, SEEK_END
  • 返回值:成功返回新的文件指针位置,失败返回 -1

综合示例与对比

示例1:使用 C 标准库写入并读取文本文件

#include <stdio.h>
#include <string.h>
int main() {
    FILE *fp;
    char filename[] = "test_std.txt";
    char buffer[100];
    // --- 写入 ---
    fp = fopen(filename, "w"); // 以写模式打开
    if (fp == NULL) {
        perror("Failed to open file for writing");
        return 1;
    }
    fprintf(fp, "Hello from C Standard I/O!\n");
    fputs("This is the second line.\n", fp);
    fclose(fp);
    printf("File written successfully.\n");
    // --- 读取 ---
    fp = fopen(filename, "r"); // 以读模式打开
    if (fp == NULL) {
        perror("Failed to open file for reading");
        return 1;
    }
    printf("\n--- Reading file content ---\n");
    while (fgets(buffer, sizeof(buffer), fp) != NULL) {
        printf("%s", buffer);
    }
    fclose(fp);
    return 0;
}

示例2:使用 Linux 系统调用写入并读取二进制文件

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main() {
    int fd;
    char filename[] = "test_sys.dat";
    char data[] = "Hello from System Calls!";
    char read_buffer[50];
    ssize_t bytes_written, bytes_read;
    // --- 写入 ---
    // O_WRONLY | O_CREAT | O_TRUNC: 只写,创建,清空
    // 0644: 权限 rw-r--r--
    fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (fd == -1) {
        perror("Failed to open file for writing");
        return 1;
    }
    bytes_written = write(fd, data, strlen(data));
    if (bytes_written == -1) {
        perror("Failed to write to file");
        close(fd);
        return 1;
    }
    printf("Wrote %zd bytes.\n", bytes_written);
    close(fd);
    // --- 读取 ---
    fd = open(filename, O_RDONLY);
    if (fd == -1) {
        perror("Failed to open file for reading");
        return 1;
    }
    printf("\n--- Reading file content ---\n");
    bytes_read = read(fd, read_buffer, sizeof(read_buffer) - 1);
    if (bytes_read == -1) {
        perror("Failed to read from file");
        close(fd);
        return 1;
    }
    read_buffer[bytes_read] = '\0'; // 确保字符串正确终止
    printf("Read %zd bytes: %s\n", bytes_read, read_buffer);
    close(fd);
    return 0;
}

最佳实践与注意事项

  1. 检查返回值:所有 I/O 函数都可能失败。务必检查它们的返回值,并根据错误情况进行处理(如使用 perror 打印错误信息)。
  2. 关闭文件:使用完文件后,一定要记得关闭,否则可能导致文件描述符泄漏(系统资源耗尽)或数据丢失(缓冲区中的数据可能没有真正写入磁盘)。
  3. 选择合适的接口
    • 优先使用 C 标准库 (fopen, fread 等):它更安全、更方便,并且自带缓冲机制,对大多数应用来说性能已经足够好。
    • 仅在必要时使用系统调用 (open, read 等):在需要处理大量小文件、追求极致性能、或与设备/网络交互时。
  4. 缓冲区大小:对于大文件读写,合理设置缓冲区大小可以显著提高性能,标准库会自动管理缓冲区,而使用系统调用时,你需要自己管理读写缓冲区。
  5. 二进制 vs. 文本模式:在 Windows 平台上,C 标准库的 "b" 模式(如 "rb", "wb")非常重要,因为它会处理 \r\n\n 的转换,在 Linux 上,这个区别不大,但为了代码的可移植性,处理二进制文件时最好也加上 "b" 模式。
  6. 原子性操作:如果多个进程同时读写同一个文件,可能会发生数据错乱,如果需要原子性操作(如写入一个完整的记录),可以考虑使用 O_APPEND 标志(系统调用)或文件锁(flockfcntl)。
-- 展开阅读全文 --
头像
Linux C格式化输出,如何掌握格式化字符串?
« 上一篇 今天
C语言中default关键字的作用是什么?
下一篇 » 今天
取消
微信二维码
支付宝二维码

目录[+]