用C++与Qt重构通讯录:从课程设计到工程实践的架构进阶
2026/9/16 14:42:55 网站建设 项目流程

简介:基于C++与Qt开发的通讯录管理系统课程设计源码,面向计算机相关专业在校生、教师及初入Qt的开发者,适合课程设计结题、期末大作业冲刺,也可作为初学Qt的练手项目。压缩包共52个文件,核心代码包括11个cpp与10个h源文件、4个ui界面布局文件、qrc资源文件和pro工程文件,另附18个png图标素材、使用说明README、程序报告doc及界面设计细节PDF,整体仅3.27MB,目录结构清晰便于对照学习。目前已有199人学习下载。随包提供完整可运行的通讯录增删改查、分类管理(同学/朋友/亲属)及近期生日提醒等功能演示,代码模块涵盖地址簿核心逻辑、主界面交互、信息表单与列表展示等,报告文档详细说明大作业界面设计实现细节,适合在现有代码上二次扩展,如增加数据持久化或搜索增强,是快速上手Qt项目实践的不错参考。代码在提交前已多次运行验证,主要功能均能正常使用。

1. 拿 C++ 和 QT 重写通讯录,为什么不是在做课程设计而是在打基础

通讯录管理系统大概是计算机专业里被布置次数最多的课程设计题目,PB 时代用 VB 做,Java 时代用 Swing 做,到了现在 C++ 配上 QT,表面上只是换了一套 GUI 框架,实际上对能力的要求完全不同。这个项目最容易被低估的地方在于:它同时踩中了 C++ 的两个核心难点——内存管理和对象生命周期,又踩中了 QT 的信号槽机制、Model/View 架构、事件循环这些真正在生产环境里天天用的东西。换句话说,这个“课程设计”的完成质量,基本能反映一个人能不能上手真实 Qt 桌面项目。常见的做法是把数据放在 vector 或链表里,用 QTableWidget 直接展示,增删改查一把梭,再把窗口拖一拖就算完事。这样做能跑,但到了要扩展成真正的通讯录产品——比如数据量过万、需要模糊搜索、需要导出导入、需要给不同的人分配权限——再回过来改,代价远比重写还大。所以这篇文章会换一个思路,用 QStandardItemModel 做数据层与界面层的分离,用文件做持久化,把通讯录拆成“数据层、业务层、界面层”三层结构,再谈怎么调搜索、排序和崩溃恢复。

2. C++ 与 QT 的选型:内存模型、对象树和模型/视图框架

2.1 通讯录系统里真正涉及的内存问题

C++ 做通讯录,最直接的方案是写一个 Contact 类,里面放姓名、电话、邮箱、备注这些字段,然后用容器管理。问题在于很多初学者把重点放在了界面能不能弹出、按钮能不能点上,而忽略了内存管理——因为 Qt 的对象树机制在一定程度上掩盖了这个问题。一个 QPushButton new 出来之后,如果不指定父对象,就得手动 delete;如果指定了父对象,父对象析构时子对象会被自动回收。这个机制在通讯录这种“用户反复打开对话框、反复增删联系人”的场景下,最容易出现两种故障:一种是 new 了对话框但忘了 delete,每次打开都泄漏一点,跑一个下午内存涨上去几百兆;另一种是栈上创建了 QDialog 然后 exec() 进去,对话框都退出了对象还没释放,逻辑上容易写出悬垂引用。

常见做法是 QDialog 用栈对象配合 exec(),或者 new 出来之后设置 WA_DeleteOnClose 属性,让窗口关闭时自动销毁。具体到通讯录项目里,编辑联系人的对话框就适合用 WA_DeleteOnClose,而主窗口适合用对象树挂载。这里的一条核心原则是:谁负责创建,谁就要管好释放;拿不准的时候明确指定父对象,让它进入 Qt 的对象树而不是依赖运气。

2.2 通讯录项目为什么推荐 Model/View 而不是直接往 QTableWidget 里塞数据

很多课设代码里常见的是 QTableWidget 配合 setItem() 往表格里填数据。这种写法在小数据量下完全没问题,几十个联系人怎么操作都流畅。但 QTableWidget 是把数据存在表格的 item 里,数据和界面是绑死的,数据一变就得手动同步界面,搜索、排序、筛选全得自己写逻辑反复遍历表格。QTableView 配合 QStandardItemModel 则是数据和界面分离:QStandardItemModel 只负责存数据、维护行列关系,QTableView 只负责把模型的内容画出来。数据变了只要发一个 dataChanged 信号,视图自动刷新;要做筛选,用一个 QSortFilterProxyModel 插在模型和视图之间,整套逻辑天然支持。

