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

C语言结构体与文件操作的结合应用

2024-07-113.4k 阅读

C语言结构体与文件操作的基础概念

结构体的定义与特点

在C语言中,结构体(struct)是一种用户自定义的数据类型,它允许将不同类型的数据组合成一个单一的实体。结构体为我们提供了一种有效的方式来组织和管理相关的数据。例如,当我们需要描述一个学生的信息时,可能需要姓名(字符串类型)、年龄(整型)、成绩(浮点型)等不同类型的数据,使用结构体就可以将这些数据整合在一起。

结构体的定义格式如下:

struct 结构体名 {
    数据类型1 成员1;
    数据类型2 成员2;
    // 更多成员...
};

例如,定义一个表示学生的结构体:

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

这里,struct Student 就是一个结构体类型,它包含了三个成员:name 是字符数组用于存储姓名,age 是整型表示年龄,score 是浮点型表示成绩。

结构体的特点之一是它可以方便地传递和处理一组相关的数据。我们可以定义结构体变量,就像定义普通变量一样:

struct Student stu1;

然后通过点运算符(.)来访问结构体变量的成员:

strcpy(stu1.name, "Tom");
stu1.age = 20;
stu1.score = 85.5;

文件操作概述

文件操作在C语言中是非常重要的部分,它允许我们将数据持久化存储在外部存储设备(如硬盘)上,以及从这些存储设备中读取数据。C语言提供了一系列标准库函数来进行文件操作,主要涉及文件的打开、读取、写入、关闭等操作。

文件操作的一般流程是:

  1. 打开文件:使用 fopen 函数,它需要两个参数,文件名和打开模式。例如,以只读模式打开一个文件:
FILE *fp;
fp = fopen("test.txt", "r");

这里,FILE 是C语言中定义的一个结构体类型,用于表示文件流。fopen 函数返回一个指向 FILE 类型的指针,如果打开文件失败,将返回 NULL

  1. 进行读写操作:根据打开模式,使用相应的函数进行读写。例如,读取文件内容可以使用 fscanffgets 等函数,写入文件可以使用 fprintffputs 等函数。

  2. 关闭文件:使用 fclose 函数关闭文件,以释放系统资源。

fclose(fp);

结构体与文件操作的简单结合:保存结构体数据到文件

以文本格式保存结构体数据

我们可以将结构体中的数据以文本格式写入文件。例如,继续以学生结构体为例,我们将学生信息保存到文件中。

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

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

int main() {
    struct Student stu1;
    strcpy(stu1.name, "Alice");
    stu1.age = 21;
    stu1.score = 90.0;

    FILE *fp;
    fp = fopen("students.txt", "w");
    if (fp == NULL) {
        printf("无法打开文件\n");
        return 1;
    }

    // 以文本格式写入结构体数据
    fprintf(fp, "姓名:%s,年龄:%d,成绩:%.2f\n", stu1.name, stu1.age, stu1.score);

    fclose(fp);
    printf("数据已成功保存到文件\n");
    return 0;
}

在这个例子中,我们首先定义了 Student 结构体并初始化了一个结构体变量 stu1。然后打开一个名为 students.txt 的文件用于写入("w" 模式)。接着使用 fprintf 函数将结构体中的数据以文本格式写入文件,fprintf 的格式化字符串与结构体成员的数据类型相对应。最后关闭文件。

以二进制格式保存结构体数据

以二进制格式保存结构体数据可以提高存储效率,并且可以完整地保存结构体的内容,避免文本格式转换可能带来的精度损失等问题。

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

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

int main() {
    struct Student stu1;
    strcpy(stu1.name, "Bob");
    stu1.age = 22;
    stu1.score = 88.5;

    FILE *fp;
    fp = fopen("students.bin", "wb");
    if (fp == NULL) {
        printf("无法打开文件\n");
        return 1;
    }

    // 以二进制格式写入结构体数据
    fwrite(&stu1, sizeof(struct Student), 1, fp);

    fclose(fp);
    printf("数据已成功保存到文件\n");
    return 0;
}

这里使用 fwrite 函数来写入结构体数据。fwrite 函数的第一个参数是要写入的数据的指针,即 &stu1,表示结构体变量的地址;第二个参数是每个数据块的大小,这里是 sizeof(struct Student),即结构体的大小;第三个参数是要写入的数据块的数量,这里为 1;第四个参数是文件指针 fp。通过这种方式,整个结构体以二进制形式被写入到文件 students.bin 中。

从文件中读取数据到结构体

从文本文件读取数据到结构体

从文本文件中读取数据并填充到结构体中,我们可以使用 fscanf 函数。

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

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

