C语言文件随机读写的完全指南

在C语言中,文件随机读写(也称为直接访问随机访问)允许程序在文件的任意位置进行读写操作,而不仅限于顺序读写。这在处理大型文件、数据库文件或需要频繁访问特定数据块的应用中尤为重要。本文将详细介绍C语言中实现文件随机读写的各种方法、相关函数、使用技巧以及注意事项,帮助你全面掌握这一重要技能。

图片[1]_C语言文件随机读写的完全指南_知途无界

一、文件随机读写的基本概念

1. 什么是文件随机读写?

文件随机读写,指的是程序能够在文件的任意位置进行读取或写入数据,而不需要按照数据在文件中存储的顺序依次进行。这与顺序读写相对,顺序读写需要从文件的起始位置开始,按照数据的排列顺序逐一读取或写入。

2. 为什么需要文件随机读写?

  • 高效访问​:对于大型文件,直接访问特定数据块比顺序查找更高效。
  • 数据更新​:在数据库或索引文件中,经常需要更新特定记录而不影响其他部分。
  • 灵活性​:允许程序根据需要动态地读取或写入文件的不同部分,适应复杂的数据处理需求。

二、文件随机读写的关键函数

在C语言中,文件随机读写主要依赖于标准库中的 <stdio.h> 提供的文件操作函数,特别是与文件指针定位相关的函数。以下是实现文件随机读写的核心函数:

1. fseek 函数

功能​:设置文件指针的位置,实现文件的随机访问。

原型​:

int fseek(FILE *stream, long int offset, int whence);

参数​:

  • stream:指向 FILE 对象的指针,标识要操作的文件流。
  • offset:偏移量,以字节为单位,表示从 whence 位置开始移动的字节数。
  • whence:起始位置,指定偏移量的参考点,可以是以下值之一:
    • SEEK_SET:文件开头(0)。
    • SEEK_CUR:当前文件指针位置(1)。
    • SEEK_END:文件末尾(2)。

返回值​:

  • 成功时返回 0
  • 失败时返回非零值(通常是 -1)。

示例​:

FILE *file = fopen("example.txt", "rb+");
if (file == NULL) {
    perror("无法打开文件");
    return 1;
}

// 将文件指针移动到文件的第100个字节处
if (fseek(file, 100, SEEK_SET) != 0) {
    perror("fseek失败");
    fclose(file);
    return 1;
}

// 现在可以从第100个字节开始读取或写入

2. ftell 函数

功能​:获取当前文件指针的位置,通常用于记录位置以便后续返回。

原型​:

long int ftell(FILE *stream);

参数​:

  • stream:指向 FILE 对象的指针,标识要操作的文件流。

返回值​:

  • 成功时返回当前文件指针的位置(从文件开头计算的字节数)。
  • 失败时返回 -1L

示例​:

long int current_pos = ftell(file);
if (current_pos == -1L) {
    perror("ftell失败");
    fclose(file);
    return 1;
}
printf("当前文件指针位置:%ld\n", current_pos);

3. rewind 函数

功能​:将文件指针重新定位到文件的开头,同时清除文件结束标志和错误标志。

原型​:

void rewind(FILE *stream);

参数​:

  • stream:指向 FILE 对象的指针,标识要操作的文件流。

示例​:

rewind(file); // 将文件指针移动到文件开头

4. fgetposfsetpos 函数(更可移植的方式)

功能​:fgetpos 用于获取文件指针的位置,fsetpos 用于设置文件指针的位置。它们使用 fpos_t 类型,提供了比 ftellfseek 更可移植的文件位置管理方式,尤其适用于处理大文件。

原型​:

int fgetpos(FILE *stream, fpos_t *pos);
int fsetpos(FILE *stream, const fpos_t *pos);

参数​:

  • stream:指向 FILE 对象的指针,标识要操作的文件流。
  • pos:指向 fpos_t 对象的指针,用于存储或设置文件位置。

返回值​:

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

示例​:

fpos_t position;

// 获取当前文件位置
if (fgetpos(file, &position) != 0) {
    perror("fgetpos失败");
    fclose(file);
    return 1;
}

// 移动文件指针到其他位置
if (fseek(file, 200, SEEK_SET) != 0) {
    perror("fseek失败");
    fclose(file);
    return 1;
}

// 执行一些读写操作

// 恢复到之前的位置
if (fsetpos(file, &position) != 0) {
    perror("fsetpos失败");
    fclose(file);
    return 1;
}

