C 练习实例48(长文解析)

更新时间:

💡一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

  • 新开坑项目:《Spring AI 项目实战》 正在持续爆肝中,基于 Spring AI + Spring Boot 3.x + JDK 21..., 点击查看 ;
  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...点击查看项目介绍 ;演示链接: http://116.62.199.48:7070 ;
  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/ ;

截止目前, 星球 内专栏累计输出 90w+ 字,讲解图 3441+ 张,还在持续爆肝中.. 后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,权限管理,Spring Cloud Alibaba 微服务等等,已有 3100+ 小伙伴加入学习 ,欢迎点击围观

前言

在编程学习过程中,通过实践具体实例来巩固理论知识是极为有效的方式。C语言作为编程领域的基石,其练习实例不仅帮助开发者理解语法细节,更能培养逻辑思维能力。本文将围绕“C 练习实例48”展开,通过案例分析、代码实现和知识点讲解,帮助读者掌握核心概念,并理解如何将理论应用到实际问题中。

问题描述与背景分析

实例背景

假设“C 练习实例48”的题目是:设计一个学生信息管理系统,支持添加、删除、查询学生信息,并按成绩排序后输出结果。这一实例需要综合运用C语言的结构体、指针、动态内存分配、文件操作等知识,是检验编程能力的典型场景。

需求拆解

  1. 学生信息存储:需要定义包含姓名、学号、成绩等字段的结构体。
  2. 动态管理数据:学生数量可能变化,需通过动态内存分配(如mallocrealloc)实现灵活管理。
  3. 功能模块划分:添加、删除、查询、排序、输出等操作需封装为函数,提高代码复用性。
  4. 持久化存储:支持将学生信息保存到文件或从文件读取数据,避免每次运行程序时数据丢失。

核心知识点详解

知识点1:结构体(Struct)

定义与用途

结构体是C语言中自定义数据类型的重要工具,允许将不同类型的数据组合成一个逻辑单元。例如,学生信息可以定义如下:

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

比喻:结构体就像一个“信息仓库”,每个字段(如nameid)是仓库中的不同储物柜,共同保存学生信息。

应用场景

在本实例中,结构体用于存储每位学生的详细数据,并通过指针操作实现动态管理。


知识点2:动态内存分配

关键函数

  • malloc():分配指定字节的内存空间。
  • realloc():调整已分配内存的大小。
  • free():释放不再使用的内存,避免内存泄漏。

实例中的实现

假设初始分配内存存放10名学生的信息,当需要添加第11名学生时,通过realloc扩展内存:

struct Student* students = (struct Student*)malloc(10 * sizeof(struct Student));  
// ... 添加数据后,当需要扩容时  
students = (struct Student*)realloc(students, 20 * sizeof(struct Student));  

比喻:动态内存分配如同租用可扩展的仓库空间,根据需求灵活调整大小,避免资源浪费或不足。


知识点3:函数指针与排序算法

快速排序的实现

排序功能可以通过快速排序算法实现,但需定义比较函数。例如,按成绩排序的比较函数:

int compare_by_score(const void* a, const void* b) {  
    struct Student* student_a = (struct Student*)a;  
    struct Student* student_b = (struct Student*)b;  
    return (student_a->score - student_b->score);  
}  

调用qsort函数时,通过函数指针传递比较逻辑:

qsort(students, count, sizeof(struct Student), compare_by_score);  

比喻:函数指针如同“遥控器”,可以随时切换排序规则(如按成绩或学号排序),而无需修改主函数代码。


知识点4:文件操作

读写文件的步骤

  1. 打开文件:使用fopen函数,指定模式(如"w+"表示读写)。
  2. 写入数据:通过fprintf将结构体数据写入文件。
  3. 读取数据:通过fscanf从文件中读取数据到结构体变量。
  4. 关闭文件:用fclose释放资源。

示例代码片段

FILE* file = fopen("students.dat", "wb");  
fwrite(students, sizeof(struct Student), count, file);  
fclose(file);  

注意事项:文件操作需处理可能的错误(如文件不存在),并通过errno或返回值判断操作是否成功。

完整代码实现与解析

主程序结构

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

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

// 函数声明  
void add_student(struct Student**, int*);  
void delete_student(struct Student**, int*);  
void sort_students(struct Student*, int);  
void print_students(struct Student*, int);  
void save_to_file(struct Student*, int);  
void load_from_file(struct Student**, int*);  

int main() {  
    struct Student* students = NULL;  
    int count = 0;  
    int choice;  

    while (1) {  
        printf("1. 添加学生\n2. 删除学生\n3. 查询并排序\n4. 保存数据\n5. 加载数据\n6. 退出\n");  
        printf("请输入选项:");  
        scanf("%d", &choice);  

        switch(choice) {  
            case 1:  
                add_student(&students, &count);  
                break;  
            case 2:  
                delete_student(&students, &count);  
                break;  
            case 3:  
                sort_students(students, count);  
                print_students(students, count);  
                break;  
            case 4:  
                save_to_file(students, count);  
                break;  
            case 5:  
                load_from_file(&students, &count);  
                break;  
            case 6:  
                free(students);  
                return 0;  
            default:  
                printf("无效选项,请重试。\n");  
        }  
    }  
}  