int main() {
    struct Student stu1;
    FILE *fp;
    fp = fopen("students.txt", "r");
    if (fp == NULL) {
        printf("无法打开文件\n");
        return 1;
    }

    // 从文本文件读取数据到结构体
    fscanf(fp, "姓名:%49s,年龄:%d,成绩:%f\n", stu1.name, &stu1.age, &stu1.score);

    fclose(fp);
    printf("读取的数据:姓名:%s,年龄:%d,成绩:%.2f\n", stu1.name, stu1.age, stu1.score);
    return 0;
}

在这个代码中,我们以只读模式("r")打开 students.txt 文件。fscanf 函数按照格式化字符串的格式从文件中读取数据,并将其存储到结构体 stu1 的相应成员中。注意在读取字符串时,为了避免缓冲区溢出,使用 %49s 来限制读取的字符数(因为 name 数组大小为 50,要留一个字符给字符串结束符 \0)。

从二进制文件读取数据到结构体

从二进制文件中读取数据到结构体,我们使用 fread 函数。

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

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

int main() {
    struct Student stu1;
    FILE *fp;
    fp = fopen("students.bin", "rb");
    if (fp == NULL) {
        printf("无法打开文件\n");
        return 1;
    }

    // 从二进制文件读取数据到结构体
    fread(&stu1, sizeof(struct Student), 1, fp);

    fclose(fp);
    printf("读取的数据:姓名:%s,年龄:%d,成绩:%.2f\n", stu1.name, stu1.age, stu1.score);
    return 0;
}

这里以二进制只读模式("rb")打开 students.bin 文件,fread 函数的参数与 fwrite 类似,它从文件中读取一个大小为 sizeof(struct Student) 的数据块,并将其存储到 stu1 结构体变量中。

结构体数组与文件操作

保存结构体数组到文件

当我们有多个结构体实例,即结构体数组时,同样可以将其保存到文件中。以下是一个将多个学生信息保存到文件的示例,以二进制格式保存。

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

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

int main() {
    struct Student students[] = {
        {"Charlie", 23, 82.0},
        {"David", 24, 78.5}
    };
    int num_students = sizeof(students) / sizeof(students[0]);

    FILE *fp;
    fp = fopen("students_array.bin", "wb");
    if (fp == NULL) {
        printf("无法打开文件\n");
        return 1;
    }

    // 以二进制格式写入结构体数组数据
    fwrite(students, sizeof(struct Student), num_students, fp);

    fclose(fp);
    printf("数据已成功保存到文件\n");
    return 0;
}

在这个代码中,我们定义了一个 Student 结构体数组 students,并初始化了两个学生的信息。通过 sizeof(students) / sizeof(students[0]) 计算出数组中元素的个数 num_students。然后以二进制写入模式("wb")打开文件,使用 fwrite 函数将整个结构体数组写入文件,这里 fwrite 的第二个参数是每个结构体的大小,第三个参数是结构体数组的元素个数。

从文件读取数据到结构体数组

从文件中读取数据并填充到结构体数组,以下是与上述保存结构体数组相对应的读取操作示例。

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

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

int main() {
    struct Student students[10];
    int num_students;
    FILE *fp;
    fp = fopen("students_array.bin", "rb");
    if (fp == NULL) {
        printf("无法打开文件\n");
        return 1;
    }

    // 从二进制文件读取数据到结构体数组
    num_students = fread(students, sizeof(struct Student), 10, fp);

    fclose(fp);
    printf("读取到 %d 个学生的数据\n", num_students);
    for (int i = 0; i < num_students; i++) {
        printf("学生 %d:姓名:%s,年龄:%d,成绩:%.2f\n", i + 1, students[i].name, students[i].age, students[i].score);
    }
    return 0;
}

这里以二进制只读模式("rb")打开 students_array.bin 文件,使用 fread 函数尝试读取最多 10 个结构体到 students 数组中。fread 函数返回实际读取到的结构体数量,我们将其存储在 num_students 变量中。然后通过循环打印出每个学生的信息。

复杂结构体与文件操作

包含指针成员的结构体与文件操作

当结构体中包含指针成员时,文件操作会变得稍微复杂一些。例如,假设我们有一个结构体,其中包含一个指向动态分配字符串的指针。

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

struct Person {
    char *name;
    int age;
};

void savePersonToFile(struct Person *person, const char *filename) {
    FILE *fp = fopen(filename, "wb");
    if (fp == NULL) {
        printf("无法打开文件\n");
        return;
    }

    // 先写入字符串长度
    size_t name_length = strlen(person->name);
    fwrite(&name_length, sizeof(size_t), 1, fp);
    // 再写入字符串内容
    fwrite(person->name, sizeof(char), name_length, fp);
    // 写入年龄
    fwrite(&person->age, sizeof(int), 1, fp);

    fclose(fp);
}

