首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >【Linux文件操作】C库函数操作

【Linux文件操作】C库函数操作

作者头像
byte轻骑兵
发布2026-01-21 19:55:05
发布2026-01-21 19:55:05
1400
举报

在 Linux 系统编程领域,文件操作是开发者必须掌握的核心技能。C 语言标准库提供了一套完善的文件操作函数,这些函数在底层系统调用的基础上进行了封装,既简化了编程复杂度,又保证了良好的跨平台兼容性。本文将系统讲解 Linux 环境下使用 C 库函数进行文件操作的全流程,涵盖文件的创建、打开、读写和关闭等关键环节。​


一、文件操作的基础认知

在深入函数细节之前,有必要先理解 Linux 文件系统的基本特性和 C 库文件操作的设计思想。这些基础概念将帮助我们更好地理解后续函数的工作原理。​

Linux 系统遵循 "一切皆文件" 的设计哲学,无论是普通文本文件、二进制文件,还是设备、管道、网络套接字等,都通过统一的文件接口进行操作。这种抽象使得文件操作具有极高的一致性,也为 C 库函数的封装提供了便利。​

C 库文件操作的核心是文件流(FILE) 结构体,它是对底层文件描述符的封装,包含了缓冲区状态、文件位置指针、错误标志等关键信息。程序员通过文件指针(FILE*) 与文件流交互,无需直接操作底层细节。这种封装带来两个显著优势:一是通过用户空间缓冲区减少系统调用次数,大幅提升 IO 效率;二是隐藏了不同系统的底层差异,保证了代码的可移植性。​

文件操作的基本流程遵循 "打开 - 操作 - 关闭" 的经典模式。在使用文件前必须先通过函数获取文件指针,所有操作都通过该指针完成,操作结束后必须关闭文件以释放资源。特别需要注意的是,Linux 系统对进程可打开的文件数量存在限制(默认通常为 1024),未正确关闭的文件会导致文件描述符泄露,最终引发 "too many open files" 错误。​

二、文件的创建与打开​

创建和打开文件是所有文件操作的起点。C 库中最常用的函数是fopen(),它承担了文件创建、打开和初始化缓冲区的全部工作。​

2.1 fopen () 函数的参数与用法​

函数原型:

代码语言:javascript
复制
FILE *fopen(const char *path, const char *mode);

参数解析:​

  • path:字符串表示的文件路径,可以是绝对路径(如/var/log/syslog)或相对路径(如./config.ini)。路径解析遵循 Linux 文件系统的层级结构,.表示当前目录,..表示上级目录。​
  • mode:打开模式字符串,决定了文件的访问权限和操作方式,常用模式如下表所示:​​

模式​

含义​

文本 / 二进制​

若文件不存在​

写入行为​

"r"​

只读​

文本​

打开失败​

不允许​

"w"​

只写​

文本​

创建新文件​

覆盖原有内容​

"a"​

追加​

文本​

创建新文件​

写入到文件末尾​

"r+"​

读写​

文本​

打开失败​

从当前位置写入​

"w+"​

读写​

文本​

创建新文件​

覆盖原有内容​

"a+"​

读写​

文本​

创建新文件​

写入到文件末尾​

"rb"​

只读​

二进制​

打开失败​

不允许​

"wb"​

只写​

二进制​

创建新文件​

覆盖原有内容​

"ab"​

追加​

二进制​

创建新文件​

写入到文件末尾​

模式字符串中的 "b" 表示二进制模式,这在 Windows 系统中会影响换行符的处理(文本模式会自动转换\n与\r\n),在 Linux 系统中虽无实质差异,但添加 "b" 可明确操作类型,提高代码可读性和可移植性。​

返回值处理:​

  • 成功:返回指向 FILE 结构体的文件指针,后续所有操作都通过该指针进行​
  • 失败:返回 NULL,并设置全局变量errno指示错误原因​

错误处理示例:

代码语言:javascript
复制
#include <stdio.h>
#include <errno.h>
#include <string.h>

int main() {
    FILE *fp = fopen("secret.dat", "r");
    if (fp == NULL) {
        // 使用strerror()将errno转换为可读字符串
        fprintf(stderr, "打开文件失败: %s\n", strerror(errno));
        // 常见错误类型:ENOENT(文件不存在)、EACCES(权限不足)、EISDIR(路径指向目录)
        return 1;
    }
    // 文件操作...
    fclose(fp);
    return 0;
}

2.2 文件创建的高级方式​

fopen()函数创建文件时使用默认权限(通常是 0666,受 umask 影响),如果需要精确控制文件权限,可结合系统调用open()与fdopen()函数:

