CP1-1-用户管理MyUser

C/C++道经 - 项目 - MyUser

文章目录

  • [S01. 用户管理项目](#S01. 用户管理项目)
    • [E01. 准备基础代码](#E01. 准备基础代码)
      • [1. 展示提示菜单](#1. 展示提示菜单)
      • [2. 获取用户输入](#2. 获取用户输入)
    • [E02. 准备结构体数据](#E02. 准备结构体数据)
      • [1. 日期结构体](#1. 日期结构体)
      • [2. 用户结构体](#2. 用户结构体)
      • [3. 用户列表结构体](#3. 用户列表结构体)
    • [E03. 开发业务代码](#E03. 开发业务代码)
      • [1. 添加一名用户](#1. 添加一名用户)
      • [2. 打印全部用户](#2. 打印全部用户)
      • [3. 统计用户数据](#3. 统计用户数据)
      • [4. 查询用户记录](#4. 查询用户记录)
      • [5. 修改用户记录](#5. 修改用户记录)
      • [6. 删除用户记录](#6. 删除用户记录)
      • [7. 导出用户记录](#7. 导出用户记录)
      • [8. 导入用户记录](#8. 导入用户记录)

S01. 用户管理项目

心法:MyUser 是基于 C 语言开发的轻量级控制台用户管理系统,旨在通过模块化设计与基础数据结构(单链表)实现用户信息的全生命周期管理。项目遵循 C 语言结构化编程思想,将功能拆解为 "数据存储 - 交互逻辑 - 业务处理" 三层架构,同时兼顾实用性与学习价值,适合 C 语言初学者理解结构体、链表、文件操作、函数封装等核心知识点的工程化应用。

项目定位:MyUser 以 "简洁高效的本地用户数据管理" 为核心目标,无需依赖数据库或外部框架,仅通过 C 标准库实现用户信息的增删改查、统计分析及数据持久化,可作为 C 语言进阶学习的实战案例,也可作为小型场景(如本地班级信息管理、简易员工档案记录)的轻量工具。

技术架构:MyUser 采用模块化拆分设计,文件结构清晰,各模块职责单一,便于维护与扩展,具体如下:

目录 / 文件 功能职责
main.c 程序入口,负责控制台编码设置、菜单展示、用户输入接收与业务逻辑分发
head/Date.h 定义日期结构体(年 / 月 / 日),为用户 "生日" 字段提供数据类型支持
head/User.h 定义用户结构体(编号 / 姓名 / 年龄 / 身高 / 生日),封装用户信息打印函数
head/UserList.h 基于单链表实现用户列表管理,定义链表节点 / 链表结构体,封装链表打印函数
head/Tool.h 输入工具类,封装字符串转 int/long/long long/float 的通用函数,处理输入异常
service/UserService.h 业务逻辑核心,实现用户增删改查、统计、导入导出等所有核心功能

武技:参考 CB1-1-新手村(一) 笔记创建 C 练习项目 MyUser。

  1. 修改外部命令窗口,并初始化主函数如下:
c 复制代码
// main.c
// Created by 周航宇 on 2025/2/26.
//
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    // 设置控制台编码为UTF-8
    system("chcp 65001 > nul");
    // 输出语句
    printf("\n你好MyUser\n\n");
    // 暂停控制台
    system("pause");
    return 0;
}

效果图

E01. 准备基础代码

1. 展示提示菜单

心法:程序启动后自动打印标准化菜单,清晰列出 9 项功能(1 - 添加 / 2 - 打印 / 3 - 统计 / 4 - 查询 / 5 - 修改 / 6 - 删除 / 7 - 导出 / 8 - 导入 / 9 - 退出),后续用户可以通过输入数字选择对应的功能。

效果图

武技:开发展示提示菜单的功能。

c 复制代码
// main.c
// Created by 周航宇 on 2025/2/26.
//
#include <stdio.h>
#include <stdlib.h>

// 展示提示菜单
void showMenu() {
    printf("\n");
    printf(" 欢迎来到《MyUser用户管理系统》\n");
    printf(" @Author 周航宇\n");
    printf("\n");
    printf(" ================\n");
    printf(" =  请输入命令  =\n");
    printf(" ================\n");
    printf("\n");
    printf("【1】添加一名用户\n");
    printf("【2】打印全部用户\n");
    printf("【3】统计用户数据(包括用户总数和一些平均值相关信息)\n");
    printf("【4】查询用户记录(按编号查找)\n");
    printf("【5】修改用户记录(按编号修改)\n");
    printf("【6】删除用户记录(按编号删除)\n");
    printf("【7】导出用户数据\n");
    printf("【8】导入用户数据\n");
    printf("【9】退出系统\n");
    printf("\n");
}

int main(void) {
    // 设置控制台编码为UTF-8
    system("chcp 65001 > nul");
    // 展示提示菜单
    showMenu();
    // 暂停控制台
    system("pause");
    return 0;
}

2. 获取用户输入

心法:通过 Tool.h 封装的通用输入函数(cin_d,cin_ll,cin_f,cin_s 等)统一处理用户输入,自动转换数据类型(如字符串转数字),并检测非数字字符(返回-1)等异常输入,提示错误信息。

退出系统:用户输入 "9" 时,终止 main 函数中的循环,程序正常退出。

效果图

武技:开发获取用户输入的功能。

  1. 封装一个工具类,用于从控制台接收值:
c 复制代码
// head/Tool.h
// Created by 周航宇 on 2025/2/27.
//
#include <string.h>

// 接收一个字符串,转为 int 类型并返回
void cin_d(char *msg, int *result) {
    // 输出提示信息
    printf("%s", msg);
    // 接收一个字符串
    fflush(stdin);
    // 接收一个字符串
    char input[100];
    gets(input);
    // 将字符串转为 10 进制 int 类型
    char *endPtr;
    *result = strtol(input, &endPtr, 10);
    // 若文件末尾不是 \0,则表示转换过程中遇到非数字字符
    if (*endPtr != '\0') {
        printf("转换过程中遇到非数字字符: %c\n", *endPtr);
    }
}

// 接收一个字符串,转为 long 类型并返回
void cin_l(char *msg, long *result) {
    cin_d(msg, (int *) result);
}

// 接收一个字符串,转为 long long 类型并返回
void cin_ll(char *msg, long long *result) {
    // 输出提示信息
    printf("%s", msg);
    // 接收一个字符串
    fflush(stdin);
    // 接收一个字符串
    char input[100];
    gets(input);
    // 将字符串转为 10 进制 long long 类型
    char *endPtr;
    *result = strtoll(input, &endPtr, 10);
    // 若文件末尾不是 \0,则表示转换过程中遇到非数字字符
    if (*endPtr != '\0') {
        printf("转换过程中遇到非数字字符: %c\n", *endPtr);
    }
}

// 接收一个字符串,转为 float 类型并返回
void cin_f(char *msg, float *result) {
    // 输出提示信息
    printf("%s", msg);
    // 接收一个字符串
    fflush(stdin);
    // 接收一个字符串
    char input[100];
    gets(input);
    // 将字符串转为 10 进制 float 类型
    char *endPtr;
    *result = strtof(input, &endPtr);
    // 若文件末尾不是 \0,则表示转换过程中遇到非数字字符
    if (*endPtr != '\0') {
        printf("转换过程中遇到非数字字符: %c\n", *endPtr);
    }
}

// 接收一个字符串并返回
void cin_s(char *msg, char *result) {
    // 输出提示信息
    printf("%s", msg);
    // 接收一个字符串
    fflush(stdin);
    // 接收一个字符串
    char input[100];
    gets(input);
    // 将字符串转为 char 类型
    strcpy(result, input);
}
  1. 开发枚举,提升菜单指令的可读性:
c 复制代码
// main.c
// Created by 周航宇 on 2025/2/26.
//

// 用户输入枚举
enum MenuCode {
    INSERT = 1, // 插入
    SELECT_ALL = 2, // 打印
    STATISTICS = 3, // 统计
    SELECT = 4, // 查询
    UPDATE = 5, // 修改
    DEL = 6, // 删除
    WRITE = 7, // 导出
    READ = 8, // 导入
    EXIT = 9 // 退出
};
  1. 在主页面中接收控制台变量,并处理用户的输入:
c 复制代码
// main.c
// Created by 周航宇 on 2025/2/26.
//
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    // 设置控制台编码为UTF-8
    system("chcp 65001 > nul");
    // 仅在用户输入 9 时退出循环
    while (1) {
        // 展示提示菜单
        showMenu();
        // 获取用户输入的命令
        int inputNum;
        cin_d("", &inputNum);
        // 根据用户输入的命令执行对应的操作
        if (inputNum == INSERT) {
            printf("\n\t====== 正在添加用户记录.. ======\n\n");
            printf("\n\t====== 用户记录添加完毕! ======\n\n");
        } else if (inputNum == SELECT_ALL) {
            printf("\n\t====== 正在打印用户记录.. ======\n\n");
            printf("\n\t====== 用户记录打印完毕!===\n\n");
        } else if (inputNum == STATISTICS) {
            printf("\n\t====== 正在统计用户数据.. ======\n\n");
            printf("\n\t====== 用户数据统计完毕! ======\n\n");
        } else if (inputNum == SELECT) {
            printf("\n\t====== 正在查询用户记录.. ======\n\n");
            printf("\n\t====== 用户记录查询完毕! ======\n\n");
        } else if (inputNum == UPDATE) {
            printf("\n\t====== 正在修改用户记录.. ======\n\n");
            printf("\n\t====== 用户记录修改完毕! ======\n\n");
        } else if (inputNum == DEL) {
            printf("\n\t====== 正在删除用户记录.. ======\n\n");
            printf("\n\t====== 用户记录删除完毕! ======\n\n");
        } else if (inputNum == WRITE) {
            printf("\n\t====== 正在导出用户数据.. ======\n\n");
            printf("\n\t====== 用户数据导出完毕! ======\n\n");
        } else if (inputNum == READ) {
            printf("\n\t====== 正在导入用户数据.. ======\n\n");
            printf("\n\t====== 用户数据导入完毕! ======\n\n");
        } else if (inputNum == EXIT) {
            break;
        } else {
            printf("\n\t输入有误,请重新输入!\n\n");
        }
        // 暂停控制台
        system("pause");
        // 清空控制台
        system("cls");
    }
    return 0;
}

E02. 准备结构体数据

1. 日期结构体

心法:在 C 语言里并没有内置的日期类型,但你可以通过几种方式来定义日期类型的变量,但可以定义一个结构体来表示日期,结构体里包含年、月、日这几个成员。
武技:开发日期结构体

  1. 开发日期结构体:
c 复制代码
// head/Date.h
// Created by 周航宇 on 2025/2/26.
//

// 定义日期结构体
typedef struct Date {
    int year;  // 年
    int month; // 月
    int day;   // 日
} Date;
  1. 测试日期结构体:
c 复制代码
// main.h
// Created by 周航宇 on 2025/2/26.
//
#include <stdio.h>
#include <stdlib.h>
#include "head/Date.h"

int main(void) {
    // 设置控制台编码为UTF-8
    system("chcp 65001 > nul");
    // 测试 Date 结构体
    Date date = {2023, 1, 1};
    printf("%d-%d-%d\n", date.year, date.month, date.day);
	// 略
    return 0;
}

2. 用户结构体

心法:用户结构体用来装载一个用户的信息(包括编号,姓名,年龄,生日等)。

注意:用户结构体包含一个日期属性(生日),需要引入并使用 Date.h,所在 main.c 中测试时,不要再单独引入 Date.h 文件,以免爆发重复引入相关的错误。

武技:开发用户结构体。

  1. 开发用户结构体:
c 复制代码
// head/User.h
// Created by 周航宇 on 2025/2/26.
//
#include "Date.h"

// 定义用户结构体,并起别名为 User
typedef struct User {
    long long id; // 编号
    char name[20]; // 姓名
    int age; // 年龄
    float height; // 身高
    Date birthday; // 生日
} User;

void printUser(User *user){
    printf("编号:%lld\n", user->id);
    printf("姓名:%s\n", user->name);
    printf("年龄:%d\n", user->age);
    printf("身高:%.2f\n", user->height);
    printf("生日:%d-%d-%d\n",
           user->birthday.year,
           user->birthday.month,
           user->birthday.day);
}
  1. 测试用户结构体:
c 复制代码
// main.h
// Created by 周航宇 on 2025/2/26.
//
#include <stdio.h>
#include <stdlib.h>
#include "head/User.h"

int main(void) {
    // 设置控制台编码为UTF-8
    system("chcp 65001 > nul");
    // 测试 User 结构体
    User user = {
            .id = 1,
            .name = "张三",
            .age = 20,
            .height = 180.5F,
            .birthday = {
                    .year = 2025,
                    .month = 2,
                    .day = 22
            }
    };
    printf("用户编号:%lld\n", user.id);
    printf("用户姓名:%s\n", user.name);
    printf("用户年龄:%d\n", user.age);
    printf("用户身高:%.1f\n", user.height);
    printf("用户生日:%d-%d-%d\n",
           user.birthday.year,
           user.birthday.month,
           user.birthday.day);
    // 略
    return 0;
}

3. 用户列表结构体

心法:用户列表结构体用来装载全部用户的信息,底层使用单链表数据结构实现。

注意:用户列表结构体包含用户结构体,用户结构体中包含一个日期属性(生日),需要引入并使用 Date.h,所在 main.c 中测试时,不要再单独引入 User.h 和 Date.h 这两个文件,以免爆发重复引入相关的错误。

武技:开发用户列表结构体。

  1. 开发用户列表结构体:
c 复制代码
// head/UserList.h
// Created by 周航宇 on 2025/2/26.
//
#include "User.h"

// 定义链表节点结构体
typedef struct Node {
    User user; // 用户数据
    struct Node *next; // 后继指针
} Node;

// 定义链表结构体
typedef struct UserList {
    Node *head; // 头节点
    int size; // 链表长度
} UserList;

void printUserList(UserList *userList) {
    // 遍历链表,输出用户信息
    Node *current = userList->head;
    if (current == NULL) {
        printf("当前没有用户信息!\n");
        return;
    }
    printf("编号\t姓名\t年龄\t身高\t生日\n");
    while (current != NULL) {
        printf("%lld\t%s\t%d\t%.2f\t%d-%d-%d\n",
               current->user.id,
               current->user.name,
               current->user.age,
               current->user.height,
               current->user.birthday.year,
               current->user.birthday.month,
               current->user.birthday.day);
        // 移动到下一个节点
        current = current->next;
    }
}
  1. 测试用户列表结构体:使用 memset() 函数需要引入 string.h 头文件:
c 复制代码
// main.c
// Created by 周航宇 on 2025/2/26.
//
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "head/UserList.h"

int main(void) {
    // 设置控制台编码为UTF-8
    system("chcp 65001 > nul");
    // 测试 UserList 结构体
    UserList userList;
    // 初始化链表:将链表的头节点指向的内存块清零,确保了链表在使用前处于一个已知的、安全的状态。
    memset(&userList, 0, sizeof(userList));
    // 使用 malloc 分配一个 Node 长度的内存
    Node *newNode = malloc(sizeof(Node));
    newNode->next = NULL;
    newNode->user.id = 1;
    newNode->user.age = 20L;
    newNode->user.height = 180.5F;
    strcpy(newNode->user.name, "张三");
    newNode->user.birthday.year = 2000L;
    newNode->user.birthday.month = 1L;
    newNode->user.birthday.day = 1L;
    // 将新节点插入链表头部
    newNode->next = userList.head;
    userList.head = newNode;
    userList.size++;
    // 输出链表头的数据
    printf("编号: %lld\n", userList.head->user.id);
    printf("姓名: %s\n", userList.head->user.name);
    printf("年龄: %ld\n", userList.head->user.age);
    printf("身高: %.1f\n", userList.head->user.height);
    printf("生日: %ld-%ld-%ld\n",
           userList.head->user.birthday.year,
           userList.head->user.birthday.month,
           userList.head->user.birthday.day);
    // 释放内存
    free(newNode);
    // 略
    return 0;
}

E03. 开发业务代码

心法:MyUser 提供 8 大核心功能(添加,打印,统计,查询,修改,删除,导出,导入),覆盖用户数据管理的全流程,所有操作均通过控制台交互完成,操作逻辑直观易懂。

注意:UserService.h 中需要引入了 UserList.h(关联引入 User.h 和 Date.h),所在 main.c 中测试时,不要再引入 UserList,User.h 和 Date.h 这三个文件,以免爆发重复引入相关的错误。

武技:创建 UserService.h 头文件

  1. 创建 UserService.h 头文件:
c 复制代码
// service/UserService.h
// Created by 周航宇 on 2025/2/26.
//
#include <stdio.h>  
#include <stdlib.h>  
#include "../head/UserList.h"  
#include "../head/Tool.h"
  1. 初始化 main.c 文件:
c 复制代码
// main.c
// Created by 周航宇 on 2025/2/26.
//
#include <stdio.h>
#include <stdlib.h>
#include "service/UserService.h"

int main(void) {
	// 略
}

1. 添加一名用户

心法:通过单链表头部插入方式添加新用户,避免遍历链表提升效率。

操作流程

  1. 程序提示用户依次输入 "用户编号(long long)、姓名(字符串)、年龄(int)、身高(float)、生日(年 / 月 / 日)"。
  2. 动态分配链表节点内存(malloc),将输入信息赋值到节点的用户结构体中。
  3. 将新节点插入链表头部,更新链表长度,并即时打印新添加的用户信息,确认插入成功。
  4. 异常处理:若内存分配失败(malloc 返回 NULL),提示 "内存分配失败" 并终止操作。

效果图

武技:开发添加一名用户的功能。

c 复制代码
// service/UserService
// Created by 周航宇 on 2025/2/26.
//

// 添加一名用户
void insert(UserList *userList) {
    // 使用 malloc 分配一个 Node 长度的内存
    Node *newNode = malloc(sizeof(Node));
    if (newNode == NULL) {
        printf("内存分配失败!\n");
        return;
    }
    // 初始化新节点
    newNode->next = NULL;
    // 输入用户信息
    cin_ll("输入编号:", &newNode->user.id);
    cin_s("输入姓名:", newNode->user.name);
    cin_d("输入年龄:", &newNode->user.age);
    cin_f("输入身高:", &newNode->user.height);
    cin_d("输入生日(年):", &newNode->user.birthday.year);
    cin_d("输入生日(月):", &newNode->user.birthday.month);
    cin_d("输入生日(日):", &newNode->user.birthday.day);
    // 将新节点插入链表头部
    newNode->next = userList->head;
    userList->head = newNode;
    // 输出刚刚插入的用户信息
    printf("\n用户信息已插入:\n\n");
    printUser(&userList->head->user);
}
c 复制代码
// main.c
// Created by 周航宇 on 2025/2/26.
//

int main(void) {
    // 设置控制台编码为UTF-8
    system("chcp 65001 > nul");
    // 创建用户链表
    UserList userList = {0};
    while (1) {
        // 略
        if (inputNum == INSERT) {
            printf("\n\t====== 正在添加用户记录.. ======\n\n");
            insert(&userList);
            printf("\n\t====== 用户记录添加完毕! ======\n\n");
        } 
		// 略
    }
    return 0;
}

2. 打印全部用户

心法:遍历单链表,以 "表格化格式" 输出所有用户信息,便于快速查看整体数据。

输出格式:采用 "编号 \t 姓名 \t 年龄 \t 身高 \t 生日" 的列对齐格式,身高保留 2 位小数,生日以 "年 - 月 - 日" 格式展示。

空数据处理:若链表为空(无用户数据),自动提示 "当前没有用户信息!",避免遍历空指针异常。

效果图

武技:开发打印全部用户的功能。

c 复制代码
// service/UserService
// Created by 周航宇 on 2025/2/26.
//

void selectAll(UserList *userList) {
    printUserList(userList);
}
c 复制代码
// main.c
// Created by 周航宇 on 2025/2/26.
//

int main(void) {
    // 设置控制台编码为UTF-8
    system("chcp 65001 > nul");
    // 创建用户链表
    UserList userList = {0};
    while (1) {
        // 略
        else if (inputNum == SELECT_ALL) {
            printf("\n\t====== 正在打印用户记录.. ======\n\n");
            selectAll(&userList);
            printf("\n\t====== 用户记录打印完毕!===\n\n");
        } 
		// 略
    }
    return 0;
}

3. 统计用户数据

心法:当前版本支持 2 类核心统计指标(用户总数和平均年龄),单次遍历链表完成计数与年龄累加,时间复杂度为 O (n),确保高效性。

统计指标 - 用户总数:遍历链表计数,统计当前系统中存储的用户总数量。

统计指标 - 平均年龄:累加所有用户年龄,计算平均值(自动转换为 float 类型,保留 2 位小数)。

效果图

武技:开发统计用户数据的功能。

c 复制代码
// service/UserService
// Created by 周航宇 on 2025/2/26.
//

void statistics(UserList *userList) {
    // 统计用户数量和平均年龄
    int count = 0;
    int totalAge = 0;
    Node *current = userList->head;
    while (current != NULL) {
        count++;
        totalAge += current->user.age;
        current = current->next;
    }
    float averageAge = (float) totalAge / (float) count;
    // 输出统计结果
    printf("\n用户数量:%d\n", count);
    printf("平均年龄:%.2f\n", averageAge);
}
c 复制代码
// main.c
// Created by 周航宇 on 2025/2/26.
//

int main(void) {
    // 设置控制台编码为UTF-8
    system("chcp 65001 > nul");
    // 创建用户链表
    UserList userList = {0};
    while (1) {
        // 略
		else if (inputNum == STATISTICS) {
            printf("\n\t====== 正在统计用户数据.. ======\n\n");
            statistics(&userList);
            printf("\n\t====== 用户数据统计完毕! ======\n\n");
        }
		// 略
    }
    return 0;
}

4. 查询用户记录

心法:按 "用户编号" 精确查询用户记录。

操作流程

  1. 提示用户输入待查询的用户编号。
  2. 遍历链表,对比节点中用户的编号与输入编号:
    • 找到匹配用户时,调用 printUser 函数打印该用户的完整信息。
    • 未找到匹配用户时,提示 "未找到该编号的用户!"。

效果图

武技:开发按用户编号查询用户记录的功能。

c 复制代码
// service/UserService
// Created by 周航宇 on 2025/2/26.
//

void select(UserList *userList) {
    // 输入要查询的用户编号
    long long id;
    cin_ll("\n请输入要查询的用户编号:", &id);
    // 遍历链表,查找用户
    Node *current = userList->head;
    while (current != NULL) {
        if (current->user.id == id) {
            // 输出用户信息
            printf("\n用户信息:\n\n");
            printUser(&current->user);
            return;
        }
        current = current->next;
    }
    printf("\n未找到该编号的用户!\n");
}
c 复制代码
// main.c
// Created by 周航宇 on 2025/2/26.
//

int main(void) {
    // 设置控制台编码为UTF-8
    system("chcp 65001 > nul");
    // 创建用户链表
    UserList userList = {0};
    while (1) {
        // 略
		else if (inputNum == SELECT) {
            printf("\n\t====== 正在查询用户记录.. ======\n\n");
            select(&userList);
            printf("\n\t====== 用户记录查询完毕! ======\n\n");
        }
		// 略
    }
    return 0;
}

5. 修改用户记录

心法:按 "用户编号" 定位目标用户,支持修改除 "编号" 外的所有字段(避免编号重复导致查询混乱)。

操作流程

  1. 提示用户输入待修改的用户编号,遍历链表定位目标节点。
  2. 定位成功后,提示用户依次输入 "新姓名、新年龄、新身高、新生日"。
  3. 将新输入的信息覆盖原用户结构体中的对应字段。
  4. 即时打印修改后的用户信息,确认修改成功。
  5. 未找到目标用户时,提示 "未找到该编号的用户!"。

效果图

武技:开发按用户编号修改用户记录的功能。

c 复制代码
// service/UserService
// Created by 周航宇 on 2025/2/26.
//

void update(UserList *userList) {
    // 输入要修改的用户编号
    long long id;
    cin_ll("\n请输入要修改的用户编号:", &id);
    // 遍历链表,查找用户
    Node *current = userList->head;
    while (current != NULL) {
        if (current->user.id == id) {
            // 输入新的用户信息
            printf("\n请输入新的用户信息:\n\n");
            cin_s("> 姓名:", current->user.name);
            cin_d("> 年龄:", &current->user.age);
            cin_f("> 身高:", &current->user.height);
            cin_d("> 生日(年):", &current->user.birthday.year);
            cin_d("> 生日(月):", &current->user.birthday.month);
            cin_d("> 生日(日):", &current->user.birthday.day);
            // 输出修改后的用户信息
            printf("\n%lld号用户信息已修改:\n\n", id);
            printUser(&current->user);
            return;
        }
        current = current->next;
    }
    printf("\n未找到该编号的用户!\n");
}
c 复制代码
// main.c
// Created by 周航宇 on 2025/2/26.
//

int main(void) {
    // 设置控制台编码为UTF-8
    system("chcp 65001 > nul");
    // 创建用户链表
    UserList userList = {0};
    while (1) {
        // 略
		else if (inputNum == UPDATE) {
            printf("\n\t====== 正在修改用户记录.. ======\n\n");
            update(&userList);
            printf("\n\t====== 用户记录修改完毕! ======\n\n");
        }
		// 略
    }
    return 0;
}

6. 删除用户记录

心法:按 "用户编号" 定位目标节点,支持删除链表头部、中间及尾部节点,删除后释放内存(避免内存泄漏)。

操作流程

  1. 提示用户输入待删除的用户编号,通过 "双指针"(当前节点 currentUser、前驱节点 previousUser)遍历链表:
    • 若删除 "头部节点":直接将链表头指针指向头部节点的下一个节点。
    • 若删除 "中间 / 尾部节点":将前驱节点的 next 指向当前节点的下一个节点;
  2. 释放当前节点内存,更新链表长度。
  3. 未找到目标用户时,提示 "未找到该编号的用户!"。

效果图

武技:开发按用户编号删除用户记录的功能。

c 复制代码
// service/UserService
// Created by 周航宇 on 2025/2/26.
//

void del(UserList *userList) {
    long long id;
    cin_ll("\n请输入要删除的用户编号:", &id);
    // 当前用户
    Node *currentUser = userList->head;
    // 上一个用户
    Node *previousUser = NULL;
    // 遍历链表,查找用户
    while (currentUser != NULL) {
        // 找到用户
        if (currentUser->user.id == id) {
            // 删除的是第一个用户
            if (previousUser == NULL) {
                userList->head = currentUser->next;
            }
            // 删除的是中间的用户
            else {
                previousUser->next = currentUser->next;
            }
            // 使用 free 释放内存,并将链表长度减一
            free(currentUser);
            userList->size--;
            return;
        }
        // 移动
        previousUser = currentUser;
        currentUser = currentUser->next;
    }
    printf("\n未找到该编号的用户!\n");
}
c 复制代码
// main.c
// Created by 周航宇 on 2025/2/26.
//

int main(void) {
    // 设置控制台编码为UTF-8
    system("chcp 65001 > nul");
    // 创建用户链表
    UserList userList = {0};
    while (1) {
        // 略
		else if (inputNum == DEL) {
            printf("\n\t====== 正在删除用户记录.. ======\n\n");
            del(&userList);
            printf("\n\t====== 用户记录删除完毕! ======\n\n");
        } 
		// 略
    }
    return 0;
}

7. 导出用户记录

心法:使用 fopen 以 "写模式(w)" 打开 Markdown 文件、"二进制写模式(wb)" 打开二进制文件,操作完成后调用 fclose 关闭文件,避免文件句柄泄漏。

导出格式:同时生成两种格式文件,满足不同场景需求:

  • Markdown 文件(user.md:以表格形式存储用户数据,支持用记事本、Markdown 编辑器打开,便于人类阅读。
  • 二进制文件(user.bin):以二进制形式存储用户数据(先存链表长度,再存每个用户的完整结构体),便于后续导入时快速读取,节省存储空间。

效果图

导出文件的位置如图

武技:开发导出用户记录的功能。

c 复制代码
// service/UserService
// Created by 周航宇 on 2025/2/26.
//

void write(UserList *userList) {
    // 打开文件
    FILE *fp = fopen("user.md", "w");
    if (fp == NULL) {
        printf("文件打开失败!\n");
        return;
    }
    // 写入表头
    fprintf(fp, "|编号|姓名|年龄|身高|生日|\n");
    fprintf(fp, "|---|---|---|---|---|\n");
    // 遍历链表,写入用户信息
    Node *current = userList->head;
    while (current != NULL) {
        fprintf(fp, "|%lld|%s|%d|%.2f|%d-%d-%d|\n",
                current->user.id,
                current->user.name,
                current->user.age,
                current->user.height,
                current->user.birthday.year,
                current->user.birthday.month,
                current->user.birthday.day);
        // 移动
        current = current->next;
    }
    // 关闭文件
    fclose(fp);
    // 写出一个二进制文件
    FILE *fp2 = fopen("user.bin", "wb");
    if (fp2 == NULL) {
        printf("文件打开失败!\n");
        return;
    }
    // 写入链表长度
    fwrite(&userList->size, sizeof(int), 1, fp2);
    // 遍历链表,写入用户信息
    current = userList->head;
    while (current != NULL) {
        // 写入用户信息
        fwrite(&current->user, sizeof(User), 1, fp2);
        // 移动
        current = current->next;
    }
    // 关闭文件
    fclose(fp2);
}
c 复制代码
// main.c
// Created by 周航宇 on 2025/2/26.
//

int main(void) {
    // 设置控制台编码为UTF-8
    system("chcp 65001 > nul");
    // 创建用户链表
    UserList userList = {0};
    while (1) {
        // 略
		else if (inputNum == WRITE) {
            printf("\n\t====== 正在导出用户数据.. ======\n\n");
            write(&userList);
            printf("\n\t====== 用户数据导出完毕! ======\n\n");
        } 
		// 略
    }
    return 0;
}

8. 导入用户记录

心法:仅从之前导出的 user.bin 二进制文件导入数据(二进制格式可保证数据完整性,避免文本解析错误)。

导入逻辑

  1. 导入前自动清空原有链表(遍历释放所有节点内存,避免内存泄漏)。
  2. 以 "二进制读模式(rb)" 打开 user.bin,先读取链表长度,再循环读取每个用户的结构体数据。
  3. 为每个读取的用户数据分配新链表节点,插入链表头部,重建用户列表。
  4. 导入完成后自动调用 "打印全部用户" 功能,展示导入结果。
  5. 异常处理:若 user.bin 文件不存在或打开失败,提示 "文件打开失败!"。

效果图

武技:开发导入用户记录的功能。

c 复制代码
// service/UserService
// Created by 周航宇 on 2025/2/26.
//

void read(UserList *userList) {
    // 清空链表
    Node *current = userList->head;
    while (current != NULL) {
        Node *next = current->next;
        free(current);
        current = next;
    }
    userList->head = NULL;
    userList->size = 0;
    // 打开二进制文件
    FILE *fp = fopen("user.bin", "rb");
    if (fp == NULL) {
        printf("文件打开失败!\n");
        return;
    }
    // 读取链表长度
    fread(&userList->size, sizeof(int), 1, fp);
    // 临时用户
    User tempUser;
    // 从文件中读取用户信息
    while (fread(&tempUser, sizeof(User), 1, fp) == 1) {
        // 使用 malloc 分配一个 Node 长度的内存
        Node *newNode = malloc(sizeof(Node));
        if (newNode == NULL) {
            printf("内存分配失败!\n");
            return;
        }
        // 初始化新节点
        newNode->next = NULL;
        newNode->user.id = tempUser.id;
        newNode->user.age = tempUser.age;
        newNode->user.height = tempUser.height;
        newNode->user.birthday.year = tempUser.birthday.year;
        newNode->user.birthday.month = tempUser.birthday.month;
        newNode->user.birthday.day = tempUser.birthday.day;
        strcpy(newNode->user.name, tempUser.name);
        // 将新节点插入链表头部
        newNode->next = userList->head;
        userList->head = newNode;
    }
    printUserList(userList);
    // 关闭文件
    fclose(fp);
}
c 复制代码
// main.c
// Created by 周航宇 on 2025/2/26.
//

int main(void) {
    // 设置控制台编码为UTF-8
    system("chcp 65001 > nul");
    // 创建用户链表
    UserList userList = {0};
    while (1) {
        // 略
		else if (inputNum == READ) {
            printf("\n\t====== 正在导入用户数据.. ======\n\n");
            read(&userList);
            printf("\n\t====== 用户数据导入完毕! ======\n\n");
        }
		// 略
    }
    return 0;
}

C/C++道经 - 项目 - MyUser