struct Person* readPersonFromFile(const char *filename) {
    FILE *fp = fopen(filename, "rb");
    if (fp == NULL) {
        printf("无法打开文件\n");
        return NULL;
    }

    struct Person *person = (struct Person*)malloc(sizeof(struct Person));
    if (person == NULL) {
        printf("内存分配失败\n");
        fclose(fp);
        return NULL;
    }

    size_t name_length;
    // 读取字符串长度
    fread(&name_length, sizeof(size_t), 1, fp);
    // 分配内存并读取字符串内容
    person->name = (char*)malloc((name_length + 1) * sizeof(char));
    if (person->name == NULL) {
        printf("内存分配失败\n");
        free(person);
        fclose(fp);
        return NULL;
    }
    fread(person->name, sizeof(char), name_length, fp);
    person->name[name_length] = '\0';
    // 读取年龄
    fread(&person->age, sizeof(int), 1, fp);

    fclose(fp);
    return person;
}

int main() {
    struct Person person1;
    person1.name = (char*)malloc(50 * sizeof(char));
    strcpy(person1.name, "Eva");
    person1.age = 25;

    savePersonToFile(&person1, "person.bin");

    struct Person *read_person = readPersonFromFile("person.bin");
    if (read_person != NULL) {
        printf("读取的数据:姓名:%s,年龄:%d\n", read_person->name, read_person->age);
        free(read_person->name);
        free(read_person);
    }

    free(person1.name);
    return 0;
}

在这个例子中,Person 结构体包含一个 char* 类型的 name 指针。在保存结构体到文件时,我们首先写入字符串的长度,然后写入字符串内容,最后写入年龄。在从文件读取结构体时,先读取字符串长度,根据长度分配内存,再读取字符串内容并添加字符串结束符,最后读取年龄。注意在使用完动态分配的内存后,要及时释放,以避免内存泄漏。

嵌套结构体与文件操作

嵌套结构体是指一个结构体的成员也是结构体类型。例如,我们定义一个表示地址的结构体,然后在表示员工的结构体中嵌套使用。

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

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

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

void saveEmployeeToFile(struct Employee *employee, const char *filename) {
    FILE *fp = fopen(filename, "wb");
    if (fp == NULL) {
        printf("无法打开文件\n");
        return;
    }

    fwrite(employee->name, sizeof(char), strlen(employee->name), fp);
    fwrite(&employee->age, sizeof(int), 1, fp);
    fwrite(employee->address.street, sizeof(char), strlen(employee->address.street), fp);
    fwrite(employee->address.city, sizeof(char), strlen(employee->address.city), fp);

    fclose(fp);
}

struct Employee* readEmployeeFromFile(const char *filename) {
    FILE *fp = fopen(filename, "rb");
    if (fp == NULL) {
        printf("无法打开文件\n");
        return NULL;
    }

    struct Employee *employee = (struct Employee*)malloc(sizeof(struct Employee));
    if (employee == NULL) {
        printf("内存分配失败\n");
        fclose(fp);
        return NULL;
    }

    fread(employee->name, sizeof(char), 50, fp);
    employee->name[strcspn(employee->name, "\n")] = '\0';
    fread(&employee->age, sizeof(int), 1, fp);
    fread(employee->address.street, sizeof(char), 100, fp);
    employee->address.street[strcspn(employee->address.street, "\n")] = '\0';
    fread(employee->address.city, sizeof(char), 50, fp);
    employee->address.city[strcspn(employee->address.city, "\n")] = '\0';

    fclose(fp);
    return employee;
}

int main() {
    struct Employee emp1;
    strcpy(emp1.name, "Frank");
    emp1.age = 30;
    strcpy(emp1.address.street, "123 Main St");
    strcpy(emp1.address.city, "Anytown");

    saveEmployeeToFile(&emp1, "employee.bin");

    struct Employee *read_emp = readEmployeeFromFile("employee.bin");
    if (read_emp != NULL) {
        printf("读取的数据:姓名:%s,年龄:%d,地址:%s,城市:%s\n", read_emp->name, read_emp->age, read_emp->address.street, read_emp->address.city);
        free(read_emp);
    }

    return 0;
}

在这个例子中,Employee 结构体嵌套了 Address 结构体。在保存和读取操作中,分别对每个成员进行相应的读写操作。注意在读取字符串时,处理可能的换行符并添加字符串结束符。

文件操作中的错误处理与优化

