4-3 项目-学生成绩管理系统(上)

4-3 项目:学生成绩管理系统(上)

⏱ 本节预计用时:75分钟

本节意义:几乎所有的软件工程师都做过管理系统,里边涉及到的增删改查工作中必用。这一讲先搭一个带"输入安检"的最小系统,把骨架立起来;还有一半功能(删除、修改、查询、统计)放在下一讲补全。

本节目标:理解需求怎么变成数据结构;做出一个多文件、带异常处理的最小学生成绩管理系统(能添加、能查看、能安全退出);第一次亲手建立"永远不要相信用户输入"的职业习惯。


1 需求分析

上午十点,老板敲了敲你的工位:"小张,来活了。隔壁小学的张老师想要一个学生成绩管理系统,你对接一下。"

你端着笔记本找到张老师,听她说了一通,记下了这么几条:

复制代码
1. 查看所有学生(学号、姓名、三门课成绩、总分、平均分)
2. 添加学生
3. 删除学生
4. 修改某学生的某门成绩
5. 按学号查询单个学生
6. 分析成绩信息(学生总数、平均总分、最高分、最低分、及格率)
7. 退出

看着这些需求,我们能想到什么?

能想到每个学生是一个结构体,里边存着他的信息,学生的信息保存在一个数组里,这里的增删改查本质是对数组这个数据结构做增删改查。能想到model-view架构,数据的定义和增删改查的逻辑部分在model里,界面展示在view里,还有一个main做流程控制和状态切换。

数据结构就是存放数据的 "容器格式",规定数据怎么摆、怎么存,方便你增删改查。数组就是一种数据结构。

在《C语言(上)》里,我们做的井字棋项目是从一个main文件拆成多函数,再用model-view架构按功能拆成多文件的。

在这里,我们直接跳过拆分这一步,直接用model-view架构去做这个项目。

在你的code目录下新建如下的文件夹和文件(跟着敲):

复制代码
code/4-3-学生成绩管理系统/
├── model/        ← 建文件夹
├── view/         ← 建文件夹
└── main.c        ← 等会儿建文件

2 第一步:确定数据结构

这一次我们在做一个最小的能跑通示例之前新增一步,确定怎么用数据来表示需求,即确定学生的数据结构。

之所以这么做是因为它符合一般人软件设计的思路,先确定怎么用数据来表示需求,再对这些数据编写算法,来实现我们想要的功能。编写的算法可以是简单的增删改查,也可以是和硬件交互来显示一些漂亮的图案(多半要用到第三方库),也可以是别的。

先想清楚数据:一个学生有哪些信息?一个系统要管什么?

一个学生:学号、姓名、三门课成绩、总分、平均分。这不就是结构体嘛:

c 复制代码
typedef struct {
    int id;                      /* 学号 */
    char name[32];               /* 姓名(字符串) */
    float scores[3];             /* 三门课成绩:语文 数学 英语 */
    float total;                 /* 总分 */
    float average;               /* 平均分 */
} Student;

一个系统:得有一排学生 + 记着"现在有几个"。数组大小先写多少?你想着说,张老师一个班也就几十人......先放 10 个,以后不够再说:

c 复制代码
#define MAX_NAME   32    /* 名字最长 31 个字符,多留 1 格(为什么?以后细说) */
#define MAX_CAPACITY 10  /* 先能存 10 个学生 */
#define SUBJECT_COUNT 3  /* 固定三门课 */

typedef struct {
    Student students[10];   /* 一排学生 */
    int count;              /* 当前有几个人 */
} StudentSystem;

到这里你发现没有:只要把数据定清楚了,后面所有功能的代码都好写了。加学生 = 往 students 里放一个;删学生 = 把后面的往前挪;查学生 = 遍历找学号。数据先行,这是软件的根基。

把这两段放进 model/student.h。

还记得什么是.h文件吗?这个文件是"目录",只写类型定义和函数声明,不写实现。

model/student.h:

复制代码
#ifndef STUDENT_H
#define STUDENT_H

#define MAX_NAME      32    /* 名字最长 31 个字符(多留 1 格) */
#define MAX_CAPACITY  10    /* 系统最多装 10 个学生 */
#define SUBJECT_COUNT 3     /* 固定三门课 */