通讯录这个体量的项目用 Model/View 看起来有点杀鸡用牛刀,但搜索、分组、排序这些都是通讯录的基本需求,用 QSortFilterProxyModel 做一套下来,比在 QTableWidget 里折腾 item 要干净得多。而且这套思路直接对应着 Qt 里做数据密集型应用的通用架构,学一次以后做表格、树、列表都能用。

2.3 一个能跑通的最小骨架:QStandardItemModel 展示联系人列表

先给出一个最小可运行的代码结构,把通讯录最核心的“展示联系人列表”这一件事跑通。下面的代码包含了主窗口的基本布局、模型初始化和数据填充。

#include <QApplication> #include <QMainWindow> #include <QTableView> #include <QStandardItemModel> #include <QHeaderView> class MainWindow : public QMainWindow { public: MainWindow(QWidget* parent = nullptr) : QMainWindow(parent) { QTableView* view = new QTableView(this); // 创建模型:4列:姓名、电话、邮箱、备注 model = new QStandardItemModel(0, 4, this); model->setHeaderData(0, Qt::Horizontal, "姓名"); model->setHeaderData(1, Qt::Horizontal, "电话"); model->setHeaderData(2, Qt::Horizontal, "邮箱"); model->setHeaderData(3, Qt::Horizontal, "备注"); // 添加一行测试数据 QList<QStandardItem*> rowItems; rowItems << new QStandardItem("张三"); rowItems << new QStandardItem("13800138000"); rowItems << new QStandardItem("zhangsan@example.com"); rowItems << new QStandardItem("同事"); model->appendRow(rowItems); // 设置表格属性 view->setModel(model); view->horizontalHeader()->setStretchLastSection(true); view->setSelectionBehavior(QAbstractItemView::SelectRows); view->setEditTriggers(QAbstractItemView::DoubleClicked | QAbstractItemView::EditKeyPressed); setCentralWidget(view); resize(800, 500); } private: QStandardItemModel* model; }; int main(int argc, char* argv[]) { QApplication app(argc, argv); MainWindow w; w.show(); return app.exec(); }

这段代码的核心逻辑分三块:QStandardItemModel(0, 4, this) 创建了一个初始 0 行 4 列的模型,第三个参数传 this 表示模型归主窗口管理,析构时自动释放;appendRow 把一组 QStandardItem 作为一整行追加到模型末尾,每个 item 对应一个单元格。QTableView 本身不存储数据,只是把模型里的行列内容渲染出来。

setSelectionBehavior 设成 SelectRows 表示点击任意单元格都选中整行,这在通讯录里比默认的选单元格要顺手得多。setEditTriggers 设成 DoubleClicked,意思是双击单元格才能编辑,避免误触改动数据。跑起来之后应该能看到一个带表头、有内容、能整行选中、能双击编辑的表格,这就是整个通讯录系统的界面底座。

3. 通讯录的核心数据结构与文件持久化方案

3.1 Contact 类怎么设计才不会被后续需求推翻

一个通讯录管理系统的数据字段到底要哪些?很多课设用四个字段就打发了。但稍加推敲就会发现不够用:通讯录里的联系人很可能有多个电话,家庭电话、工作电话、手机;可能有生日、公司、职务、地址;可能需要分组——家人、朋友、同事、黑名单。如果一开始就做一个固定的四字段结构,后面每加一个需求就要改类定义、改模型初始化、改文件读写、改界面表单,改动成本成倍增长。

合理做法是设计一个 Contact 结构体,里面用标准容器承载变长数据:

#include <QString> #include <QStringList> #include <QMap> struct Contact { int id = -1; QString name; QStringList phones; QStringList emails; QString company; QString group; QString note; bool operator==(const Contact& other) const { return id == other.id; // 以 id 为唯一标识 } };

把电话和邮箱设成 QStringList 而不是单个 QString,是给“一个人有多个联系方式”留的口子。用 int id 做唯一标识而不是用名字做键,是因为名字可能重复,而文件同步、编辑定位都需要一个稳定的标识。避免用 QMap<QString, Contact> 以姓名为键的做法——通讯录里重名的情况并不少见,一旦重名,后插入的数据会覆盖前面的。

