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

C语言结构体在文件I/O中的读写操作

2021-08-278.0k 阅读

C语言结构体在文件I/O中的读写操作基础

结构体基础回顾

在C语言中,结构体是一种用户自定义的数据类型,它允许将不同类型的数据组合在一起。例如,我们要描述一个学生的信息,可能包含姓名(字符串)、年龄(整数)和成绩(浮点数),就可以使用结构体来实现。

struct Student {
    char name[50];
    int age;
    float score;
};

这里定义了一个名为Student的结构体,它包含三个成员:name(字符数组用于存储字符串)、age(整数)和score(浮点数)。

文件I/O基础

在C语言中,文件操作主要通过标准输入输出库<stdio.h>来实现。文件I/O操作的基本流程通常是打开文件、进行读写操作,最后关闭文件。

  1. 打开文件:使用fopen函数,它的原型为FILE *fopen(const char *filename, const char *mode)filename是要打开的文件名,mode指定打开文件的方式,如"r"表示只读,"w"表示只写(如果文件存在则覆盖,不存在则创建),"a"表示追加等。例如:
FILE *file = fopen("example.txt", "w");
if (file == NULL) {
    perror("Failed to open file");
    return 1;
}

这里尝试以只写模式打开example.txt文件,如果打开失败,fopen返回NULL,并通过perror打印错误信息。

  1. 关闭文件:使用fclose函数,原型为int fclose(FILE *stream)。关闭文件可以释放系统资源,避免数据丢失。例如:
int result = fclose(file);
if (result != 0) {
    perror("Failed to close file");
    return 1;
}

fclose返回0表示成功关闭,非零表示失败。

  1. 基本的读写函数
    • 字符读写fputc用于向文件写入一个字符,fgetc用于从文件读取一个字符。
    • 字符串读写fputs用于向文件写入一个字符串,fgets用于从文件读取一行字符串。

结构体在文件I/O中的写入操作

按成员逐个写入

一种简单的将结构体写入文件的方式是按结构体的成员逐个写入。以之前定义的Student结构体为例:

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

struct Student {
    char name[50];
    int age;
    float score;
};

int main() {
    struct Student student = {"Alice", 20, 85.5};
    FILE *file = fopen("students.txt", "w");
    if (file == NULL) {
        perror("Failed to open file");
        return 1;
    }

    // 按成员逐个写入
    fputs(student.name, file);
    fputc('\n', file);
    fprintf(file, "%d\n", student.age);
    fprintf(file, "%.2f\n", student.score);

    fclose(file);
    return 0;
}

在这个例子中,我们先将学生的姓名用fputs写入文件,并添加一个换行符。然后使用fprintf分别将年龄和成绩以格式化的方式写入文件,每行一个数据。这种方式的优点是数据在文件中以文本形式存储,可读性强,方便查看和编辑。但缺点也很明显,读取时需要按照相同的格式解析,并且如果结构体成员较多,代码会显得繁琐。

使用fwrite整体写入

fwrite函数可以将一块内存中的数据直接写入文件,非常适合写入结构体。fwrite的原型为size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream),其中ptr是指向要写入数据的指针,size是每个数据项的大小,nmemb是数据项的数量,stream是文件指针。

#include <stdio.h>

struct Student {
    char name[50];
    int age;
    float score;
};

int main() {
    struct Student student = {"Bob", 22, 90.0};
    FILE *file = fopen("students.bin", "wb");
    if (file == NULL) {
        perror("Failed to open file");
        return 1;
    }

    // 使用fwrite整体写入结构体
    size_t result = fwrite(&student, sizeof(struct Student), 1, file);
    if (result != 1) {
        perror("Failed to write to file");
        fclose(file);
        return 1;
    }

    fclose(file);
    return 0;
}

这里我们以二进制写入模式"wb"打开文件,然后使用fwrite将整个student结构体写入文件。fwrite返回成功写入的数据项数量,如果返回值不等于nmemb(这里是1),则表示写入失败。使用fwrite写入结构体的优点是简单高效,适合大量结构体数据的写入。但缺点是数据以二进制形式存储,直接查看文件内容时可读性差。

结构体在文件I/O中的读取操作

按成员逐个读取

与按成员逐个写入相对应,我们也可以按成员逐个从文件中读取数据来填充结构体。假设文件students.txt是按之前按成员逐个写入的方式生成的:

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