三、文件打开模式与随机读写

要实现文件的随机读写,文件的打开模式必须允许读写操作。常用的文件打开模式包括:

  • "r+":读写模式,文件必须存在,且从文件开头进行读写。
  • "w+":读写模式,如果文件存在则截断为零长度,否则创建新文件。
  • "a+":读写模式,文件指针位于文件末尾,允许读取和追加写入。
  • "rb+""wb+""ab+":对应的二进制模式,用于处理二进制文件。

注意​:对于文本文件和二进制文件,使用不同的模式(带 b 表示二进制模式),尤其是在不同操作系统间移植时,二进制模式可以避免文本模式下的换行符转换问题。

四、文件随机读写的具体实现

1. 写入数据到文件的指定位置

步骤​:

  1. 使用 fopen 以读写模式打开文件(如 "rb+""wb+")。
  2. 使用 fseek 将文件指针移动到目标位置。
  3. 使用 fwrite 写入数据。
  4. 关闭文件。

示例​:

#include <stdio.h>
#include <stdlib.h>

int main() {
    FILE *file = fopen("data.bin", "wb+");
    if (file == NULL) {
        perror("无法打开文件");
        return 1;
    }

    int data = 12345;
    // 将数据写入文件的第10个字节位置
    if (fseek(file, 10, SEEK_SET) != 0) {
        perror("fseek失败");
        fclose(file);
        return 1;
    }

    if (fwrite(&data, sizeof(int), 1, file) != 1) {
        perror("fwrite失败");
        fclose(file);
        return 1;
    }

    printf("数据已写入文件的第10个字节位置。\n");

    fclose(file);
    return 0;
}

2. 从文件的指定位置读取数据

步骤​:

  1. 使用 fopen 以读写模式打开文件(如 "rb+""rb")。
  2. 使用 fseek 将文件指针移动到目标位置。
  3. 使用 fread 读取数据。
  4. 关闭文件。

示例​:

#include <stdio.h>
#include <stdlib.h>

int main() {
    FILE *file = fopen("data.bin", "rb+");
    if (file == NULL) {
        perror("无法打开文件");
        return 1;
    }

    int data;
    // 将文件指针移动到文件的第10个字节位置
    if (fseek(file, 10, SEEK_SET) != 0) {
        perror("fseek失败");
        fclose(file);
        return 1;
    }

    if (fread(&data, sizeof(int), 1, file) != 1) {
        if (feof(file)) {
            printf("到达文件末尾,未读取到数据。\n");
        } else if (ferror(file)) {
            perror("fread失败");
        }
        fclose(file);
        return 1;
    }

    printf("从第10个字节位置读取到的数据:%d\n", data);

    fclose(file);
    return 0;
}

3. 更新文件中的特定数据

步骤​:

  1. 打开文件以读写模式。
  2. 移动文件指针到需要更新的数据位置。
  3. 写入新的数据。
  4. 关闭文件。

示例​:

#include <stdio.h>
#include <stdlib.h>

int main() {
    FILE *file = fopen("data.bin", "rb+");
    if (file == NULL) {
        perror("无法打开文件");
        return 1;
    }

    int new_data = 54321;
    // 假设我们要更新第10个字节位置的数据
    if (fseek(file, 10, SEEK_SET) != 0) {
        perror("fseek失败");
        fclose(file);
        return 1;
    }

    if (fwrite(&new_data, sizeof(int), 1, file) != 1) {
        perror("fwrite失败");
        fclose(file);
        return 1;
    }

    printf("第10个字节位置的数据已更新为:%d\n", new_data);

    fclose(file);
    return 0;
}

五、使用 fpos_t 进行更可移植的文件定位

对于需要更高可移植性,尤其是处理大文件时,推荐使用 fgetposfsetpos 函数,它们使用 fpos_t 类型来存储文件位置,能够处理超出 long 类型范围的文件位置。

示例:使用 fgetposfsetpos

#include <stdio.h>
#include <stdlib.h>