3.2 文件格式选 JSON 还是自定义文本:读写与兼容的权衡

通讯录数据要持久化,最简单的方式是存文件。存文件的方式五花八门:可以按行写“姓名,电话,邮箱”,可以用 QSettings 存 ini,也可以用 QJsonDocument 存 JSON。对通讯录这个项目来说,JSON 是最合适的——原因有三个:第一,Qt 自带的 QJsonDocument 支持完整,不引入第三方库;第二,JSON 的嵌套结构能表达“一个联系人有多个电话”这种一对多关系;第三,文件内容可读,出了问题能用文本编辑器直接检查。

自定义格式虽然写起来简单,但解析时容易埋坑——比如电话字段里如果出现了分隔符,读出来就是错乱的。JSON 天然规避了这类问题。

#include <QJsonArray> #include <QJsonDocument> #include <QJsonObject> #include <QFile> bool saveContacts(const QString& filePath, const QList<Contact>& contacts) { QJsonArray rootArray; for (const Contact& c : contacts) { QJsonObject obj; obj["id"] = c.id; obj["name"] = c.name; obj["phones"] = QJsonArray::fromStringList(c.phones); obj["emails"] = QJsonArray::fromStringList(c.emails); obj["company"] = c.company; obj["group"] = c.group; obj["note"] = c.note; rootArray.append(obj); } QFile file(filePath); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { return false; } file.write(QJsonDocument(rootArray).toJson(QJsonDocument::Indented)); file.close(); return true; }

注意 QJsonArray::fromStringList 是专门把 QStringList 转成 QJsonArray 的快捷接口,省去了手动逐条 append。写文件时用 Indented 模式,生成的是带缩进、人类可读的 JSON,方便调试时直接打开看内容。读取是反向操作:整体读入 QJsonDocument,把根节点转成数组,遍历每个元素用 obj["name"].toString() 之类的接口取值。

3.3 文件读写里最容易翻车的三个细节

文件读写看似简单,实际开发中每个细节都可能变成线上问题。第一个坑是路径问题。如果直接写 relative.json,程序的工作目录不同,文件可能落在完全不同的地方。我一般会在程序启动时用 QCoreApplication::applicationDirPath() 拼接出可执行文件同目录下的 data/contacts.json——这样不管你从哪个目录启动程序,文件位置都是确定的。

第二是编码问题。Windows 下默认编码可能是 GBK,但 QT 6 默认源码编码是 UTF-8,直接调用 QString::fromLocal8Bit 在某些环境下会出现乱码。推荐的写法是一律以 UTF-8 作为文件编码,读写时用 QFile 搭配 QTextStream 显式指定 setEncoding(QStringConverter::Utf8)。打开一个之前写好的通讯录文件,如果姓名全是乱码,十有八九是编码不匹配。

第三是写文件的安全性问题。通讯录数据虽不值钱,但用户输了一下午的联系人因为程序崩溃被清空,体验是毁灭性的。稳妥的做法是先写临时文件,成功后再用 QFile::remove 加 QFile::rename 覆盖旧文件。这样即使写入中途断电,旧文件也还在。

3.4 把模型和 Contact 列表同步起来:增删改查的统一走法

模型存的是界面要展示的 item,Contact 列表存的是业务数据。增删改查时,两者必须保持同步。常见做法是动作发生时同时改模型和列表,但顺序不一样,结果可能完全不同。推荐顺序是“先改数据层,再刷新模型”——数据永远以 QList 为准,模型只是投影。