struct Student {
    char name[50];
    int age;
    float score;
};

int main() {
    struct Student student;
    FILE *file = fopen("students.txt", "r");
    if (file == NULL) {
        perror("Failed to open file");
        return 1;
    }

    // 按成员逐个读取
    fgets(student.name, sizeof(student.name), file);
    student.name[strcspn(student.name, "\n")] = '\0'; // 去掉换行符
    fscanf(file, "%d", &student.age);
    fscanf(file, "%f", &student.score);

    fclose(file);

    printf("Name: %s\n", student.name);
    printf("Age: %d\n", student.age);
    printf("Score: %.2f\n", student.score);

    return 0;
}

在这个例子中,我们使用fgets读取学生的姓名,并通过strcspn去掉换行符。然后使用fscanf分别读取年龄和成绩。这种方式在读取文本格式存储的结构体数据时比较方便,但如果文件格式不正确,可能会导致读取错误。

使用fread整体读取

fwrite对应的读取函数是fread,它的原型为size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream),参数含义与fwrite类似,只不过它是从文件读取数据到内存。假设文件students.bin是使用fwrite写入的:

#include <stdio.h>

struct Student {
    char name[50];
    int age;
    float score;
};

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

    // 使用fread整体读取结构体
    size_t result = fread(&student, sizeof(struct Student), 1, file);
    if (result != 1) {
        perror("Failed to read from file");
        fclose(file);
        return 1;
    }

    fclose(file);

    printf("Name: %s\n", student.name);
    printf("Age: %d\n", student.age);
    printf("Score: %.2f\n", student.score);

    return 0;
}

这里我们以二进制读取模式"rb"打开文件,使用fread将文件中的数据一次性读取到student结构体中。同样,fread返回成功读取的数据项数量,如果不等于nmemb,则表示读取失败。

处理多个结构体的文件I/O

写入多个结构体

当需要处理多个结构体时,无论是使用按成员逐个写入还是fwrite整体写入,都只需要在循环中进行相应操作即可。以下是使用fwrite写入多个学生结构体的示例:

#include <stdio.h>

struct Student {
    char name[50];
    int age;
    float score;
};

int main() {
    struct Student students[] = {
        {"Charlie", 21, 88.0},
        {"David", 23, 92.5}
    };
    int numStudents = sizeof(students) / sizeof(students[0]);
    FILE *file = fopen("students_list.bin", "wb");
    if (file == NULL) {
        perror("Failed to open file");
        return 1;
    }

    for (int i = 0; i < numStudents; i++) {
        size_t result = fwrite(&students[i], sizeof(struct Student), 1, file);
        if (result != 1) {
            perror("Failed to write to file");
            fclose(file);
            return 1;
        }
    }

    fclose(file);
    return 0;
}

在这个例子中,我们定义了一个students数组,包含两个学生结构体。通过循环,使用fwrite将每个学生结构体写入文件。

读取多个结构体

读取多个结构体同样可以在循环中使用fread来实现。假设文件students_list.bin是按上述方式写入的:

#include <stdio.h>

struct Student {
    char name[50];
    int age;
    float score;
};

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

    while (fread(&student, sizeof(struct Student), 1, file) == 1) {
        printf("Name: %s\n", student.name);
        printf("Age: %d\n", student.age);
        printf("Score: %.2f\n", student.score);
    }

    fclose(file);
    return 0;
}

这里使用while循环,只要fread成功读取一个结构体(返回值为1),就打印该结构体的信息。当fread返回值不为1时(表示到达文件末尾或读取错误),循环结束。

结构体文件I/O中的注意事项

结构体对齐

在C语言中,结构体成员在内存中的存储存在对齐规则。不同的编译器和平台可能有不同的对齐方式,这会影响结构体的实际大小。例如,在某些平台上,为了提高内存访问效率,结构体成员会按照一定的字节数对齐。假设我们有如下结构体:

struct Example {
    char c;
    int i;
};

理论上,char类型占1个字节,int类型占4个字节,该结构体大小应该是5个字节。但实际上,由于对齐规则,在一些平台上,struct Example的大小可能是8个字节,因为int类型成员i需要对齐到4字节边界,所以c后面会填充3个字节。

