Linux C文件操作如何高效读写与错误处理?

99ANYc3cd6
预计阅读时长 29 分钟
位置: 首页 C语言 正文

Linux 中的文件操作主要分为两大类:

linux c语言 文件操作
(图片来源网络,侵删)
  1. 低级 I/O (Unbuffered I/O):也称为系统调用,直接与内核交互,效率高,但每次操作都会涉及系统调用开销,编程接口更原始(使用文件描述符 int 类型)。
  2. 高级 I/O (Buffered I/O):也称为标准 I/O 库,在用户空间提供了缓冲机制,减少了系统调用的次数,更方便、更安全(使用 FILE* 指针)。

对于绝大多数应用场景,我们都推荐使用高级 I/O (标准 I/O 库),除非你有特殊需求(如追求极致性能或进行某些底层开发)。


低级 I/O (系统调用)

这部分函数在 <unistd.h> 头文件中声明。

核心概念:文件描述符

  • 是什么:一个非负整数,是内核为了高效管理已打开的文件而创建的索引。
  • 范围:在进程中,0, 1, 2 是三个标准文件描述符,默认已经打开:
    • 0:标准输入
    • 1:标准输出
    • 2:标准错误
  • 获取:后续打开的文件描述符会从 3 开始,按顺序递增。

主要函数

open() - 打开或创建文件

#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.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_EXCL: 与 O_CREAT 一起使用,如果文件已存在则返回错误。
      • O_TRUNC: 如果文件存在且以只写或读写方式成功打开,则清空文件内容。
      • O_APPEND: 每次写入都追加到文件末尾。
    • mode: 当使用 O_CREAT 时,指定文件的权限(如 0644),注意,这个权限会受到进程 umask 值的影响。
  • 返回值
    • 成功:返回一个新的文件描述符(一个非负整数)。
    • 失败:返回 -1,并设置 errno

read() - 从文件读取数据

#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
  • 参数
    • fd: 文件描述符。
    • buf: 存储读取数据的缓冲区。
    • count: 请求读取的最大字节数。
  • 返回值
    • 成功:返回实际读取到的字节数,如果到文件末尾,返回 0
    • 失败:返回 -1,并设置 errno

write() - 向文件写入数据

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

close() - 关闭文件描述符

#include <unistd.h>
int close(int fd);
  • 参数fd:要关闭的文件描述符。
  • 返回值
    • 成功:返回 0
    • 失败:返回 -1,并设置 errno

lseek() - 移动文件读写指针

#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
  • 参数
    • fd: 文件描述符。
    • offset: 偏移量。
    • whence: 起始位置:
      • SEEK_SET: 从文件开头计算。
      • SEEK_CUR: 从当前读写指针位置计算。
      • SEEK_END: 从文件末尾计算。
  • 返回值
    • 成功:返回新的文件读写指针位置(相对于文件开头的字节偏移量)。
    • 失败:返回 (off_t)-1,并设置 errno

高级 I/O (标准 I/O 库)

这部分函数在 <stdio.h> 头文件中声明。

核心概念:文件流

  • 是什么:一个指向 FILE 结构体的指针(FILE*)。FILE 结构体包含了文件描述符、缓冲区、当前读写位置等信息。
  • 优势:提供了全缓冲、行缓冲和无缓冲三种模式,减少了直接调用 read/write 的次数,提高了效率,并提供了格式化 I/O 功能(如 printf, scanf)。

主要函数

fopen() - 打开文件

#include <stdio.h>
FILE *fopen(const char *pathname, const char *mode);
  • 参数
    • pathname: 文件路径。
    • mode: 打开模式:
      • "r": 只读,文件必须存在。
      • "w": 只写,文件若存在则清空,不存在则创建。
      • "a": 追加,文件若不存在则创建,写入内容追加到末尾。
      • "r+": 读写,文件必须存在。
      • "w+": 读写,文件若存在则清空,不存在则创建。
      • "a+": 读写,文件若不存在则创建,写入内容追加到末尾。
  • 返回值
    • 成功:返回 FILE* 指针。
    • 失败:返回 NULL

fread() - 从文件流读取数据

#include <stdio.h>
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
  • 参数
    • ptr: 存储读取数据的缓冲区。
    • size: 每个数据项的大小(字节)。
    • nmemb: 要读取的数据项数量。
    • stream: 文件流指针。
  • 返回值:成功读取的数据项的数量(不是字节数),如果到文件末尾或出错,可能小于 nmemb

fwrite() - 向文件流写入数据

#include <stdio.h>
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
  • 参数:同 fread
  • 返回值:成功写入的数据项的数量。

fclose() - 关闭文件流

#include <stdio.h>
int fclose(FILE *stream);

fgetc() / fputc() - 读写单个字符

#include <stdio.h>
int fgetc(FILE *stream); // 读取一个字符
int fputc(int c, FILE *stream); // 写入一个字符

fgets() / fputs() - 读写一行字符串

#include <stdio.h>
char *fgets(char *s, int size, FILE *stream); // 读取一行
int fputs(const char *s, FILE *stream); // 写入一行

fscanf() / fprintf() - 格式化读写

#include <stdio.h>
int fscanf(FILE *stream, const char *format, ...); // 从文件流格式化读取
int fprintf(FILE *stream, const char *format, ...); // 向文件流格式化写入

fseek() / ftell() - 移动和获取文件指针

#include <stdio.h>
int fseek(FILE *stream, long offset, int whence);
long ftell(FILE *stream);
  • fseeklseek 类似,但参数是 long 类型。
  • ftell 用于获取当前文件指针的位置(相对于文件开头)。

rewind() - 重置文件指针

#include <stdio.h>
void rewind(FILE *stream);
  • 相当于 fseek(stream, 0, SEEK_SET)

