直接继承QAbstractItemModel容易崩溃,因视图密集调用重写接口时,若index()、parent()、rowCount()等返回值逻辑不自洽(如父子索引循环引用、空指针未判、行数跳变),会导致读越界或野指针解引用而crash。

为什么直接继承 QAbstractItemModel 容易崩溃
因为 Qt 的视图(如 QTreeView、QTableView)在初始化、刷新、展开节点时会密集调用你重写的接口,而一旦 index()、parent()、rowCount()、columnCount() 返回值逻辑不自洽——比如父子索引循环引用、空指针未判、行数突然跳变——视图就会读越界或解引用野指针,直接 crash 或卡死。
关键不是“写全虚函数”,而是保证索引体系的拓扑一致性。最常踩的坑是:parent() 返回的 index 与 index(row, col, parent) 反向构造出的 index 不等价;或者 rowCount(parent) 在 parent.isValid() == false 时没处理根层级。
-
index()必须用唯一、稳定的方式把数据位置映射为QModelIndex(通常用QPersistentModelIndex存储内部指针或 ID,但不能存裸指针) -
parent()必须能从子 index 精确还原出其父 index;若子是顶层项,必须返回QModelIndex() - 所有
rowCount()/columnCount()调用都应有明确的数据源依据,避免依赖外部状态(如临时过滤变量),否则信号通知会失效
如何安全封装底层数据并生成 QModelIndex
别用裸指针绑定 QModelIndex,Qt 不管理其生命周期。推荐两种方式:
- 用
quintptr存储对象 ID 或数组下标(适用于扁平列表或树节点带唯一 ID):QModelIndex index(int row, int column, const QModelIndex &parent) const override { if (!hasIndex(row, column, parent)) return QModelIndex(); MyTreeNode *node = nodeFromParent(parent); MyTreeNode *child = node->child(row); if (!child) return QModelIndex(); // 将 child 地址转为整数(仅限调试/单线程安全场景) // 更稳妥:用 map<void int> 维护 ID 映射,或改用自增 serialId return createIndex(row, column, reinterpret_cast<quintptr>(child)); }</quintptr></void> - 用
QVariant携带轻量标识(如QVariant::fromValue),再在(nodeId) parent()中查表还原。适合需要跨线程或长期缓存 index 的场景
注意:createIndex() 第三个参数是用户数据,Qt 不解释它,但要求你后续能用它无歧义还原出对应数据项——这是整个模型可工作的基础。
必须重写的最小接口集及顺序验证法
只实现 data() 和 rowCount() 是不够的;视图第一次 layout 时就会调 index(0,0,QModelIndex()) → parent() → rowCount() 循环。按以下顺序验证可快速定位断裂点:
- 先让
rowCount(QModelIndex())返回一个非零常数(比如 3),确认视图能显示三行 - 再实现
index(0,0,QModelIndex())并确保它返回有效 index(isValid() == true) - 接着实现
parent(index),对顶层 index 必须返回QModelIndex();对子 index 必须能反推回父 index - 最后补
data()和columnCount()。列数至少为 1,否则视图可能不触发绘制
每加一个函数,用 qDebug() 打印关键路径(如 “in index(2,0,parent=…) → returning idx with ptr=0x…”),比断点更直观。Qt Creator 的 QAbstractItemModelTester 类也能自动跑一致性检查,但需手动集成到测试 main 中。
插入/删除行时 notify 信号怎么发才不崩
核心原则:beginXxx / endXxx 必须严格配对,且中间不能调用任何会触发视图重绘的函数(包括 data()、index())。常见错误是:在 beginInsertRows() 后立刻修改底层容器,导致 rowCount() 返回值与视图预期不符。
- 正确顺序:先修改数据结构 → 再调
beginInsertRows()→ 立即调endInsertRows()(中间不做任何事) - 删除同理:先调
beginRemoveRows()→ 立即调endRemoveRows()→ 再删数据(或反过来,但必须保证 begin/end 包裹期间rowCount()返回值稳定) - 如果要批量操作,用
beginResetModel()/endResetModel(),但代价是整个视图重刷——适合结构剧烈变动(如切换数据源)
漏发或错发信号不会立刻报错,但会导致视图状态错乱:滚动条范围不对、选中项残留、右键菜单位置偏移。这类问题往往延迟暴露,调试成本远高于一开始就写对 notify 流程。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