关键函数实现

添加学生函数

void add_student(struct Student** students, int* count) {  
    struct Student* new_students;  
    int new_count = (*count) + 1;  

    // 扩容内存  
    new_students = (struct Student*)realloc(*students, new_count * sizeof(struct Student));  
    if (!new_students) {  
        printf("内存分配失败!\n");  
        return;  
    }  

    *students = new_students;  
    struct Student* current = &(*students)[*count];  

    printf("请输入姓名:");  
    scanf("%s", current->name);  
    printf("请输入学号:");  
    scanf("%d", &current->id);  
    printf("请输入成绩:");  
    scanf("%f", &current->score);  

    (*count)++;  
}  

说明:通过realloc动态扩展内存空间,并通过指针操作添加新学生数据。

删除学生函数

void delete_student(struct Student** students, int* count) {  
    if (*count == 0) {  
        printf("当前无学生信息!\n");  
        return;  
    }  

    int id_to_delete;  
    printf("请输入要删除学生的学号:");  
    scanf("%d", &id_to_delete);  

    int found = 0;  
    for (int i = 0; i < *count; i++) {  
        if ((*students)[i].id == id_to_delete) {  
            // 将后续元素前移一位  
            memmove(&(*students)[i], &(*students)[i + 1],  
                (*count - i - 1) * sizeof(struct Student));  
            (*count)--;  
            found = 1;  
            break;  
        }  
    }  

    if (!found) {  
        printf("未找到该学号的学生!\n");  
    } else {  
        // 重新分配内存(可选,根据需求决定是否收缩内存)  
        *students = (struct Student*)realloc(*students, (*count) * sizeof(struct Student));  
    }  
}  

说明:使用memmove移动数据覆盖被删除的记录,并通过realloc优化内存空间。


文件操作函数

void save_to_file(struct Student* students, int count) {  
    FILE* file = fopen("students.dat", "wb");  
    if (!file) {  
        printf("无法打开文件!\n");  
        return;  
    }  

    fwrite(students, sizeof(struct Student), count, file);  
    fclose(file);  
    printf("数据已保存!\n");  
}  

void load_from_file(struct Student** students, int* count) {  
    FILE* file = fopen("students.dat", "rb");  
    if (!file) {  
        printf("文件不存在或无法读取!\n");  
        return;  
    }  

    // 先清空原有数据  
    free(*students);  
    *students = NULL;  
    *count = 0;  

    // 读取文件大小  
    fseek(file, 0L, SEEK_END);  
    long file_size = ftell(file);  
    rewind(file);  

    if (file_size % sizeof(struct Student) != 0) {  
        printf("文件格式错误!\n");  
        fclose(file);  
        return;  
    }  

    int new_count = file_size / sizeof(struct Student);  
    *students = (struct Student*)malloc(new_count * sizeof(struct Student));  
    fread(*students, sizeof(struct Student), new_count, file);  
    *count = new_count;  

    fclose(file);  
    printf("数据加载成功!\n");  
}  

实战案例与调试技巧

案例1:添加与删除操作

假设依次执行以下操作:

  1. 添加学生A(学号1001,成绩90)。
  2. 添加学生B(学号1002,成绩85)。
  3. 删除学号1001的学生。
  4. 查看剩余学生信息。

预期结果:仅显示学生B的信息。

案例2:排序与输出

输入以下学生信息:

  • 学生C(学号1003,成绩88)
  • 学生D(学号1004,成绩92)
  • 学生E(学号1005,成绩78)

按成绩排序后,输出顺序应为:D(92)、C(88)、E(78)。

调试技巧

  1. 内存泄漏检查:使用工具如Valgrind检测malloc/free是否配对。
  2. 断点调试:在关键函数(如add_student)中设置断点,逐步观察内存变化。
  3. 日志输出:在函数入口和出口打印变量值,辅助排查逻辑错误。

总结与扩展

核心收获

通过“C 练习实例48”的实现,读者可以掌握以下能力:

  1. 结构体的定义与动态管理。
  2. 指针操作与内存分配的技巧。
  3. 排序算法与函数指针的结合使用。
  4. 文件读写与数据持久化的实现方法。

进阶方向

  1. 用户界面优化:将命令行交互改为图形界面(如使用Windows API或跨平台框架)。
  2. 数据验证:添加输入合法性检查(如学号唯一性、成绩范围限制)。
  3. 多线程支持:在多用户场景下实现线程安全的数据操作。

开发者建议

  • 分模块开发:先实现核心功能(如添加和删除),再逐步扩展排序和文件操作。
  • 代码复用:将常用函数(如内存分配、文件操作)封装为独立工具库。
  • 版本控制:使用Git记录开发过程,便于回溯和团队协作。

通过系统化学习和实践,开发者不仅能解决具体问题,更能构建扎实的编程思维,为后续学习更复杂的C语言进阶内容(如网络编程、嵌入式开发)奠定基础。

最新发布