fflush() - 刷新缓冲区

#include <stdio.h>
int fflush(FILE *stream);
  • 将缓冲区中的数据立即写入文件,对于输出流,fflush(NULL) 会刷新所有打开的输出流。

错误处理

在文件操作中,错误处理至关重要。

linux c语言 文件操作
(图片来源网络,侵删)
  • 系统调用:失败时返回 -1,全局变量 errno 会被设置为一个特定的错误码(如 ENOENT 表示文件不存在),需要包含 <errno.h>
  • 标准 I/O 库:失败时通常返回 NULLEOF,可以通过 perror() 函数打印出系统描述的错误信息,或者使用 strerror(errno) 将错误码转换为字符串。

示例代码:

#include <stdio.h>
#include <errno.h>
#include <string.h>
int main() {
    FILE *fp = fopen("non_existent_file.txt", "r");
    if (fp == NULL) {
        // 使用 perror 打印 "fopen: No such file or directory"
        perror("fopen"); 
        // 或者使用 strerror
        // fprintf(stderr, "Error opening file: %s\n", strerror(errno));
        return 1;
    }
    // ... 正常操作 ...
    fclose(fp);
    return 0;
}

综合示例

示例 1:使用标准 I/O 库复制文件 (推荐方式)

#include <stdio.h>
#define BUFFER_SIZE 4096
int main(int argc, char *argv[]) {
    if (argc != 3) {
        fprintf(stderr, "Usage: %s <source> <destination>\n", argv[0]);
        return 1;
    }
    FILE *src_fp = fopen(argv[1], "rb"); // "rb" for binary read, to avoid newline translation
    if (src_fp == NULL) {
        perror("fopen source");
        return 1;
    }
    FILE *dest_fp = fopen(argv[2], "wb"); // "wb" for binary write
    if (dest_fp == NULL) {
        perror("fopen destination");
        fclose(src_fp); // 确保关闭已打开的源文件
        return 1;
    }
    char buffer[BUFFER_SIZE];
    size_t bytes_read;
    // 循环读取直到文件末尾
    while ((bytes_read = fread(buffer, 1, BUFFER_SIZE, src_fp)) > 0) {
        // 将读取到的字节写入目标文件
        size_t bytes_written = fwrite(buffer, 1, bytes_read, dest_fp);
        if (bytes_written != bytes_read) {
            perror("fwrite");
            break; // 写入出错,跳出循环
        }
    }
    if (ferror(src_fp)) {
        perror("fread");
    }
    // 关闭文件
    fclose(src_fp);
    fclose(dest_fp);
    printf("File copied successfully.\n");
    return 0;
}

示例 2:使用系统调用复制文件

#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#define BUFFER_SIZE 4096
int main(int argc, char *argv[]) {
    if (argc != 3) {
        fprintf(stderr, "Usage: %s <source> <destination>\n", argv[0]);
        return 1;
    }
    int src_fd = open(argv[1], O_RDONLY);
    if (src_fd == -1) {
        perror("open source");
        return 1;
    }
    // O_CREAT | O_WRONLY | O_TRUNC: 如果文件不存在则创建,只写,如果存在则清空
    // 0644: 文件权限 (所有者读写,组和其他用户只读)
    int dest_fd = open(argv[2], O_CREAT | O_WRONLY | O_TRUNC, 0644);
    if (dest_fd == -1) {
        perror("open destination");
        close(src_fd);
        return 1;
    }
    char buffer[BUFFER_SIZE];
    ssize_t bytes_read;
    while ((bytes_read = read(src_fd, buffer, BUFFER_SIZE)) > 0) {
        ssize_t bytes_written = write(dest_fd, buffer, bytes_read);
        if (bytes_written == -1) {
            perror("write");
            break;
        }
    }
    if (bytes_read == -1) {
        perror("read");
    }
    close(src_fd);
    close(dest_fd);
    printf("File copied successfully.\n");
    return 0;
}

总结与最佳实践

特性 低级 I/O (系统调用) 高级 I/O (标准 I/O 库)
头文件 <unistd.h>, <fcntl.h> <stdio.h>
核心对象 文件描述符 (int) 文件流 (FILE*)
缓冲机制 无缓冲 有缓冲 (全/行/无)
效率 单次操作快,但频繁调用效率低 高,因为减少了系统调用次数
易用性 较低,需要手动管理 高,提供丰富的格式化 I/O 函数
适用场景 设备文件、管道、套接字、追求极致性能 几乎所有常规的磁盘文件操作

最佳实践建议:

  1. 优先使用标准 I/O 库 (fopen, fread, fwrite 等):除非你有充分的理由(如性能瓶颈或特殊设备操作),否则应始终使用标准 I/O 库,它更安全、更方便、效率也足够高。
  2. 检查所有函数的返回值:这是编写健壮 C 程序的基本要求,永远不要假设文件操作会成功。
  3. 处理错误信息:使用 perrorstrerror 为用户提供有意义的错误信息,方便调试。
  4. 始终关闭文件:使用 fcloseclose 释放资源,避免文件描述符泄漏,在 open/fopen 成功后,后续操作失败时,要记得关闭之前已成功打开的文件。
  5. 使用合适的缓冲区大小:在进行大文件复制或读写时,使用一个较大的缓冲区(如 4KB 或 8KB)可以显著提高性能,因为它减少了 I/O 操作的次数。
linux c语言 文件操作
(图片来源网络,侵删)
-- 展开阅读全文 --
头像
C语言中long和int有何区别与选择?
« 上一篇 02-10
dede5.7后台页面空白
下一篇 » 02-10

相关文章

取消
微信二维码
支付宝二维码

目录[+]