在进行文件I/O操作时,如果涉及到结构体的写入和读取,尤其是在不同平台之间传输数据时,需要注意结构体对齐问题。一种解决方法是使用#pragma pack指令来指定结构体的对齐方式。例如:

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

这里#pragma pack(push, 1)表示将结构体对齐方式设置为1字节对齐,#pragma pack(pop)则恢复原来的对齐方式。这样,struct Example的大小就会是5个字节,无论在什么平台上都能保证一致的存储方式,有利于文件I/O操作的跨平台兼容性。

文件指针位置

在进行文件I/O操作时,文件指针的位置非常重要。每次读写操作后,文件指针会自动移动到下一个数据位置。例如,使用fwrite写入一个结构体后,文件指针会移动到结构体数据之后。如果需要在文件的特定位置进行读写操作,可以使用fseek函数来调整文件指针的位置。

fseek的原型为int fseek(FILE *stream, long offset, int whence),其中stream是文件指针,offset是偏移量,whence指定偏移的起始位置。whence可以取值SEEK_SET(从文件开头偏移)、SEEK_CUR(从当前位置偏移)、SEEK_END(从文件末尾偏移)。

假设我们已经向文件写入了多个结构体,现在想要读取文件中间某个结构体,可以先使用fseek移动文件指针到指定位置:

#include <stdio.h>

struct Student {
    char name[50];
    int age;
    float score;
};

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

    // 假设要读取第3个结构体(从0开始计数)
    int structIndex = 2;
    fseek(file, structIndex * sizeof(struct Student), SEEK_SET);

    size_t result = fread(&student, sizeof(struct Student), 1, file);
    if (result != 1) {
        perror("Failed to read from file");
    } else {
        printf("Name: %s\n", student.name);
        printf("Age: %d\n", student.age);
        printf("Score: %.2f\n", student.score);
    }

    fclose(file);
    return 0;
}

在这个例子中,我们使用fseek将文件指针移动到第3个结构体的位置(从文件开头偏移structIndex * sizeof(struct Student)字节),然后使用fread读取该结构体。

错误处理

在文件I/O操作中,错误处理至关重要。无论是打开文件、读写文件还是关闭文件,都可能发生错误。之前的示例中已经多次展示了使用perror函数来打印错误信息。除了perror,还可以使用ferror函数来检查文件操作是否出错。ferror的原型为int ferror(FILE *stream),如果文件操作出错,它返回非零值,否则返回0。

例如,在每次读写操作后,可以使用ferror检查是否出错:

#include <stdio.h>

struct Student {
    char name[50];
    int age;
    float score;
};

int main() {
    struct Student student = {"Eve", 24, 95.0};
    FILE *file = fopen("students.bin", "wb");
    if (file == NULL) {
        perror("Failed to open file");
        return 1;
    }

    size_t result = fwrite(&student, sizeof(struct Student), 1, file);
    if (result != 1 || ferror(file)) {
        perror("Failed to write to file");
        fclose(file);
        return 1;
    }

    fclose(file);
    return 0;
}

这里在fwrite操作后,不仅检查了fwrite的返回值,还使用ferror检查是否有错误发生。如果有错误,通过perror打印错误信息并进行相应处理。

结构体嵌套在文件I/O中的应用

结构体嵌套的定义

在C语言中,结构体可以嵌套,即一个结构体的成员可以是另一个结构体类型。例如,我们要描述一个教师的信息,教师除了基本的姓名、年龄,还可能有一个办公地址,而办公地址又可以用一个结构体来描述:

struct Address {
    char street[100];
    char city[50];
    int zipCode;
};

struct Teacher {
    char name[50];
    int age;
    struct Address address;
};

这里struct Teacher结构体包含了一个struct Address类型的成员address

嵌套结构体的写入操作

对于嵌套结构体的写入,同样可以使用fwrite或按成员逐个写入的方式。使用fwrite时,由于fwrite是按内存块进行写入,只要保证结构体定义的一致性,就可以直接写入整个嵌套结构体。

#include <stdio.h>

struct Address {
    char street[100];
    char city[50];
    int zipCode;
};

struct Teacher {
    char name[50];
    int age;
    struct Address address;
};