int main() {
    FILE *file = fopen("data.bin", "wb+");
    if (file == NULL) {
        perror("无法打开文件");
        return 1;
    }

    int data1 = 11111;
    int data2 = 22222;

    // 写入一些初始数据
    fwrite(&data1, sizeof(int), 1, file);
    fwrite(&data2, sizeof(int), 1, file);

    fpos_t pos;

    // 获取当前文件位置(应该是8字节,假设int为4字节)
    if (fgetpos(file, &pos) != 0) {
        perror("fgetpos失败");
        fclose(file);
        return 1;
    }

    // 移动到第10个字节位置并写入新数据
    if (fseek(file, 10, SEEK_SET) != 0) {
        perror("fseek失败");
        fclose(file);
        return 1;
    }

    int data3 = 33333;
    if (fwrite(&data3, sizeof(int), 1, file) != 1) {
        perror("fwrite失败");
        fclose(file);
        return 1;
    }

    // 恢复到之前的位置
    if (fsetpos(file, &pos) != 0) {
        perror("fsetpos失败");
        fclose(file);
        return 1;
    }

    int read_data1, read_data2;
    if (fread(&read_data1, sizeof(int), 1, file) != 1) {
        perror("fread失败");
        fclose(file);
        return 1;
    }
    if (fread(&read_data2, sizeof(int), 1, file) != 1) {
        perror("fread失败");
        fclose(file);
        return 1;
    }

    printf("读取到的数据1:%d\n", read_data1);
    printf("读取到的数据2:%d\n", read_data2);

    fclose(file);
    return 0;
}

六、文件随机读写的注意事项

  1. 文件打开模式​:
    • 确保以正确的模式打开文件,允许读写操作(如 "rb+""wb+""ab+")。
    • 文本模式(不带 b)在某些操作系统上可能会对换行符进行转换,影响文件指针的准确定位,尤其是在跨平台应用中。因此,推荐在需要精确定位的场景中使用二进制模式(带 b)。
  2. 文件指针位置​:
    • 文件指针的位置是基于字节的,因此在读写不同大小的数据类型时,需要准确计算偏移量。
    • 使用 sizeof 运算符来确定数据类型的大小,确保偏移量和读写操作的正确性。
  3. 错误处理​:
    • 始终检查文件操作函数的返回值,如 fseekfreadfwrite 等,以确保操作成功。
    • 使用 perrorstrerror(errno) 来输出错误信息,便于调试。
  4. 缓冲区管理​:
    • 标准库函数通常使用缓冲区来提高性能,但在某些需要即时写入或读取的场景中,可能需要使用 fflush 函数刷新缓冲区,或以无缓冲模式打开文件。
  5. 并发访问​:
    • 在多线程或多进程环境中,同时访问同一个文件可能导致数据竞争和不一致。需要使用适当的同步机制(如文件锁)来管理并发访问。
  6. 大文件支持​:
    • 对于超过 long 类型范围的大文件,使用 fgetposfsetpos 更加可移植和安全。
    • 确保编译器和平台支持大文件操作,必要时定义适当的宏(如 _FILE_OFFSET_BITS=64 在某些系统上)。

七、综合示例:随机读写学生信息

以下是一个综合示例,展示如何使用文件随机读写来管理学生信息(如学号和姓名)。每个学生的信息占用固定大小的记录,便于随机访问。

示例说明:

  • 每个学生记录包含一个整型学号(int,4字节)和一个字符串姓名(假设最大长度为50字节,包括终止符)。
  • 每个记录总大小为 4 + 50 = 54 字节。
  • 程序允许用户添加学生信息、根据学号查找学生信息、以及显示所有学生信息。

代码实现:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define RECORD_SIZE 54
#define MAX_NAME_LEN 50
#define FILENAME "students.dat"

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

void add_student(FILE *file) {
    Student s;
    printf("请输入学号: ");
    scanf("%d", &s.id);
    printf("请输入姓名: ");
    scanf("%s", s.name);

    // 移动到文件末尾
    fseek(file, 0, SEEK_END);
    if (fwrite(&s, RECORD_SIZE, 1, file) != 1) {
        perror("写入学生信息失败");
    } else {
        printf("学生信息已添加。\n");
    }
}

void find_student(FILE *file, int id) {
    Student s;
    rewind(file); // 移动到文件开头
    while (fread(&s, RECORD_SIZE, 1, file) == 1) {
        if (s.id == id) {
            printf("找到学生: 学号=%d, 姓名=%s\n", s.id, s.name);
            return;
        }
    }
    printf("未找到学号为 %d 的学生。\n", id);
}

void display_all_students(FILE *file) {
    Student s;
    rewind(file);
    printf("所有学生信息:\n");
    while (fread(&s, RECORD_SIZE, 1, file) == 1) {
        printf("学号=%d, 姓名=%s\n", s.id, s.name);
    }
}