代码语言:javascript
复制
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>

int main() {
    // 创建权限为0600的文件(仅所有者可读写)
    int fd = open("private.txt", O_WRONLY | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR);
    if (fd == -1) {
        perror("open failed");
        return 1;
    }
    // 将文件描述符转换为文件指针
    FILE *fp = fdopen(fd, "w");
    if (fp == NULL) {
        perror("fdopen failed");
        close(fd); // 转换失败时需手动关闭文件描述符
        return 1;
    }
    // 使用文件指针操作...
    fputs("敏感数据", fp);
    fclose(fp); // fclose会自动关闭对应的文件描述符
    return 0;
}

O_EXCL标志确保文件不存在时才创建,避免意外覆盖已有文件,这在实现日志轮转、临时文件等场景中非常有用。fdopen()函数的作用是将底层文件描述符(由open()返回)转换为 C 库的文件指针,从而兼顾权限控制和缓冲 IO 的优势。​

三、文件的读写操作​

文件打开后,核心操作就是数据的读取和写入。C 库提供了多套读写函数,分别适用于不同场景:字符级、行级和块级操作,开发者可根据实际需求选择。​

3.1 字符级读写:fgetc () 与 fputc ()​

fgetc () 函数:从文件流中读取单个字符

代码语言:javascript
复制
int fgetc(FILE *stream);
  • 工作原理:从文件流的缓冲区读取一个字符,缓冲区为空时会自动从内核读取数据​
  • 返回值:成功返回字符的 ASCII 码(转换为 int 类型),失败或到达文件尾返回 EOF(通常定义为 - 1)​

fputc () 函数:向文件流写入单个字符

代码语言:javascript
复制
int fputc(int c, FILE *stream);
  • 参数:c 为要写入的字符(以 int 形式传递,实际只使用低 8 位)​
  • 返回值:成功返回写入的字符,失败返回 EOF​

示例:实现文件复制功能

代码语言:javascript
复制
void copy_file(const char *src_path, const char *dest_path) {
    FILE *src = fopen(src_path, "r");
    FILE *dest = fopen(dest_path, "w");
    if (src == NULL || dest == NULL) {
        perror("文件打开失败");
        // 避免内存泄漏:关闭已成功打开的文件
        if (src) fclose(src);
        if (dest) fclose(dest);
        return;
    }
    
    int c;
    // 循环读取并写入,直到遇到EOF
    while ((c = fgetc(src)) != EOF) {
        if (fputc(c, dest) == EOF) {
            perror("写入失败");
            break;
        }
    }
    
    // 检查读取过程是否出现错误
    if (ferror(src)) {
        perror("读取失败");
    }
    
    // 关闭文件
    fclose(src);
    fclose(dest);
}

字符级函数适合处理文本文件中的逐个字符,但效率较低,不适合大文件操作。​

3.2 行级读写:fgets () 与 fputs ()​

fgets () 函数:从文件流读取一行数据

代码语言:javascript
复制
char *fgets(char *s, int size, FILE *stream);

参数解析:

  • s:存储读取数据的缓冲区​
  • size:缓冲区大小(包括终止符 '\0')​
  • stream:目标文件指针​

工作机制:读取到换行符 '\n' 或 size-1 个字符时停止,自动在末尾添加 '\0'​

返回值:成功返回缓冲区地址 s,失败或到达文件尾返回 NULL​

注意事项:​

  1. 读取的字符串可能包含换行符,需要手动处理​
  2. 若一行长度超过 size-1,会被截断为不完整的行​
  3. 无法直接区分 "正常到达文件尾" 和 "读取错误",需用feof()和ferror()判断​

fputs () 函数:向文件流写入字符串

代码语言:javascript
复制
int fputs(const char *s, FILE *stream);
  • 功能:将字符串 s 写入文件流(不包含终止符 '\0')​
  • 特点:不会自动添加换行符,与puts()函数不同​
  • 返回值:成功返回非负值,失败返回 EOF​

示例:处理配置文件

代码语言:javascript
复制
void parse_config(const char *filename) {
    FILE *fp = fopen(filename, "r");
    if (!fp) {
        perror("打开配置文件失败");
        return;
    }
    
    char line[1024];
    int line_num = 0;
    while (fgets(line, sizeof(line), fp) != NULL) {
        line_num++;
        // 去除行尾的换行符和回车符
        size_t len = strlen(line);
        if (len > 0 && line[len-1] == '\n') {
            line[len-1] = '\0';
        }
        // 跳过空行和注释行
        if (line[0] == '#' || line[0] == '\0') {
            continue;
        }
        // 处理配置项
        printf("第%d行配置: %s\n", line_num, line);
    }
    
    // 判断循环结束原因
    if (ferror(fp)) {
        perror("读取配置文件出错");
    } else if (feof(fp)) {
        printf("配置文件处理完毕,共%d行\n", line_num);
    }
    
    fclose(fp);
}

