MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C 语言fread和fwrite函数用法详解

2024-06-212.5k 阅读

1. 概述

在 C 语言中,freadfwrite 函数是用于对文件进行二进制读写操作的重要工具。它们属于标准输入输出库(<stdio.h>)的一部分,与文本模式读写函数(如 fscanffprintf)不同,freadfwrite 更侧重于处理二进制数据,这使得它们在处理诸如图像、音频、视频等二进制文件以及结构体等复杂数据类型时非常有用。

2. fread 函数详解

2.1 函数原型

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);

  • ptr:指向用于存储读取数据的内存块的指针。这个内存块必须有足够的空间来存储从文件中读取的数据。
  • size:要读取的每个数据项的大小(以字节为单位)。例如,如果要读取 int 类型的数据,size 通常为 sizeof(int)
  • nmemb:要读取的数据项的数量。实际读取的字节数为 size * nmemb
  • stream:指向 FILE 类型结构体的指针,该结构体表示要读取的文件流。通常是通过 fopen 函数打开的文件。

2.2 返回值

fread 函数返回成功读取的数据项的数量。如果到达文件末尾或发生错误,返回值可能小于 nmemb。如果发生错误,还可以通过 ferror 函数检查错误标志,通过 feof 函数检查是否到达文件末尾。

2.3 示例代码

#include <stdio.h>

int main() {
    FILE *file = fopen("data.bin", "rb");
    if (file == NULL) {
        perror("Failed to open file");
        return 1;
    }

    int numbers[5];
    size_t result = fread(numbers, sizeof(int), 5, file);
    if (result < 5) {
        if (feof(file)) {
            printf("Reached end of file before reading all data.\n");
        } else if (ferror(file)) {
            perror("Error reading file");
        }
    }

    for (int i = 0; i < result; i++) {
        printf("%d ", numbers[i]);
    }
    printf("\n");

    fclose(file);
    return 0;
}

在上述代码中,我们尝试从名为 data.bin 的二进制文件中读取 5 个 int 类型的数据。如果读取的数据项数量小于 5,我们检查是到达文件末尾还是发生了错误。

2.4 深入理解

fread 函数从文件流 stream 中按照指定的 sizenmemb 读取数据,并将其存储到 ptr 指向的内存位置。它以二进制方式读取数据,不会对数据进行任何格式转换。这意味着如果文件中存储的是 int 类型的二进制数据,fread 会直接将这些字节读取到内存中,而不会像 fscanf 那样将字节解释为文本并进行转换。

3. fwrite 函数详解

3.1 函数原型

size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);

  • ptr:指向要写入文件的数据块的指针。
  • size:要写入的每个数据项的大小(以字节为单位)。
  • nmemb:要写入的数据项的数量。实际写入的字节数为 size * nmemb
  • stream:指向 FILE 类型结构体的指针,该结构体表示要写入的文件流。通常是通过 fopen 函数打开的文件,且打开模式需允许写入(如 "wb")。

3.2 返回值

fwrite 函数返回成功写入的数据项的数量。如果返回值小于 nmemb,则表示写入过程中发生了错误。可以通过 ferror 函数检查错误标志。

3.3 示例代码

#include <stdio.h>

int main() {
    int numbers[] = {1, 2, 3, 4, 5};
    FILE *file = fopen("data.bin", "wb");
    if (file == NULL) {
        perror("Failed to open file");
        return 1;
    }

    size_t result = fwrite(numbers, sizeof(int), 5, file);
    if (result < 5) {
        perror("Error writing file");
    }

    fclose(file);
    return 0;
}

在这段代码中,我们将一个包含 5 个 int 类型数据的数组写入到名为 data.bin 的二进制文件中。如果写入的数据项数量小于 5,说明写入过程出现错误,我们通过 perror 输出错误信息。

3.4 深入理解

fwrite 函数将 ptr 指向的内存中的数据,按照指定的 sizenmemb 以二进制形式写入到文件流 stream 中。与 fread 类似,它不进行任何格式转换,直接将内存中的字节序列写入文件。这使得它非常适合写入结构体、数组等复杂数据类型,因为不需要担心格式问题。

4. 处理结构体数据

4.1 结构体写入文件

#include <stdio.h>

typedef struct {
    char name[50];
    int age;
} Person;

int main() {
    Person people[] = {{"Alice", 25}, {"Bob", 30}};
    FILE *file = fopen("people.bin", "wb");
    if (file == NULL) {
        perror("Failed to open file");
        return 1;
    }

    size_t result = fwrite(people, sizeof(Person), 2, file);
    if (result < 2) {
        perror("Error writing file");
    }

    fclose(file);
    return 0;
}

在上述代码中,我们定义了一个 Person 结构体,并创建了一个包含两个 Person 结构体实例的数组。然后使用 fwrite 函数将整个数组写入到二进制文件 people.bin 中。

4.2 结构体从文件读取

