资讯动态

C++学生信息管理系统实战:SQLite数据库增删改查完整项目

发布时间:2026/10/24 4:36:14 来源:尧图企业网站定制
简介这份资源面向计算机专业本科生、课程设计或毕业设计阶段的开发者提供一套基于C与数据库技术实现的学生信息管理系统完整源码可用于学籍管理、成绩录入与查询等场景的实践参考。压缩包共146个文件以63个.h头文件与62个.cpp源文件为主体配合ico图标、rc资源脚本及mdb数据库文件另有sln、vcproj等工程配置整体约3.76MB可直接在Visual C环境中打开编译。系统涵盖用户权限管理、学生信息增删改查、成绩统计与密码加密等模块代码按登录注册、信息维护、成绩管理等职责分层组织便于读者理解C面向对象设计与关系型数据库的协作方式。目前已有1415人学习下载适合希望对照完整工程结构、梳理模块划分与数据库交互思路的学习者参考借鉴。1. 一个能跑通增删改查的 C 学生信息管理系统到底长什么样很多人学完 C 语法后卡在同一个地方课本上的例子都会但真让自己写一个带数据库、能持久化、有菜单交互的完整程序就不知道从哪下手。学生信息管理系统恰好是补上这一环的经典练手项目——它不花哨但把 C 面向对象、STL 容器、文件与数据库操作、控制台交互这几块串成了一条完整链路。这份资源给的就是一套能直接编译运行的 C 学生信息管理系统带数据库持久化适合刚学完 C 想找个完整项目练手的人也适合需要拿它当课程设计底稿、再往上加功能的开发者。下面我按「它是什么、怎么跑起来、核心代码怎么写的、怎么改」的顺序拆一遍。2. 技术选型与工程结构为什么用 C 配数据库而不是纯文件2.1 这套系统用了哪些技术从项目定位看这是一个控制台交互的学生信息管理系统核心能力围绕学生记录的增、删、改、查、列表展示展开数据落在数据库里而不是内存里程序退出再进来数据还在。技术栈上语言是 C数据库层常见做法是 SQLite——单文件、零配置、不需要单独装服务编译时链接一个库就能用特别适合这种单机小系统。也有版本会用 MySQL但那需要额外装服务、配账号对练手项目来说门槛偏高。我一般会优先选 SQLite原因很直接拷一个.db文件走换台机器照样跑没有环境依赖的玄学问题。界面层是标准控制台菜单用std::cout输出选项、std::cin读输入配合switch分发到不同功能函数。数据在内存里用std::vectorStudent或std::listStudent承载查询时遍历容器落库时批量写回。这套组合没有框架、没有第三方 UI纯标准库加一个数据库驱动编译命令短新手不容易在环境上翻车。2.2 目录结构长什么样一个结构清晰的版本通常是这样组织的student-manager/ ├── include/ │ ├── Student.h # 学生实体类声明 │ ├── Database.h # 数据库操作封装 │ └── Manager.h # 业务逻辑增删改查调度 ├── src/ │ ├── Student.cpp # 实体类实现 │ ├── Database.cpp # SQLite 连接、建表、CRUD │ ├── Manager.cpp # 菜单与业务分发 │ └── main.cpp # 程序入口 ├── data/ │ └── students.db # 运行时生成的数据库文件 ├── CMakeLists.txt # 构建脚本 └── README.mdinclude放头文件、src放实现是 C 项目最常规的分法好处是编译单元清晰改一个模块不用动其他文件。data目录专门存数据库文件程序启动时检查文件是否存在不存在就建表。CMakeLists.txt负责把源码和 SQLite 库链起来比手写一长串g命令可靠得多。2.3 分层设计实体、数据访问、业务各管一摊这套系统虽然小但分层是清楚的。Student是纯数据实体只负责装字段Database封装所有 SQL 语句和连接生命周期对外暴露insert、remove、update、queryAll这类方法Manager负责菜单循环和把用户输入翻译成对Database的调用。这样分的好处是将来把 SQLite 换成 MySQL只动Database.cpp业务层一行不用改。新手常犯的错是把 SQL 直接写在菜单的switch分支里几十行之后就没法维护了这个项目的分层正好是反面教材的解药。提示如果你的版本没有include/src分目录所有.cpp堆在一起也能编译但建议按上面的结构整理一遍后面加功能时你会感谢自己。3. 从零跑起来编译、建库、第一次增删改查3.1 环境准备与依赖安装跑这个项目需要三样东西一个支持 C11 及以上的编译器g 或 clang 都行、CMake3.10 以上、SQLite3 开发库。Linux 下装依赖# Ubuntu/Debian 环境 sudo apt update sudo apt install build-essential cmake libsqlite3-devbuild-essential带 g 和 makelibsqlite3-dev提供sqlite3.h头文件和链接用的库。Windows 下用 MinGW 或 MSVCSQLite 需要手动下载源码编译成静态库或者用 vcpkg 一条命令装好。macOS 自带 SQLite装个 CMake 就能开工。这一步最容易翻车的地方是头文件找不到——编译报sqlite3.h: No such file or directory八成是开发库没装只装了命令行工具。3.2 CMake 构建与编译项目的CMakeLists.txt核心内容大致如下cmake_minimum_required(VERSION 3.10) project(StudentManager CXX) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 收集所有源文件 file(GLOB SOURCES src/*.cpp) add_executable(student_manager ${SOURCES}) # 链接 SQLite3 find_package(SQLite3 REQUIRED) target_include_directories(student_manager PRIVATE include) target_link_libraries(student_manager PRIVATE SQLite::SQLite3)file(GLOB SOURCES src/*.cpp)自动收集源文件新增.cpp不用改构建脚本find_package(SQLite3 REQUIRED)让 CMake 自己找库找不到会直接报错而不是链接阶段才炸target_include_directories把include加进搜索路径源码里就能写#include Student.h而不用带相对路径。构建命令mkdir build cd build cmake .. make ./student_manager如果find_package报找不到 SQLite3说明系统里没有对应的 CMake 查找模块可以退化成手动指定target_link_libraries(student_manager PRIVATE sqlite3)前提是库在默认搜索路径里。3.3 首次运行与数据库初始化程序启动后第一件事是打开数据库、确保表存在。典型初始化逻辑// Database.cpp 中的初始化片段 bool Database::init(const std::string dbPath) { int rc sqlite3_open(dbPath.c_str(), db_); if (rc ! SQLITE_OK) { std::cerr 打开数据库失败: sqlite3_errmsg(db_) std::endl; return false; } const char* sql CREATE TABLE IF NOT EXISTS students ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, age INTEGER, gender TEXT, major TEXT, score REAL );; char* errMsg nullptr; rc sqlite3_exec(db_, sql, nullptr, nullptr, errMsg); if (rc ! SQLITE_OK) { std::cerr 建表失败: errMsg std::endl; sqlite3_free(errMsg); return false; } return true; }sqlite3_open在文件不存在时会自动创建所以第一次运行不需要手动建库CREATE TABLE IF NOT EXISTS保证重复启动不会报「表已存在」id用AUTOINCREMENT自增主键插入时不用自己管编号。sqlite3_exec的最后一个参数接收错误信息用完必须sqlite3_free否则每次失败都漏一小块内存——这种细节在练手项目里最容易被忽略。跑通之后菜单大概长这样 学生信息管理系统 1. 添加学生 2. 删除学生 3. 修改学生 4. 查询学生 5. 显示全部 0. 退出 请选择:选 1 输入姓名、年龄、性别、专业、成绩回车后数据落库选 5 能看到刚加的记录。到这一步项目就算真正跑起来了。4. 核心模块拆解一条学生记录从输入到落库的完整链路4.1 实体类 Student字段设计与构造实体类是整个系统的数据契约字段设计直接决定后面 SQL 和界面怎么写// include/Student.h #pragma once #include string class Student { public: Student() default; Student(const std::string name, int age, const std::string gender, const std::string major, double score) : name_(name), age_(age), gender_(gender), major_(major), score_(score) {} // getter / setter 省略实际项目里按需补 int getId() const { return id_; } void setId(int id) { id_ id; } std::string getName() const { return name_; } double getScore() const { return score_; } private: int id_ 0; // 数据库自增主键插入前为 0 std::string name_; int age_ 0; std::string gender_; std::string major_; double score_ 0.0; };id_默认 0 表示「尚未入库」插入成功后由数据库回填真实 id这个约定让「新增」和「更新」能共用同一个对象。成绩用double而不是int是因为很多场景要保留小数如果你们学校成绩是整数制改成int更省事。字段全部私有、通过 getter/setter 访问是 C 封装的基本功虽然练手项目里有人图省事直接 public但一旦要在赋值时做校验比如年龄不能为负私有字段的优势就出来了。4.2 数据访问层参数化 SQL 防注入插入操作是最能体现「会不会写」的地方。拼字符串的写法// 不推荐的写法字符串拼接 std::string sql INSERT INTO students(name,age,gender,major,score) VALUES( s.getName() , std::to_string(s.getAge()) ,...);名字里带个单引号SQL 直接语法错误更糟的是被人输入恶意内容就是注入漏洞。正确做法是用sqlite3_prepare_v2加绑定参数// Database.cpp参数化插入 bool Database::insert(const Student s) { const char* sql INSERT INTO students(name,age,gender,major,score) VALUES(?,?,?,?,?);; sqlite3_stmt* stmt nullptr; if (sqlite3_prepare_v2(db_, sql, -1, stmt, nullptr) ! SQLITE_OK) { return false; } sqlite3_bind_text(stmt, 1, s.getName().c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_int (stmt, 2, s.getAge()); sqlite3_bind_text(stmt, 3, s.getGender().c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 4, s.getMajor().c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_double(stmt, 5, s.getScore()); bool ok (sqlite3_step(stmt) SQLITE_DONE); sqlite3_finalize(stmt); // 必须释放否则语句对象泄漏 return ok; }?是占位符sqlite3_bind_*按序号从 1 开始绑定类型自动处理单引号、特殊字符都不再是问题。SQLITE_TRANSIENT告诉 SQLite「这份字符串你拷贝一份」因为传入的c_str()在函数返回后就失效了不拷贝会读到野指针——这是新手用 SQLite 最常见的翻车点之一。sqlite3_finalize释放语句对象漏掉它程序跑久了内存会涨。4.3 查询与结果集遍历查询全部记录并转成vectorStudentstd::vectorStudent Database::queryAll() { std::vectorStudent result; const char* sql SELECT id,name,age,gender,major,score FROM students;; sqlite3_stmt* stmt nullptr; if (sqlite3_prepare_v2(db_, sql, -1, stmt, nullptr) ! SQLITE_OK) { return result; } while (sqlite3_step(stmt) SQLITE_ROW) { Student s; s.setId(sqlite3_column_int(stmt, 0)); s.setName(reinterpret_castconst char*(sqlite3_column_text(stmt, 1))); s.setAge(sqlite3_column_int(stmt, 2)); s.setGender(reinterpret_castconst char*(sqlite3_column_text(stmt, 3))); s.setMajor(reinterpret_castconst char*(sqlite3_column_text(stmt, 4))); s.setScore(sqlite3_column_double(stmt, 5)); result.push_back(s); } sqlite3_finalize(stmt); return result; }sqlite3_step返回SQLITE_ROW表示还有一行返回SQLITE_DONE表示遍历结束这个循环模式是 SQLite C API 的标准用法。sqlite3_column_text返回const unsigned char*要reinterpret_cast成const char*才能构造std::string。列序号从 0 开始和SELECT里的字段顺序一一对应改查询语句时记得同步改序号否则字段会错位——这种 bug 不报错只是数据悄悄串了排查起来很费劲。4.4 菜单循环与输入校验业务层把用户选择分发到具体操作void Manager::run() { int choice -1; while (choice ! 0) { printMenu(); std::cin choice; if (std::cin.fail()) { // 输入非数字时清理状态 std::cin.clear(); std::cin.ignore(std::numeric_limitsstd::streamsize::max(), \n); continue; } switch (choice) { case 1: addStudent(); break; case 2: removeStudent(); break; case 3: updateStudent(); break; case 4: queryStudent(); break; case 5: listAll(); break; case 0: std::cout 再见\n; break; default: std::cout 无效选项\n; } } }std::cin.fail()那段是控制台程序的保命代码用户输入字母时cin进入失败状态不clear加ignore就会死循环疯狂打印菜单。这个坑几乎每个写控制台菜单的人都踩过血泪经验是——只要用cin 读数字后面就跟着这段清理逻辑。5. 避坑与排查编译、链接、运行时的五个高频问题5.1 编译报 sqlite3.h 找不到现象fatal error: sqlite3.h: No such file or directory。原因只装了 SQLite 命令行工具没装开发头文件。解决Linux 装libsqlite3-devmacOS 确认 Xcode 命令行工具已装Windows 用 vcpkg 装sqlite3并在 CMake 里find_package。装完用find /usr -name sqlite3.h确认路径再检查 CMake 的 include 目录有没有指对。5.2 链接阶段报 undefined reference现象编译通过链接时报一堆undefined reference to sqlite3_open。原因头文件找到了但库没链上。解决确认target_link_libraries里有SQLite::SQLite3或sqlite3如果库不在默认路径用link_directories或target_link_directories指定。顺序也有讲究库要放在源文件之后CMake 的 target 写法会自动处理手写 g 命令时容易搞反。5.3 中文姓名存进去变乱码现象输入中文姓名查出来是问号或乱码。原因源文件编码、控制台编码、数据库编码三者不一致。解决源文件统一存 UTF-8Windows 控制台执行chcp 65001切到 UTF-8SQLite 本身按字节存储只要写入和读出用同一编码就不会乱。跨平台项目里我一般会在main开头设一次 locale避免各平台默认编码打架。5.4 程序退出后数据没了现象加了几条记录重启程序全空。原因要么没调sqlite3_close导致事务没提交要么每次启动都DROP TABLE重建。解决检查初始化 SQL 是不是误写成DROP TABLE IF EXISTS确认退出路径上调用了sqlite3_close(db_)。SQLite 默认自动提交单条INSERT执行完就落盘但如果开了显式事务忘了COMMIT数据就会丢。5.5 输入字母后菜单疯狂刷屏现象输入非数字菜单无限循环打印。原因cin失败后状态位没清流里残留字符一直被读。解决就是 4.4 节那段clear加ignore。另外读字符串用getline时前面cin 留下的换行符会被getline立刻读到空串记得先cin.ignore()一次。6. 二次开发从能跑到好用我一般改这四处拿到能跑的基础版之后真正有价值的是往上加东西。第一处是查询能力基础版通常只有「按 id 查」实际用起来肯定要按姓名模糊查、按专业筛选、按成绩排序。做法是在Database里加一个带条件的查询方法SQL 用WHERE name LIKE ?配合绑定%关键字%排序用ORDER BY score DESC业务层加一个子菜单让用户选查询维度。第二处是数据校验年龄范围、成绩 0 到 100、姓名非空这些校验放在Manager的输入环节做别等到写库才报错用户体验差一大截。第三处是统计功能这是学生管理系统最容易被要求加的需求平均分、最高分、各专业人数分布。用一条 SQL 就能出结果// 按专业统计人数与平均分 const char* sql SELECT major, COUNT(*), AVG(score) FROM students GROUP BY major;;遍历结果集打印成表格比在内存里手动累加可靠得多数据库的聚合函数就是干这个的。第四处是导出把queryAll的结果写成 CSV几行std::ofstream就够方便交给别人用表格软件打开。改的时候有个习惯我一直保持每加一个功能先在Database层写方法并单独测通再接菜单别一上来就改界面否则出了问题分不清是 SQL 错还是菜单逻辑错。还有一个容易被忽略的改造点把Database的构造和析构做成 RAII构造时sqlite3_open、析构时sqlite3_close中间任何异常路径都不会漏关连接。基础版里常见的是手动 open/close一旦某个分支提前 return连接就悬着。改成 RAII 之后代码里再也不用惦记关库这件事。从那以后我每次接这类带数据库的小项目都强制先把连接封装成对象生命周期管理再写业务——省下来的排查时间远比多写的几行构造析构值。希望这些拆解能帮你把这个项目真正跑起来、改出自己的版本。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