typedef struct {
    int id;                  /* 学号 */
    char name[MAX_NAME];     /* 姓名 */
    float scores[SUBJECT_COUNT];  /* 各科成绩 */
    float total;             /* 总分 */
    float average;           /* 平均分 */
} Student;

typedef struct {
    Student students[MAX_CAPACITY];  /* 一排学生(大小写死) */
    int count;               /* 当前有几个学生 */
} StudentSystem;

#endif

3 第二步:做一个最小系统

3.1 新增功能并跑通

不要把功能一口气全写完再编译,这样最后跑不通,我们也不知道哪错了。

正确的做法是每加一个功能,都要完整跑通,看运行效果符不符合预期。

我们在这一步里先加入 添加学生 + 查看学生 + 退出 这三个功能。

新建view/view.h,加入查看学生的view_show_all函数:

复制代码
#ifndef VIEW_H
#define VIEW_H

#include "../model/student.h"

void view_show_all(StudentSystem *sys);        

#endif

新建 view/view.c,补全具体实现:

c 复制代码
/* view/view.c ------ 管显示 */
#include <stdio.h>
#include "view.h"

/* 把一排学生打印成表格 */
void view_show_all(StudentSystem *sys)
{
    if (sys->count == 0) {
        printf("暂无学生,先用菜单 2 添加几个吧。\n");
        return;
    }

    printf("\n----------------------------------------------\n");
    printf("学号  姓名      语文  数学  英语  总分  平均分\n");
    printf("----------------------------------------------\n");

    for (int i = 0; i < sys->count; i++) {
        Student *s = &sys->students[i];
        printf("%-5d %-8s %6.1f %6.1f %6.1f %6.1f %6.1f\n",
               s->id, s->name, s->scores[0], s->scores[1],
               s->scores[2], s->total, s->average);
    }
    printf("----------------------------------------------\n");
}

发现没有,view 里用了 sys->count、sys->students[i]------它只读数据、不改数据。老老实实当"显示器"。

Student *s = &sys->students[i] 里的 * 和 ->,正是上一讲指针的实战用法:取一个学生的地址,顺着地址读他的字段。

往model/student.h里新增:

复制代码
#include <stdio.h>

/* 科目名,供 view 和 main 打印用 */
extern const char *SUBJECT_NAMES[SUBJECT_COUNT];

void calculate_student_total(Student *student);
int system_add_student(StudentSystem *sys, Student stu);

新建model/student.c,补全添加和计算的核心逻辑:

c 复制代码
/* model/student.c ------ 数据和规则 */
#include <stdio.h>
#include "student.h"

const char *SUBJECT_NAMES[SUBJECT_COUNT] = {"语文", "数学", "英语"};

/* 根据各科成绩,算出一个学生的总分和平均分 */
void calculate_student_total(Student *student)
{
    student->total = 0;
    for (int i = 0; i < SUBJECT_COUNT; i++) {
        student->total += student->scores[i];
    }
    student->average = student->total / SUBJECT_COUNT;
}

/* 把一个学生放进系统 */
int system_add_student(StudentSystem *sys, Student stu)
{
    /* 1. 满没满? */
    if (sys->count >= MAX_CAPACITY) {
        printf("系统容量已满(最多 %d 人)!\n", MAX_CAPACITY);
        return 0;
    }
    /* 2. 学号重不重? */
    for (int i = 0; i < sys->count; i++) {
        if (sys->students[i].id == stu.id) {
            printf("学号 %d 已存在!\n", stu.id);
            return 0;
        }
    }
    /* 3. 算好总分平均分,整体放进去 */
    calculate_student_total(&stu);
    sys->students[sys->count] = stu;  /* 结构体整体赋值 */
    sys->count++;
    return 1;
}

看第 3 步那行 sys->students[sys->count] = stu;------这叫结构体整体赋值,把 stu 这一整份"学生档案"(学号、姓名、三门成绩、总分、平均分)一次性搬进数组。

C 语言里数组之间不能直接 =,但结构体变量之间可以,这个特性帮了我们大忙。现在你不用纠结它的底层原理是什么样的,怎么复制字符串,直接整份搬就行。

然后是 main.c,先搭最简骨架:

c 复制代码
/* main.c ------ 指挥 */
#include <stdio.h>
#include "model/student.h"
#include "view/view.h"