行级函数适合处理文本文件、配置文件、日志文件等按行组织的数据,在脚本解析、日志分析等场景中应用广泛。​

3.3 块级读写:fread () 与 fwrite ()​

对于二进制文件或需要高效处理大量数据的场景,块级读写函数是更好的选择。​

fread () 函数:从文件流读取数据块

代码语言:javascript
复制
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);

参数解析:​

  • ptr:接收数据的缓冲区指针​
  • size:每个数据块的大小(字节)​
  • nmemb:要读取的数据块数量​
  • stream:目标文件指针​

返回值:成功读取的完整数据块数量,可能小于 nmemb(到达文件尾或出错)​

fwrite () 函数:向文件流写入数据块

代码语言:javascript
复制
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);

返回值:成功写入的数据块数量,若小于 nmemb 则表示出错​

示例:处理二进制数据(结构体数组)

代码语言:javascript
复制
#include <stdio.h>
#include <stdlib.h>

// 定义用户数据结构体
typedef struct {
    int id;
    char name[32];
    float salary;
} Employee;

// 写入员工数据到文件
void save_employees(const char *filename, Employee *emps, int count) {
    FILE *fp = fopen(filename, "wb");
    if (!fp) {
        perror("打开文件失败");
        return;
    }
    
    // 写入整个结构体数组
    size_t written = fwrite(emps, sizeof(Employee), count, fp);
    if (written != count) {
        fprintf(stderr, "写入不完整:预期%d条,实际%d条\n", count, written);
    } else {
        printf("成功写入%d条员工数据\n", count);
    }
    
    fclose(fp);
}

// 从文件读取员工数据
Employee* load_employees(const char *filename, int *count) {
    FILE *fp = fopen(filename, "rb");
    if (!fp) {
        perror("打开文件失败");
        return NULL;
    }
    
    // 获取文件大小
    fseek(fp, 0, SEEK_END);
    long file_size = ftell(fp);
    rewind(fp); // 重置到文件开头
    
    // 计算员工数量
    *count = file_size / sizeof(Employee);
    if (file_size % sizeof(Employee) != 0) {
        fprintf(stderr, "文件格式异常,大小不是结构体整数倍\n");
        fclose(fp);
        return NULL;
    }
    
    // 分配内存并读取数据
    Employee *emps = malloc(*count * sizeof(Employee));
    if (!emps) {
        perror("内存分配失败");
        fclose(fp);
        return NULL;
    }
    
    size_t read = fread(emps, sizeof(Employee), *count, fp);
    if (read != *count) {
        fprintf(stderr, "读取不完整:预期%d条,实际%d条\n", *count, read);
        free(emps);
        emps = NULL;
        *count = 0;
    }
    
    fclose(fp);
    return emps;
}

块级读写的优势在于效率 —— 通过一次函数调用处理多个数据单元,大幅减少函数调用开销。在处理图像、音频、数据库文件等二进制数据时是首选方式。需要注意的是,二进制数据具有平台相关性(如字节序、结构体对齐),跨平台传输时需进行格式转换。​

3.4 格式化读写:fprintf () 与 fscanf ()

格式化读写函数允许我们按照指定的格式读写数据,非常适合处理具有固定格式的文本数据。​

fprintf () 函数:按照指定格式向文件流写入数据

代码语言:javascript
复制
int fprintf(FILE *stream, const char *format, ...);

参数解析:​

  • stream:目标文件指针。​
  • format:格式控制字符串,与printf()函数的格式字符串用法相同。​
  • ...:可变参数列表,即要写入的数据。​

返回值:成功时返回写入的字符总数;失败时返回负数。​

fscanf () 函数:按照指定格式从文件流读取数据

代码语言:javascript
复制
int fscanf(FILE *stream, const char *format, ...);

参数解析:​

  • stream:目标文件指针。​
  • format:格式控制字符串,与scanf()函数的格式字符串用法相同。​
  • ...:指向变量的指针列表,用于存储读取到的数据。​

返回值:成功时返回成功读取并赋值的变量个数;到达文件末尾时返回 EOF;失败时返回小于 EOF 的值。​

示例:使用格式化函数读写学生信息

代码语言:javascript
复制
#include <stdio.h>
#include <stdlib.h>

typedef struct {
    int id;
    char name[20];
    float score;
} Student;