int main() {
    FILE *file = fopen(FILENAME, "rb+");
    if (file == NULL) {
        // 如果文件不存在,尝试创建
        file = fopen(FILENAME, "wb+");
        if (file == NULL) {
            perror("无法创建或打开文件");
            return 1;
        }
    }

    int choice, id;

    while (1) {
        printf("\n学生信息管理系统\n");
        printf("1. 添加学生\n");
        printf("2. 查找学生\n");
        printf("3. 显示所有学生\n");
        printf("4. 退出\n");
        printf("请选择操作: ");
        scanf("%d", &choice);

        switch (choice) {
            case 1:
                add_student(file);
                break;
            case 2:
                printf("请输入要查找的学号: ");
                scanf("%d", &id);
                find_student(file, id);
                break;
            case 3:
                display_all_students(file);
                break;
            case 4:
                fclose(file);
                printf("退出程序。\n");
                return 0;
            default:
                printf("无效的选择,请重新输入。\n");
        }
    }

    fclose(file);
    return 0;
}

代码说明:

  1. 数据结构​:定义了一个 Student 结构体,包含学号(int)和姓名(char 数组)。
  2. 记录大小​:每个学生记录固定为 RECORD_SIZE 字节,便于随机访问。
  3. 文件打开​:程序尝试以 "rb+" 模式打开文件,如果文件不存在,则以 "wb+" 模式创建文件。
  4. 功能​:
    • 添加学生​:将新学生信息写入文件末尾。
    • 查找学生​:从文件开头顺序查找指定学号的学生信息(可以根据需求优化为随机访问)。
    • 显示所有学生​:从文件开头顺序读取并显示所有学生信息。
    • 退出​:关闭文件并退出程序。

注意​:上述示例中的查找功能是顺序查找,适用于小规模数据。对于大规模数据,可以通过记录的固定大小和学号的索引,实现更高效的随机查找。

八、高级主题:使用内存映射文件(Memory-Mapped Files)

除了标准的文件操作函数,C语言还可以通过内存映射文件技术实现高效的文件随机读写,尤其是在处理非常大的文件时。内存映射文件将文件的内容映射到进程的地址空间,使得文件的读写操作可以像访问内存一样高效。

1. 内存映射文件的概念

内存映射文件(Memory-Mapped Files)是一种将文件或文件的一部分映射到进程的地址空间的技术。通过这种方式,程序可以像访问内存一样访问文件数据,操作系统负责将内存中的更改同步回文件。

2. 在C语言中实现内存映射文件

在标准C语言中,并没有直接提供内存映射文件的函数。但是,可以通过平台相关的API实现,例如:

  • POSIX系统(如Linux、macOS)​​:使用 mmap 函数。
  • Windows系统​:使用 CreateFileMappingMapViewOfFile 函数。

由于内存映射文件涉及平台相关的代码,这里不做详细展开。如果需要处理超大文件或追求极高的性能,建议学习并使用这些高级技术。

九、总结

文件随机读写是C语言文件操作中的重要部分,通过灵活使用 fseekftellfgetposfsetpos 等函数,可以在文件的任意位置进行高效的读写操作。这在处理大型文件、数据库文件或需要频繁访问特定数据块的应用中尤为有用。

关键点回顾:

  1. 核心函数​:
    • fseek:设置文件指针位置。
    • ftell:获取当前文件指针位置。
    • fgetposfsetpos:更可移植的文件位置管理。
    • rewind:将文件指针重置到文件开头。
  2. 文件打开模式​:确保以允许读写的模式(如 "rb+""wb+")打开文件,以支持随机访问。
  3. 数据对齐与大小​:使用 sizeof 运算符确保读写的数据类型大小与文件中的记录大小匹配,保证文件指针的准确定位。
  4. 错误处理​:始终检查文件操作函数的返回值,确保操作成功,并处理可能的错误情况。
  5. 可移植性与大文件支持​:对于需要处理大文件或跨平台应用,优先使用 fgetposfsetpos,并考虑大文件支持的相关设置。
  6. 综合应用​:通过结构体和固定大小的记录,可以实现高效的数据管理和随机访问,如示例中的学生信息管理系统。

通过掌握文件随机读写的技巧,你可以编写出更高效、灵活的C语言程序,满足各种复杂的数据处理需求。在实际开发中,根据具体应用场景选择最合适的文件操作方式,是实现高效、可靠程序的关键。

© 版权声明
THE END
喜欢就点个赞,支持一下吧!
点赞27 分享
评论 抢沙发
头像
欢迎您留下评论!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容