int main() {
    struct Teacher teacher = {
        "Mr. Smith",
        35,
        {"123 Main St", "Anytown", 12345}
    };
    FILE *file = fopen("teachers.bin", "wb");
    if (file == NULL) {
        perror("Failed to open file");
        return 1;
    }

    size_t result = fwrite(&teacher, sizeof(struct Teacher), 1, file);
    if (result != 1) {
        perror("Failed to write to file");
        fclose(file);
        return 1;
    }

    fclose(file);
    return 0;
}

在这个例子中,我们定义了一个teacher结构体变量,并使用fwrite将其写入文件。

嵌套结构体的读取操作

读取嵌套结构体也可以使用fread。假设文件teachers.bin是按上述方式写入的:

#include <stdio.h>

struct Address {
    char street[100];
    char city[50];
    int zipCode;
};

struct Teacher {
    char name[50];
    int age;
    struct Address address;
};

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

    size_t result = fread(&teacher, sizeof(struct Teacher), 1, file);
    if (result != 1) {
        perror("Failed to read from file");
        fclose(file);
        return 1;
    }

    printf("Name: %s\n", teacher.name);
    printf("Age: %d\n", teacher.age);
    printf("Address: %s, %s, %d\n", teacher.address.street, teacher.address.city, teacher.address.zipCode);

    fclose(file);
    return 0;
}

这里使用fread将文件中的数据读取到teacher结构体中,并打印出教师的信息,包括嵌套的地址信息。

结构体与动态内存分配在文件I/O中的结合

结构体中使用动态内存

在某些情况下,结构体的成员可能需要使用动态内存分配。例如,我们要描述一个包含可变长度字符串的学生信息,就可以使用字符指针并动态分配内存:

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

struct Student {
    char *name;
    int age;
    float score;
};

在使用这个结构体时,需要为name成员分配内存:

struct Student student;
student.name = (char *)malloc(50 * sizeof(char));
strcpy(student.name, "Tom");
student.age = 20;
student.score = 80.0;

写入包含动态内存的结构体

当将包含动态内存分配的结构体写入文件时,不能简单地使用fwrite,因为fwrite只会写入指针的值,而不会写入指针指向的动态分配的内存中的数据。一种解决方法是先写入字符串的长度,然后写入字符串内容,再写入其他结构体成员。

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

struct Student {
    char *name;
    int age;
    float score;
};

int main() {
    struct Student student;
    student.name = (char *)malloc(50 * sizeof(char));
    strcpy(student.name, "Tom");
    student.age = 20;
    student.score = 80.0;

    FILE *file = fopen("students_dynamic.bin", "wb");
    if (file == NULL) {
        perror("Failed to open file");
        free(student.name);
        return 1;
    }

    int nameLength = strlen(student.name);
    fwrite(&nameLength, sizeof(int), 1, file);
    fwrite(student.name, sizeof(char), nameLength, file);
    fwrite(&student.age, sizeof(int), 1, file);
    fwrite(&student.score, sizeof(float), 1, file);

    free(student.name);
    fclose(file);
    return 0;
}

在这个例子中,我们先写入name字符串的长度,然后写入字符串内容,最后写入年龄和成绩。

读取包含动态内存的结构体

读取包含动态内存分配的结构体时,要按照写入的顺序,先读取字符串长度,然后根据长度分配内存并读取字符串内容,最后读取其他成员。

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

struct Student {
    char *name;
    int age;
    float score;
};

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

    int nameLength;
    fread(&nameLength, sizeof(int), 1, file);
    student.name = (char *)malloc((nameLength + 1) * sizeof(char));
    fread(student.name, sizeof(char), nameLength, file);
    student.name[nameLength] = '\0';

    fread(&student.age, sizeof(int), 1, file);
    fread(&student.score, sizeof(float), 1, file);

    printf("Name: %s\n", student.name);
    printf("Age: %d\n", student.age);
    printf("Score: %.2f\n", student.score);

    free(student.name);
    fclose(file);
    return 0;
}

这里我们先读取字符串长度,然后分配足够的内存来存储字符串,读取字符串后添加字符串结束符'\0',最后读取年龄和成绩。

通过以上内容,我们详细介绍了C语言结构体在文件I/O中的读写操作,包括基础操作、处理多个结构体、注意事项以及结构体嵌套和与动态内存分配的结合应用。希望这些知识能帮助你在实际编程中更好地处理结构体与文件I/O相关的任务。