// 写入学生信息
void write_students(const char *filename) {
    FILE *fp = fopen(filename, "w");
    if (fp == NULL) {
        perror("打开文件失败");
        return;
    }
    
    Student students[] = {
        {1, "张三", 90.5},
        {2, "李四", 85.0},
        {3, "王五", 92.5}
    };
    int count = sizeof(students) / sizeof(Student);
    
    for (int i = 0; i < count; i++) {
        // 按照"id,name,score"的格式写入
        fprintf(fp, "%d,%s,%.1f\n", students[i].id, students[i].name, students[i].score);
    }
    
    fclose(fp);
}

// 读取学生信息
void read_students(const char *filename) {
    FILE *fp = fopen(filename, "r");
    if (fp == NULL) {
        perror("打开文件失败");
        return;
    }
    
    Student s;
    int count = 0;
    // 按照格式读取数据
    while (fscanf(fp, "%d,%[^,],%f\n", &s.id, s.name, &s.score) == 3) {
        count++;
        printf("ID: %d, 姓名: %s, 成绩: %.1f\n", s.id, s.name, s.score);
    }
    
    printf("共读取%d条学生信息\n", count);
    fclose(fp);
}

int main() {
    write_students("students.txt");
    read_students("students.txt");
    return 0;
}

fscanf()函数中的%[^,]格式表示读取除逗号之外的所有字符,直到遇到逗号为止,非常适合读取 CSV 格式的数据。格式化读写函数在处理具有固定格式的配置文件、日志文件等场景中非常方便,但需要注意格式字符串与数据格式的匹配,否则可能导致读取错误。

四、文件的关闭操作​

文件操作完成后,必须及时关闭文件以释放资源。fclose()函数负责这一关键工作,它不仅关闭文件描述符,还会刷新缓冲区确保数据完整性。​

4.1 fclose () 函数的用法与原理​

函数原型:

代码语言:javascript
复制
int fclose(FILE *stream);

功能:​

  1. 刷新用户空间缓冲区(将未写入内核的数据强制写入)​
  2. 关闭底层文件描述符​
  3. 释放 FILE 结构体占用的内存​

返回值:成功返回 0,失败返回 EOF(通常因刷新缓冲区时出错)​

示例:安全关闭文件

代码语言:javascript
复制
FILE *fp = fopen("data.txt", "w");
if (!fp) {
    perror("打开失败");
    return 1;
}

// 执行写入操作...
fputs("重要数据", fp);

// 关闭文件并检查错误
if (fclose(fp) == EOF) {
    perror("关闭文件失败");
    // 这里需要特别注意:数据可能未完全写入
    return 1;
}

4.2 关闭文件的重要性与最佳实践​

1. 资源释放:​

  • 每个进程可打开的文件数量有限(通过ulimit -n查看)​
  • 未关闭的文件会导致文件描述符泄露,最终引发 "too many open files" 错误​
  • 长时间运行的服务程序(如守护进程)必须严格确保文件关闭​

2. 数据完整性:

代码语言:javascript
复制
// 手动刷新缓冲区
if (fflush(fp) == EOF) {
    perror("刷新缓冲区失败");
}
  • C 库使用缓冲机制,数据会先存储在用户空间缓冲区​
  • 缓冲区满或文件关闭时才会刷新到内核​
  • 程序异常退出可能导致缓冲区数据丢失​
  • 关键数据可使用fflush()手动刷新

3. 异常处理策略:

代码语言:javascript
复制
void process_files() {
    FILE *f1 = fopen("a.txt", "r");
    FILE *f2 = fopen("b.txt", "w");
    
    if (!f1 || !f2) {
        perror("打开文件失败");
        goto cleanup; // 跳转到清理代码
    }
    
    // 执行文件操作...
    if (some_error) {
        goto cleanup;
    }
    
cleanup:
    if (f1) fclose(f1);
    if (f2) fclose(f2);
}

4.3 特殊场景的处理

  • 多次关闭同一文件:对已关闭的文件指针再次调用fclose(),会导致未定义行为(可能崩溃或损坏数据),因此应确保每个文件只被关闭一次。
  • 程序异常退出时的关闭:若程序因信号或错误异常退出,fclose()可能无法被调用,导致数据丢失。此时可使用fflush()函数手动刷新缓冲区,或使用atexit()注册清理函数,确保程序退出前关闭文件。
代码语言:javascript
复制
// 使用atexit()注册文件关闭函数
FILE *fp;

void close_file() {
    if (fp != NULL) {
        fclose(fp);
        fp = NULL;
    }
}