#include <stdio.h>

typedef struct {
    char name[50];
    int age;
} Person;

int main() {
    Person people[2];
    FILE *file = fopen("people.bin", "rb");
    if (file == NULL) {
        perror("Failed to open file");
        return 1;
    }

    size_t result = fread(people, sizeof(Person), 2, file);
    if (result < 2) {
        if (feof(file)) {
            printf("Reached end of file before reading all data.\n");
        } else if (ferror(file)) {
            perror("Error reading file");
        }
    }

    for (int i = 0; i < result; i++) {
        printf("Name: %s, Age: %d\n", people[i].name, people[i].age);
    }

    fclose(file);
    return 0;
}

这段代码从 people.bin 文件中读取 Person 结构体数据,并打印出每个人的姓名和年龄。在处理结构体数据时,要注意结构体的对齐方式。不同的编译器和平台可能有不同的对齐规则,这可能会影响 freadfwrite 的行为。为了确保可移植性,可以使用 #pragma pack 指令来指定结构体的对齐方式。

5. 错误处理

在使用 freadfwrite 函数时,错误处理非常重要。常见的错误包括文件打开失败、读取或写入过程中的 I/O 错误等。

5.1 文件打开失败

在使用 freadfwrite 之前,必须确保文件已成功打开。fopen 函数如果失败会返回 NULL,可以通过检查这个返回值来判断文件是否成功打开。

FILE *file = fopen("example.bin", "rb");
if (file == NULL) {
    perror("Failed to open file");
    return 1;
}

perror 函数会输出错误信息,帮助我们定位问题。

5.2 读取或写入错误

freadfwrite 函数返回值可以提示是否发生读取或写入错误。如果返回值小于预期的 nmemb,可以通过 ferror 函数进一步检查错误标志。

size_t result = fread(buffer, sizeof(int), 10, file);
if (result < 10) {
    if (ferror(file)) {
        perror("Error reading file");
    } else if (feof(file)) {
        printf("Reached end of file before reading all data.\n");
    }
}

ferror 函数在文件流发生错误时返回非零值,feof 函数在到达文件末尾时返回非零值。

6. 性能考虑

6.1 缓冲区大小

freadfwrite 的性能在一定程度上取决于缓冲区的大小。较大的缓冲区可以减少系统调用的次数,从而提高性能。例如,在读取大文件时,如果每次只读取一个字节,会导致大量的系统调用,而如果一次读取较大的数据块(如 4096 字节),系统调用次数会显著减少。

#define BUFFER_SIZE 4096
char buffer[BUFFER_SIZE];
while ((result = fread(buffer, 1, BUFFER_SIZE, file)) > 0) {
    // 处理读取的数据
}

6.2 磁盘 I/O 特性

不同的存储设备(如硬盘、固态硬盘)有不同的 I/O 特性。固态硬盘通常具有较低的随机 I/O 延迟,而硬盘在顺序 I/O 方面表现较好。在设计文件读写操作时,应尽量利用存储设备的特性。例如,对于硬盘,顺序读写大文件块可以提高性能;对于固态硬盘,随机读写小块数据也能有较好的表现,但过大的缓冲区可能在某些情况下反而降低性能,需要根据实际情况进行测试和优化。

7. 跨平台注意事项

7.1 字节序

不同的计算机体系结构可能使用不同的字节序(大端序或小端序)。当使用 freadfwrite 在不同平台间交换数据时,字节序问题可能导致数据解析错误。例如,在小端序系统上写入的 int 类型数据,在大端序系统上读取时可能会得到错误的值。为了解决这个问题,可以在写入数据时进行字节序转换(如使用 htonlhtons 等函数),或者在读取数据后进行转换。

#include <arpa/inet.h>

// 假设我们要写入一个 int 类型数据
int num = 1234;
int net_num = htonl(num);
fwrite(&net_num, sizeof(int), 1, file);

// 读取时
int read_num;
fread(&read_num, sizeof(int), 1, file);
int local_num = ntohl(read_num);

7.2 结构体对齐

如前文所述,不同的编译器和平台对结构体的对齐方式可能不同。这可能导致在一个平台上写入的结构体数据,在另一个平台上无法正确读取。使用 #pragma pack 指令可以指定结构体的对齐方式,以确保跨平台的兼容性。

#pragma pack(push, 1)
typedef struct {
    char c;
    int i;
} MyStruct;
#pragma pack(pop)

上述代码通过 #pragma packMyStruct 结构体的对齐方式设置为 1 字节对齐,避免了因对齐方式不同而导致的问题。

8. 与其他文件操作函数的对比