void addContact(Contact& c) { c.id = getNextId(); // 自动分配自增 id m_contacts.append(c); refreshModel(); } void updateContact(const Contact& c) { for (int i = 0; i < m_contacts.size(); ++i) { if (m_contacts[i].id == c.id) { m_contacts[i] = c; break; } } refreshModel(); } void removeContact(int id) { for (int i = 0; i < m_contacts.size(); ++i) { if (m_contacts[i].id == id) { m_contacts.removeAt(i); break; } } refreshModel(); } void refreshModel() { model->removeRows(0, model->rowCount()); for (const Contact& c : m_contacts) { QList<QStandardItem*> items; items << new QStandardItem(c.name); items << new QStandardItem(c.phones.isEmpty() ? "" : c.phones.first()); items << new QStandardItem(c.emails.isEmpty() ? "" : c.emails.first()); items << new QStandardItem(c.notes); model->appendRow(items); } }

refreshModel 的逻辑是“全量重建”:先把模型里所有行删掉,再按 m_contacts 列表重建所有行。数据量在 1 万以内时,这个操作的耗时基本是毫秒级,用户无感知。但如果你预期联系人会到十万级,就不能这么写了——那需要只刷新变化的那一行,用 model->item(row, col)->setText() 定点更新,这属于后面的优化项,不是课程设计阶段该优先考虑的。

4. 用 QSortFilterProxyModel 做搜索、筛选和排序

4.1 为什么不在 QStandardItemModel 里直接做查找

通讯录系统的核心使用场景是“找人”,不是“看列表”。一个用户加了 500 个联系人之后,往下翻是不现实的,搜索就是刚需。很多课设的做法是遍历 model 的行,拿 item 里的文本做字符串匹配,然后重建 model。这样能做到,但有一个明显的坏处:排序和筛选的逻辑和数据模型写在一起,代码会越来越乱。更好的做法是利用 Qt 自带的 QSortFilterProxyModel,在模型与视图之间插入一个有“过滤能力”的模型代理。

#include <QSortFilterProxyModel> QSortFilterProxyModel* proxyModel = new QSortFilterProxyModel(this); proxyModel->setSourceModel(model); proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); proxyModel->setFilterKeyColumn(-1); // 搜索所有列 view->setModel(proxyModel); // 视图挂代理模型,而不是原模型

setFilterKeyColumn(-1) 表示过滤时搜索所有列的文本,而不是只搜某一列——在通讯录场景里,用户输入“张”可能匹配姓名里的“张”,也可能匹配公司里的“张”,全列搜索体验更好。setFilterCaseSensitivity 设成大小写不敏感,避免用户输小写字母时搜不到大写开头的英文名。核心方法就一个:调用 proxyModel->setFilterFixedString(text) 或 setFilterRegularExpression(regExp),界面自动过滤,什么都不用手动刷新。

4.2 动态搜索:QLineEdit 的 textChanged 信号怎么和代理模型对接

QLineEdit 每输入一个字符都会发 textChanged 信号,把这个信号直接连到代理模型的过滤方法上,搜索就是实时的。

connect(searchEdit, &QLineEdit::textChanged, this, [=](const QString& text) { if (text.isEmpty()) { proxyModel->setFilterRegularExpression(QRegularExpression()); } else { proxyModel->setFilterRegularExpression(QRegularExpression::escape(text)); } });

这里有一个关键细节是 QRegularExpression::escape——用户搜索“C++”时,如果直接把文本传给 setFilterRegularExpression,“+”会被当成正则量词,匹配结果完全不对。正确的做法是对用户输入做转义,把用户输入当作字面量来匹配。这是通讯录搜索里最常见的隐性 bug,也是面试时值得讲出来的细节。

4.3 按字母分组和常用字段排序:代理模型的两个进阶用法

如果通讯录的联系人超过几百人,按姓名首字母分组是一个非常实用的功能。QSortFilterProxyModel 本身不做分组,但可以配合 QTableView 的 setRowHidden 来做,或者换用 QTreeView 加 QStandardItemModel 做树形分组。对课设而言,更简单可靠的是在代理模型上重写 lessThan 实现点击表头排序:

class ContactProxyModel : public QSortFilterProxyModel { protected: bool lessThan(const QModelIndex& left, const QModelIndex& right) const override { QString leftData = sourceModel()->data(left).toString(); QString rightData = sourceModel()->data(right).toString(); return QString::localeAwareCompare(leftData, rightData) < 0; } };

重写 lessThan 时用 localeAwareCompare 而不是按字节比较,是为了让中文排序按拼音进行,而不是按 Unicode 码点排序。QTableView 默认就能点击表头排序,把 setSortingEnabled(true) 打开即可。这一套组合起来,通讯录的“搜索—排序”体验就完整了。

5. 界面交互细节:从课设代码到可用软件的差距

5.1 弹窗编辑联系人时的窗口生命周期管理

编辑联系人的对话框是通讯录系统中最常打开的窗口。这里有一个常见的错误用法:每次点“添加”都 new 一个 QDialog,但从不 delete。虽然 Qt 的对象树可能在父窗口销毁时统一回收,但如果 parent 传的是 nullptr,这个窗口就会泄漏到程序退出。推荐做法有两种:一是用 QDialog::exec() 配合栈对象,二是 new 出来加 WA_DeleteOnClose。

void MainWindow::onAddContact() { Contact c; ContactEditDialog dlg(this); dlg.setContact(c); if (dlg.exec() == QDialog::Accepted) { addContact(dlg.contact()); } }

把对话框创建在栈上,exec() 会开启一个嵌套事件循环,直到对话框关闭才返回。if 判断返回值是 Accepted 还是 Rejected,决定是保存数据还是丢弃。整个过程不会泄漏。如果用户加了 100 个联系人,打开关闭了 100 次编辑框,内存不会有任何增长。

5.2 空状态提示:没有联系人时界面不能是一张白表

一个在课设里很少做、但真实产品里一定会有的细节:当通讯录为空时,界面应该提示用户“没有联系人,点击添加”而不是显示一个空白表格。实现方式是在 QTableView 上覆盖一层 QLabel,当行数为 0 时让它可见,有数据时隐藏,代码很简单但能显著改善观感。同样值得做的细节还有:删除联系人前弹确认框、编辑未保存关闭时提示等。这些不是算法难题,但决定了用户愿不愿意真正使用这个系统。

5.3 中文乱码和路径兼容:QT 课程设计里最常被忽略的问题

Qt 在 Windows 下写中文,如果源码文件不是 UTF-8 编码,或者 MSVC 编译时没指定 /utf-8 参数,中文字符串字面量在运行时会变成乱码。Windows 用 MSVC 或 MinGW,Qt Creator 里一般默认已经是 UTF-8,但如果拿 Visual Studio 配 Qt 插件,需要手动加上 /utf-8 编译选项。判断方法很简单:如果界面上的姓名、按钮文字全是乱码,第一反应就查编译参数和源码编码。

文件路径方面,如果要把通讯录作为绿色软件拷贝到别的机器上,直接写相对路径可能读不到数据文件。用 QStandardPaths 或者至少 QCoreApplication::applicationDirPath() 拼路径,能让程序的健壮性上一个台阶。

6. 发布与进一步扩展:打包、崩溃恢复和后续优化方向

6.1 用 windeployqt 打出一个免安装包

课程设计要提交可运行的程序,就不可能让老师在自己的电脑上装一套 Qt 环境。Qt 的发布方案是工具链自带的 windeployqt(Windows)和 macdeployqt(macOS),它会自动把程序依赖的 Qt 动态库、插件、翻译文件全部拷贝到目标目录。

在 Qt 命令行环境里执行:

cd build/Release windeployqt contacts.exe

这条命令能扫描 exe 的导入表,把需要的 Qt5Core.dll、Qt5Gui.dll、Qt5Widgets.dll 以及 platform 插件(如 qwindows.dll)一并拷过来。常见的坑是只拷贝了 exe 和几个看着眼熟的 DLL,结果在别的机器上双击没反应——大部分情况是缺了 platforms\qwindows.dll,这个目录必须保持与 exe 的相对位置。打完包后最好在一台干净的机器上测试,确认能跑、数据文件能读写、中文显示正常。

6.2 崩溃恢复:临时文件加主文件的两次写入策略

桌面软件的崩溃恢复不是通讯录的必需功能,但如果要做得比别人更完整,最常见的做法是“双文件策略”:数据写入时先写 contacts.json.tmp,写完并校验后再覆盖 contacts.json。程序启动时检查临时文件是否比主文件新,如果是,说明上次写入可能不完整,可以做一次恢复。实现成本不高,但对数据安全的意义很大——课设答辩时的“程序在老师电脑上闪退但数据没丢”,比任何口头描述都更能说明工程意识。

6.3 这个系统还能往哪个方向扩展

做完通讯录基本的增删改查、搜索、排序、文件持久化之后,自然的扩展方向有三个:第一个是导入导出功能,支持 CSV 或 vCard 格式,把联系人和安卓手机通讯录打通;第二个是 Qt 国际化,用 QTranslator 加载中文和英文的 .qm 文件,做一个语言切换菜单,这也是 Qt 框架里非常成熟的一块;第三个是数据库迁移,从 JSON 文件换成 SQLite(用 Qt SQL 模块),数据量上来之后的查询效率完全是另一档。这三个方向里随便挑一个深入下去,都足够撑起更复杂项目的技术储备——这一点,比课设本身的成绩重要得多。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询