int main() {
    fp = fopen("log.txt", "a");
    if (fp == NULL) {
        perror("打开日志文件失败");
        return 1;
    }
    atexit(close_file); // 注册程序退出时的清理函数
    
    // 程序逻辑...
    return 0;
}

五、高级主题与最佳实践

5.1 跨平台路径处理

兼容Windows/Linux的路径

代码语言:javascript
复制
#ifdef _WIN32
#define PATH_SEPARATOR '\\'
#else
#define PATH_SEPARATOR '/'
#endif

void create_path(const char *dir, const char *file) {
    char path[1024];
    snprintf(path, sizeof(path), "%s%c%s", dir, PATH_SEPARATOR, file);
    // 使用path进行文件操作...
}

5.2 原子操作与并发安全

使用O_EXCL创建唯一文件(需结合open系统调用):

代码语言:javascript
复制
#include <fcntl.h>
#include <unistd.h>

int create_unique_file(const char *path) {
    int fd = open(path, O_WRONLY | O_CREAT | O_EXCL, 0644);
    if (fd == -1) {
        perror("文件已存在或创建失败");
        return -1;
    }
    // 使用fd进行操作...
    close(fd);
    return 0;
}

C库替代方案:对于简单场景,可使用fopen+检查文件是否存在(非原子操作)。

5.3 内存映射文件(高级)

虽然不属于标准C库,但值得了解的替代方案:

代码语言:javascript
复制
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>

void map_file(const char *path) {
    int fd = open(path, O_RDONLY);
    struct stat sb;
    fstat(fd, &sb);
    
    char *addr = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
    if (addr == MAP_FAILED) {
        perror("内存映射失败");
        close(fd);
        return;
    }
    
    // 直接访问文件内容如同内存
    printf("文件内容: %s\n", addr);
    
    munmap(addr, sb.st_size);
    close(fd);
}

适用场景

  • 需要随机访问大文件(>1GB)。
  • 追求极致性能的特殊应用。

六、完整案例:学生信息管理系统

代码语言:javascript
复制
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAX_NAME_LEN 50
#define DB_FILE "students.db"

typedef struct {
    int id;
    char name[MAX_NAME_LEN];
    float gpa;
} Student;

void save_student(const Student *s) {
    FILE *fp = fopen(DB_FILE, "ab"); // 追加二进制模式
    if (fp == NULL) {
        perror("数据库打开失败");
        return;
    }
    fwrite(s, sizeof(Student), 1, fp);
    fclose(fp);
}

void list_students() {
    FILE *fp = fopen(DB_FILE, "rb");
    if (fp == NULL) {
        printf("数据库为空或无法打开\n");
        return;
    }
    
    Student s;
    printf("%-10s %-20s %-10s\n", "ID", "姓名", "GPA");
    while (fread(&s, sizeof(Student), 1, fp) == 1) {
        printf("%-10d %-20s %-10.2f\n", s.id, s.name, s.gpa);
    }
    
    fclose(fp);
}

int main() {
    int choice;
    Student s;
    
    while (1) {
        printf("\n学生信息管理系统\n");
        printf("1. 添加学生\n2. 显示所有\n3. 退出\n选择: ");
        scanf("%d", &choice);
        
        switch (choice) {
            case 1:
                printf("输入学号: ");
                scanf("%d", &s.id);
                printf("输入姓名: ");
                scanf("%s", s.name);
                printf("输入GPA: ");
                scanf("%f", &s.gpa);
                save_student(&s);
                break;
            case 2:
                list_students();
                break;
            case 3:
                exit(0);
            default:
                printf("无效选择\n");
        }
    }
    
    return 0;
}
  1. 使用二进制模式存储结构化数据。
  2. 追加写入避免覆盖已有记录。
  3. 简单的控制台界面演示核心操作。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-08-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、文件操作的基础认知
  • 二、文件的创建与打开​
    • 2.1 fopen () 函数的参数与用法​
    • 2.2 文件创建的高级方式​
  • 三、文件的读写操作​
    • 3.1 字符级读写:fgetc () 与 fputc ()​
    • 3.2 行级读写:fgets () 与 fputs ()​
    • 3.3 块级读写:fread () 与 fwrite ()​
    • 3.4 格式化读写:fprintf () 与 fscanf ()
  • 四、文件的关闭操作​
    • 4.1 fclose () 函数的用法与原理​
    • 4.2 关闭文件的重要性与最佳实践​
    • 4.3 特殊场景的处理
  • 五、高级主题与最佳实践
    • 5.1 跨平台路径处理
    • 5.2 原子操作与并发安全
    • 5.3 内存映射文件(高级)
  • 六、完整案例:学生信息管理系统
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档