void flow_add_student(StudentSystem *sys)
{
    Student stu;

    printf("请输入学号:");
    scanf("%d", &stu.id);
    printf("请输入姓名:");
    scanf("%s", stu.name);
    for (int i = 0; i < SUBJECT_COUNT; i++) {
        printf("请输入%s成绩:", SUBJECT_NAMES[i]);
        scanf("%f", &stu.scores[i]);
    }

    if (system_add_student(sys, stu)) {
        printf("添加成功!现在共 %d 名学生。\n", sys->count);
    }
}

int main(void)
{
    StudentSystem sys = {0};   /* 全部清零:10 个学生位都是空的,count = 0 */
    int choice;

    while (1) {
        printf("\n========== 学生成绩管理系统 ==========\n");
        printf("  1. 查看所有学生\n");
        printf("  2. 添加学生\n");
        printf("  7. 退出\n");
        printf("请选择操作(1-7):");
        scanf("%d", &choice);

        switch (choice) {
            case 1:
                view_show_all(&sys);
                break;
            case 2:
                flow_add_student(&sys);
                break;
            case 7:
                printf("感谢使用,再见!\n");
                return 0;
            default:
                printf("× 无效输入!\n");
        }
    }
}

StudentSystem sys = {0}; 这一行把整个系统清零:10 个学生位置全部是空(成绩全是 0.0),count 是 0。干干净净开场。

编译运行(在 code/4-3-学生成绩管理系统/ 目录下执行):

复制代码
gcc main.c model/student.c view/view.c util/input.c -o student_mgr.exe
./student_mgr.exe

菜单 1、2、7 已经能用了。现在让我们添加两个学生,查看一下。

一切正常!


4 第三步:加入异常处理

真的一切正常吗?下面我交给你一个"捣乱任务":故意往程序里输入一些奇奇怪怪的值,看看程序能不能做出正常反馈,我们管这个叫边界测试。

4.1 随便乱输会发生什么

方式一:学号输负数

复制代码
请输入学号:-5

这个只是显示问题,对用户体验影响不是那么大,现在看下边的。

方式二:姓名往死里输长长长长......

复制代码
请输入姓名:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

scanf("%s", stu.name) 不管你输多长,都会一个字符一个字符往 stu.name 里塞。name 只有 32 格,超长的名字把后面的格子全写爆了------这叫数组越界(也叫缓冲区溢出的雏形)。程序可能崩溃,可能输出乱码,行为完全不可预期。

把信息都输入后,按回车,程序直接崩溃!

方式三:菜单里输 "abc"

复制代码
请选择操作(1-7):abc

scanf("%d", &choice) 看到 "abc" 根本读不进去。它返回 0 表示"没读成",choice 还是旧值(或者垃圾值),而 "abc" 还留在输入缓冲区里。下次循环它又去读,又失败......结果就是菜单像发疯一样疯狂往下刷,程序卡死。

小练习:尝试在添加学生时给学号、成绩等信息输入字母,观察程序情况,询问AI为什么会这样。

4.2 目前代码的问题

三招全得手了。你发现没有,我们刚才写的代码,默认了"每次输入都是对的"。可现实中,用户什么都可能输:手滑、乱按、输错......你不可能要求每个人都像程序员一样小心。假如用户输入异常值,就会显示出来不正确的结果,甚至系统直接崩溃了,那他的体验是很糟糕的。

这个系统现在有两个致命的问题:

  1. 没有溢出检测 :字符串往数组里写之前,从不检查"装不装得下"。姓名 50 个字符照样往 32 格的 name 里写,把数组后面的数据全写坏。C 语言最臭名昭著的安全漏洞(缓冲区溢出),根源就是这种"写不检查"。
  2. 没有数字 / 字符检测:学号本该是正整数,成绩本该在 0~100,菜单本该是 1~7------可代码一个字都没查,负数、字母、500 分全都放行。

写软件的职业觉悟:永远不要相信用户输入。 只要数据是从外面进来的(键盘、文件、网络......),都要当成"可疑对象"检查一遍再放行。

4.3 改进思路

改进思路就一条:每个输入进来时做检查,不符合预期格式就让用户重输。

写在哪里呢?检验数据是否合规,这是"任何程序都可能用"的通用能力,塞在 main.c 里的话,它会越来越胖,以后别的文件也不方便调用,不合适。按 C/C++ 工程的惯例,这类通用代码应该独立成模块。让我们新建一个文件夹 util/(utility,工具),里边放通用工具:

复制代码
code/4-3-学生成绩管理系统/
├── model/          ← 数据和规则
├── view/           ← 显示
├── util/           ← 输入工具(今天新建)
└── main.c          ← 指挥

组织方式和 model 一样,声明和实现分开 :util/input.h 是"目录",util/input.c 是"书"。

input的中文意思是输入,这里取名为这个是检验输入的意思。

新建 util/input.h:

c 复制代码
#ifndef INPUT_H
#define INPUT_H

void flush_input(void);                  /* 清空输入缓冲区 */
int read_int(int min, int max);          /* 安全读整数:不是数字/超范围就重输 */
float read_float(float min, float max);  /* 安全读小数 */

#endif

新建 util/input.c,把安检门的具体实现放进去:

c 复制代码
#include <stdio.h>
#include "input.h"

/* 清空输入缓冲区:把残留的字符全部吃掉 */
void flush_input(void)
{
    int c;
    while ((c = getchar()) != '\n' && c != EOF) {
    }
}

/* 读一个整数:不是数字、超出范围,就一直要求重来 */
int read_int(int min, int max)
{
    int value;
    while (1) {
        if (scanf("%d", &value) != 1) {   /* 输入的不是数字 */
            printf("输入的不是数字,请重新输入(%d~%d):", min, max);
            flush_input();                 /* 把坏输入清掉,防止卡死 */
            continue;
        }
        flush_input();                     /* 吃掉行尾多余字符 */
        if (value < min || value > max) {
            printf("输入应在 %d~%d 之间,请重新输入:", min, max);
            continue;
        }
        return value;
    }
}

/* 读一个小数:作用和 read_int 一模一样 */
float read_float(float min, float max)
{
    float value;
    while (1) {
        if (scanf("%f", &value) != 1) {
            printf("输入的不是数字,请重新输入(%.0f~%.0f):", min, max);
            flush_input();
            continue;
        }
        flush_input();
        if (value < min || value > max) {
            printf("输入应在 %.0f~%.0f 之间,请重新输入:", min, max);
            continue;
        }
        return value;
    }
}

还记得 2-6 拆井字棋时说的吗------头文件是目录,源文件是书。 现在这套规矩第一次用在了"业务代码之外":工具模块也是这么组织的。正式工程里 util/、common/、utils 这类文件夹遍地都是。

拆开看 read_int 这个"安检门"做了三件事:

  • 第一道安检:scanf 的返回值是"成功读进了几个数"。如果 scanf("%d", &value) != 1,说明刚才输的不是数字------告诉用户重输,并且 flush_input() 把缓冲区里的垃圾清掉(不清的话,垃圾会一直在那里,每次 scanf 都失败,就死循环了)。
  • 第二道安检:确认读进来的数在 min~max 之间,超范围要求重来。
  • 三道安检全过,才把这个"干净"的数字放行。

flush_input 就干一件事:getchar() 一次次把缓冲区里的字符读走,直到碰到换行(\n)或文件结束(EOF),把这次输入剩下的"尾巴"全清干净。