8.1 与文本模式读写函数对比

  • 文本模式读写函数:如 fscanffprintf,它们以文本形式处理数据。在读取数据时,会将文件中的文本按照指定的格式转换为相应的数据类型;在写入数据时,会将数据转换为文本格式写入文件。这种方式适用于处理人类可读的文本文件,但在处理二进制数据或复杂数据类型时不太方便,因为需要进行额外的格式转换,并且可能会丢失精度或导致数据损坏。
  • freadfwrite:以二进制方式处理数据,不进行格式转换。这使得它们在处理图像、音频、视频等二进制文件以及结构体等复杂数据类型时更加高效和准确。但它们不适合处理需要人类可读格式的文本文件,因为直接读取或写入的二进制数据对于人类来说是不可读的。

8.2 与低级文件操作函数对比

  • 低级文件操作函数:如 readwrite(在 Unix 系统中),它们属于系统调用,直接与操作系统的文件系统交互。这些函数通常在性能上比 freadfwrite 略高,因为它们没有标准 I/O 库的缓冲区开销。然而,它们的使用相对复杂,需要手动管理缓冲区,并且在可移植性方面不如 freadfwrite,因为它们是系统相关的。
  • freadfwrite:属于标准 I/O 库函数,具有较好的可移植性,适用于各种操作系统。它们提供了缓冲区管理功能,减少了系统调用的次数,提高了编程的便利性。在大多数情况下,freadfwrite 能够满足一般的文件读写需求,并且代码更易于维护和移植。

9. 实际应用场景

9.1 数据存储与备份

在数据库系统中,数据通常以二进制格式存储在文件中。freadfwrite 函数可用于将数据库中的记录写入文件进行备份,或从备份文件中恢复数据。例如,一个简单的学生信息管理系统,可以将学生的结构体数据(包含姓名、学号、成绩等)使用 fwrite 写入文件进行持久化存储,在需要时使用 fread 读取数据进行查询或修改。

9.2 多媒体处理

在处理图像、音频和视频文件时,freadfwrite 是必不可少的工具。例如,在图像处理中,可以使用 fread 读取图像文件的二进制数据,对数据进行处理(如调整亮度、对比度等),然后使用 fwrite 将处理后的图像数据写回文件。对于音频文件,同样可以读取音频数据进行音频特效处理,再写回文件保存处理后的音频。

9.3 网络编程

在网络编程中,freadfwrite 可以用于处理网络套接字的 I/O 操作。例如,在客户端 - 服务器模型中,服务器可以使用 fread 从套接字中读取客户端发送的数据,进行处理后,再使用 fwrite 将响应数据写回套接字发送给客户端。这种方式可以方便地处理二进制数据在网络中的传输,如传输自定义协议的数据帧。

10. 优化技巧

10.1 减少不必要的 I/O 操作

在进行文件读写时,尽量批量处理数据,避免频繁的小数据量读写操作。例如,如果需要写入多个小的数据块,可以先将这些数据收集到一个缓冲区中,然后一次性使用 fwrite 写入文件。这样可以减少系统调用的次数,提高 I/O 性能。

#define BUFFER_SIZE 1024
char buffer[BUFFER_SIZE];
int buffer_index = 0;

// 假设我们有一系列小数据块要写入
for (int i = 0; i < num_small_blocks; i++) {
    // 将小数据块复制到缓冲区
    int small_block_size = get_small_block_size(i);
    if (buffer_index + small_block_size > BUFFER_SIZE) {
        fwrite(buffer, 1, buffer_index, file);
        buffer_index = 0;
    }
    copy_small_block_to_buffer(i, buffer + buffer_index);
    buffer_index += small_block_size;
}
// 写入剩余的数据
if (buffer_index > 0) {
    fwrite(buffer, 1, buffer_index, file);
}

10.2 合理使用缓冲区

根据文件大小和系统资源,选择合适的缓冲区大小。对于大文件,较大的缓冲区通常能提高性能,但也不能过大,以免占用过多的内存。可以通过性能测试来确定最优的缓冲区大小。此外,还可以考虑使用双缓冲区技术,即在读取数据时,一个缓冲区用于读取数据,另一个缓冲区用于处理已经读取的数据,这样可以在一定程度上重叠 I/O 操作和数据处理操作,提高整体效率。

10.3 预读和预写

在某些情况下,可以提前预测需要读取或写入的数据,并进行预读或预写操作。例如,在顺序读取文件时,可以提前读取一些数据到缓冲区中,这样当真正需要数据时,可以直接从缓冲区获取,减少等待 I/O 的时间。对于写入操作,也可以提前准备好要写入的数据,减少实际写入时的延迟。这需要对应用程序的逻辑有深入的理解,以便准确地预测数据的使用情况。

通过深入理解 freadfwrite 函数的用法、注意事项以及优化技巧,我们能够在 C 语言编程中更高效、准确地处理文件的二进制读写操作,满足各种实际应用场景的需求。无论是数据存储、多媒体处理还是网络编程等领域,这两个函数都为我们提供了强大而灵活的工具。在实际编程中,根据具体情况合理应用这些知识,将有助于提高程序的性能和稳定性。