文件操作的错误处理

在进行文件操作时,错误处理是非常重要的。每次调用文件操作函数后,都应该检查返回值以判断操作是否成功。例如,在打开文件时,如果 fopen 返回 NULL,表示打开文件失败,可能是文件不存在、权限不足等原因。

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

int main() {
    FILE *fp;
    fp = fopen("nonexistent_file.txt", "r");
    if (fp == NULL) {
        printf("无法打开文件:%s\n", strerror(errno));
        return 1;
    }

    fclose(fp);
    return 0;
}

这里使用 strerror(errno) 函数获取错误信息的字符串描述,errno 是一个全局变量,在文件操作失败时会被设置为相应的错误代码。通过这种方式,我们可以更准确地了解文件操作失败的原因。

在读写操作中,同样需要检查返回值。例如,freadfwrite 函数返回实际读写的字节数或数据块数,如果返回值与预期不符,说明读写操作可能出现了问题。

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

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

int main() {
    struct Student stu1;
    FILE *fp;
    fp = fopen("students.bin", "rb");
    if (fp == NULL) {
        printf("无法打开文件\n");
        return 1;
    }

    size_t read_count = fread(&stu1, sizeof(struct Student), 1, fp);
    if (read_count != 1) {
        printf("读取数据失败\n");
        fclose(fp);
        return 1;
    }

    fclose(fp);
    printf("读取的数据:姓名:%s,年龄:%d,成绩:%.2f\n", stu1.name, stu1.age, stu1.score);
    return 0;
}

在这个例子中,通过检查 fread 的返回值 read_count 是否等于 1,来判断是否成功读取了一个结构体数据块。

文件操作的优化

  1. 缓冲机制:C语言的文件操作函数通常使用缓冲机制来提高读写效率。默认情况下,标准库函数会在内存中维护一个缓冲区,当进行读写操作时,数据先在缓冲区中进行处理,只有当缓冲区满或者调用 fflush 函数时,数据才会真正写入文件。例如,在频繁写入小数据块时,合理利用缓冲区可以减少磁盘I/O操作的次数,从而提高效率。
#include <stdio.h>
#include <string.h>

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

int main() {
    struct Student students[] = {
        {"Grace", 26, 87.0},
        {"Hank", 27, 84.5}
    };
    int num_students = sizeof(students) / sizeof(students[0]);

    FILE *fp;
    fp = fopen("students_optimized.bin", "wb");
    if (fp == NULL) {
        printf("无法打开文件\n");
        return 1;
    }

    for (int i = 0; i < num_students; i++) {
        fwrite(&students[i], sizeof(struct Student), 1, fp);
    }
    // 刷新缓冲区,确保所有数据写入文件
    fflush(fp);

    fclose(fp);
    printf("数据已成功保存到文件\n");
    return 0;
}

在这个例子中,虽然每次只写入一个结构体,但由于缓冲区的存在,实际的磁盘I/O操作次数会减少。在写入完成后,调用 fflush 函数确保所有数据都被写入文件。

  1. 文件定位操作:在某些情况下,我们可能需要在文件中随机访问数据。C语言提供了 fseek 函数来实现文件定位。例如,假设我们已经保存了一个结构体数组到文件中,现在想要直接读取第 n 个结构体,可以使用 fseek 函数移动文件指针到相应的位置。
#include <stdio.h>
#include <string.h>

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

int main() {
    struct Student student;
    FILE *fp;
    fp = fopen("students_array.bin", "rb");
    if (fp == NULL) {
        printf("无法打开文件\n");
        return 1;
    }

    int n = 1; // 要读取的结构体索引
    long offset = n * sizeof(struct Student);
    if (fseek(fp, offset, SEEK_SET) != 0) {
        printf("文件定位失败\n");
        fclose(fp);
        return 1;
    }

    if (fread(&student, sizeof(struct Student), 1, fp) != 1) {
        printf("读取数据失败\n");
        fclose(fp);
        return 1;
    }

    fclose(fp);
    printf("读取到第 %d 个学生的数据:姓名:%s,年龄:%d,成绩:%.2f\n", n + 1, student.name, student.age, student.score);
    return 0;
}

在这个例子中,通过 fseek 函数将文件指针移动到第 n 个结构体的位置(SEEK_SET 表示从文件开头开始偏移),然后使用 fread 函数读取该结构体的数据。

通过合理的错误处理和优化措施,可以使C语言中结构体与文件操作的结合更加稳定和高效,满足不同场景下的数据存储和读取需求。无论是简单的文本文件存储,还是复杂的二进制数据处理,掌握这些技术都能为我们的程序开发提供有力的支持。