有了安检门,主菜单和添加学生的代码改成这样(替换 main.c,记得加上 #include "util/input.h"):

注意姓名那行 scanf("%31s", ...),限制最多读 31 个字符,剩下的格子留给结尾的 \0,数组就再也写不爆了。

c 复制代码
/* main.c ------ 加了"安检门"的完整版 */
#include <stdio.h>
#include "model/student.h"
#include "view/view.h"
#include "util/input.h"

void flow_add_student(StudentSystem *sys)
{
    Student stu;

    printf("请输入学号(1~99999):");
    stu.id = read_int(1, 99999);
    printf("请输入姓名:");
    scanf("%31s", stu.name);     /* 最多读 31 个字符,剩下的格子留给 \0,写不爆 */
    flush_input();
    for (int i = 0; i < SUBJECT_COUNT; i++) {
        printf("请输入%s成绩(0~100):", SUBJECT_NAMES[i]);
        stu.scores[i] = read_float(0, 100);
    }

    if (system_add_student(sys, stu)) {
        printf("添加成功!现在共 %d 名学生。\n", sys->count);
    }
}

int main(void)
{
    StudentSystem sys = {0};   /* 全部清零:10 个学生位都是空的,count = 0 */
    int choice;

    while (1) {
        printf("\n========== 学生成绩管理系统 ==========\n");
        printf("  1. 查看所有学生\n");
        printf("  2. 添加学生\n");
        printf("  7. 退出\n");
        printf("请选择操作(1-7):");
        choice = read_int(1, 7);

        switch (choice) {
            case 1:
                view_show_all(&sys);
                break;
            case 2:
                flow_add_student(&sys);
                break;
            case 7:
                printf("感谢使用,再见!\n");
                return 0;
            default:
                printf("× 无效输入!\n");
        }
    }
}

编译:

复制代码
gcc main.c model/student.c view/view.c util/input.c -o student_mgr.exe
./student_mgr.exe

现在把刚才几个捣乱招数再试一遍。

全部没用了!

下一讲(4-4),我们把删除、修改、查询、统计补全,把这个骨架做成完整的学生成绩管理系统。


5 总结

  1. 数据先行:写代码前先把"学生长什么样、系统存哪"想清楚,后面所有功能都是对数据的操作。
  2. 每加入一个功能就跑一遍:不要一口气写完再编译;每加一个功能就跑一次,永远知道自己改坏没坏。
  3. 四个文件各管一摊:model 管数据和规则,view 管显示,util 管通用输入工具,main 管指挥。加功能先想"该动哪个文件"。
  4. 永远不要相信用户输入:数字限范围、字符串限量读取,坏输入打回去重输------这就是"输入安检"。

6 完整代码

model/student.h

c 复制代码
#ifndef STUDENT_H
#define STUDENT_H

#define MAX_NAME      32    /* 名字最长 31 个字符(多留 1 格) */
#define MAX_CAPACITY  10    /* 系统最多装 10 个学生 */
#define SUBJECT_COUNT 3     /* 固定三门课 */

typedef struct {
    int id;                  /* 学号 */
    char name[MAX_NAME];     /* 姓名 */
    float scores[SUBJECT_COUNT];  /* 各科成绩 */
    float total;             /* 总分 */
    float average;           /* 平均分 */
} Student;

typedef struct {
    Student students[MAX_CAPACITY];  /* 一排学生(大小写死) */
    int count;               /* 当前有几个学生 */
} StudentSystem;

#include <stdio.h>
/* 科目名,供 view 和 main 打印用 */
extern const char *SUBJECT_NAMES[SUBJECT_COUNT];
void calculate_student_total(Student *student);
int system_add_student(StudentSystem *sys, Student stu);

#endif

model/student.c

c 复制代码
#include <stdio.h>
#include "student.h"

const char *SUBJECT_NAMES[SUBJECT_COUNT] = {"语文", "数学", "英语"};

/* 按各科成绩算总分、平均分 */
void calculate_student_total(Student *student)
{
    student->total = 0;
    for (int i = 0; i < SUBJECT_COUNT; i++) {
        student->total += student->scores[i];
    }
    student->average = student->total / SUBJECT_COUNT;
}

/* 添加学生:满了/重号就失败 */
int system_add_student(StudentSystem *sys, Student stu)
{
    if (sys->count >= MAX_CAPACITY) {
        printf("系统容量已满(最多 %d 人)!\n", MAX_CAPACITY);
        return 0;
    }
    for (int i = 0; i < sys->count; i++) {
        if (sys->students[i].id == stu.id) {
            printf("学号 %d 已存在!\n", stu.id);
            return 0;
        }
    }
    calculate_student_total(&stu);
    sys->students[sys->count] = stu;   /* 结构体整体赋值 */
    sys->count++;
    return 1;
}

view/view.h

c 复制代码
#ifndef VIEW_H
#define VIEW_H

#include "../model/student.h"

void view_show_all(StudentSystem *sys);        /* 打印全部学生表格 */

#endif

view/view.c

c 复制代码
#include <stdio.h>
#include "view.h"

/* 打印全部学生 */
void view_show_all(StudentSystem *sys)
{
    if (sys->count == 0) {
        printf("暂无学生,先用菜单 2 添加几个吧。\n");
        return;
    }

    printf("\n----------------------------------------------\n");
    printf("学号  姓名      语文  数学  英语  总分  平均分\n");
    printf("----------------------------------------------\n");
    for (int i = 0; i < sys->count; i++) {
        Student *s = &sys->students[i];
        printf("%-5d %-8s %6.1f %6.1f %6.1f %6.1f %6.1f\n",
               s->id, s->name, s->scores[0], s->scores[1],
               s->scores[2], s->total, s->average);
    }
    printf("----------------------------------------------\n");
}

util/input.h

c 复制代码
#ifndef INPUT_H
#define INPUT_H

void flush_input(void);                  /* 清空输入缓冲区 */
int read_int(int min, int max);          /* 安全读整数:不是数字/超范围就重输 */
float read_float(float min, float max);  /* 安全读小数 */

#endif

util/input.c

c 复制代码
#include <stdio.h>
#include "input.h"

/* 清空输入缓冲区:把残留的字符全部吃掉 */
void flush_input(void)
{
    int c;
    while ((c = getchar()) != '\n' && c != EOF) {
    }
}

/* 读一个整数:不是数字、超出范围,就一直要求重来 */
int read_int(int min, int max)
{
    int value;
    while (1) {
        if (scanf("%d", &value) != 1) {   /* 输入的不是数字 */
            printf("输入的不是数字,请重新输入(%d~%d):", min, max);
            flush_input();                 /* 把坏输入清掉,防止卡死 */
            continue;
        }
        flush_input();                     /* 吃掉行尾多余字符 */
        if (value < min || value > max) {
            printf("输入应在 %d~%d 之间,请重新输入:", min, max);
            continue;
        }
        return value;
    }
}

/* 读一个小数:作用和 read_int 一模一样 */
float read_float(float min, float max)
{
    float value;
    while (1) {
        if (scanf("%f", &value) != 1) {
            printf("输入的不是数字,请重新输入(%.0f~%.0f):", min, max);
            flush_input();
            continue;
        }
        flush_input();
        if (value < min || value > max) {
            printf("输入应在 %.0f~%.0f 之间,请重新输入:", min, max);
            continue;
        }
        return value;
    }
}

main.c

c 复制代码
#include <stdio.h>
#include "model/student.h"
#include "view/view.h"
#include "util/input.h"

/* ------ 各菜单项的"流程",只负责问问题和指挥,干活交给 model / view ------ */

void flow_add_student(StudentSystem *sys)
{
    Student stu;

    printf("请输入学号(1~99999):");
    stu.id = read_int(1, 99999);
    printf("请输入姓名:");
    scanf("%31s", stu.name);     /* 最多读 31 个字符,剩下的格子留给 \0,写不爆 */
    flush_input();
    for (int i = 0; i < SUBJECT_COUNT; i++) {
        printf("请输入%s成绩(0~100):", SUBJECT_NAMES[i]);
        stu.scores[i] = read_float(0, 100);
    }

    if (system_add_student(sys, stu)) {
        printf("添加成功!现在共 %d 名学生。\n", sys->count);
    }
}

int main(void)
{
    StudentSystem sys = {0};
    int choice;

    while (1) {
        printf("\n========== 学生成绩管理系统 ==========\n");
        printf("  1. 查看所有学生\n");
        printf("  2. 添加学生\n");
        printf("  7. 退出\n");
        printf("请选择操作(1-7):");
        choice = read_int(1, 7);

        switch (choice) {
            case 1:
                view_show_all(&sys);
                break;
            case 2:
                flow_add_student(&sys);
                break;
            case 7:
                printf("感谢使用,再见!\n");
                return 0;
            default:
                printf("× 无效输入!\n");
        }
    }
}
相关推荐
悠仁さん1 小时前
非类型模板参数的特化与偏特化
java·数据结构·算法
芯片智造1 小时前
十二烷基硫酸钠等润湿剂如何抑制NiFe电镀针孔?
经验分享
外收内放1 小时前
Python基础语法练习题(72)
开发语言·python
无忧.芙桃1 小时前
C++语言原理与实践(十三):stack、queue 和 priority_queue的介绍与使用
开发语言·c++·栈·队列
波力海苔夹心脆6751 小时前
VisionMaster 常用工具入门:图像采集、定位、测量、识别一篇打通
经验分享·机器人·视觉检测·工业相机·visionmaster·海康
Shaoshing2 小时前
消息发送
java·开发语言
haluhalu.2 小时前
深入理解 AI Agent(1.2):Harness 工程——模型之外的竞争力
前端·数据结构·人工智能·python·langchain·个人开发
垆边人似月.2 小时前
幻兽防御战(贪心算法)
数据结构·算法