一、问题描述

最近在写程序时产生了一个 BUG,具体的情况描述如下:

  • QT 版本: 5.15.2
  • C++ 版本: MSVC 2005 64bit
  • OS 版本: Windows10 22H2

软件运行后的截图如下(图片已注释相关内容):
图1.1 软件运行截图

具体问题表现为:
当软件初次运行后,如果控件 IReadListView 中没有任何数据(也从未加载过数据),此时点击“删除选中”按钮(IDelOneTaskBtn),程序就会突然卡死并出现崩溃。但是,当软件的 IReadListView 中加载过一次数据后(就是添加过内容并全部删除),再次点击“删除选中”按钮却并不会出现程序崩溃及其它任何异常问题。

具体发生奔溃的代码如下展示(DEBUG 定位问题出现在第 7 行代码):

发生崩溃的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void MainWindow::on_IDelOneTaskBtn_clicked()// 发生崩溃
{
// 获取当前选中的行
QItemSelectionModel *SelectionModel = ui->IReadListView->selectionModel();

// 检查是否有选中的行
if (!SelectionModel->hasSelection())
return;

// 获取当前选中的行
QModelIndexList SelectedIndexes = SelectionModel->selectedRows();

// 按照倒序的方式逐行删除选中的数据
// 一定要倒序删除,正序删除会在选中很多行数据的时候出现无法全部删除(被选中的行数据)的情况!
for (int i = SelectedIndexes.count() - 1; i >= 0; --i)
{
IReadListViewDataModel->removeRow(SelectedIndexes.at(i).row());
}
}

QT 给出的问题描述情况如下列举:

  1. 在 DEBUG 调试模式下 QT 抛出异常警告:error: Debugger encountered an exception: Exception at 0x7ffe1254d73a, code: 0xc0000005: read access violation at: 0x8, flags=0x0 (first chance)

图1.2 QT 异常报告

  1. 在 DEBUG 调试模式下 QT 会将程序阻塞在下图所示位置:

图1.3 DEBUG 阻塞位置

  1. 在运行模式下 QT 会给出如下运行输出:

图1.4 应用程序输出信息

二、问题解决

经过 DEBUG 调试,得到的信息如下:

运行到第 4 行代码时:
图2.1 运行至第 4 行

运行到第 7 行代码时:
图2.2 运行至第 7 行

如果再接着运行就会出现程序崩溃。

可以很明显的看出来,在第七行代码处 SelectionModel 变量指向的地址为 0x0 为空值。也就是说,变量 SelectionModel 并没有接收到具体的值,也就是一个空指针。所以,可以认定此次问题是由于空指针的访问问题引起的。

那么,解决该问题的方式也很简单。我们只需要在使用变量 SelectionModel 之前对其进行检查,如果其为空指针,那么就应该直接阻断函数运行(也可以抛出异常配合 try...catch... 解决)。

下面是修正代码:

修正代码(空指针拦截)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void MainWindow::on_IDelOneTaskBtn_clicked()
{
// 获取当前选中的行
QItemSelectionModel *SelectionModel = ui->IReadListView->selectionModel();

// 检查是否为空
// 当数据为空时,TableView->selectionModel() 会返回一个空指针,进而导致程序的崩溃(没有该检查的话)!
if (!SelectionModel)
return;

// 检查是否有选中的行
if (!SelectionModel->hasSelection())
return;

// ...
}

三、问题研究

到此为止,都还顺利(因为这个问题本身并不难),之所以要记录这次解决的过程,原因有二:

  1. 这个问题虽然简单,但是初学者还是要注意,空指针/野指针一直都是 CPP 编程中非常棘手的问题。
  2. 我想知道为什么 IReadListView 加载过一次数据后就可以正常运行了,即使数据已经被清除干净

最后经过查阅各方资料,可以总结第二个现象出现的原因:

ui->IReadListView->selectionModel() 返回的 QItemSelectionModel 对象是由 Qt 在内部管理的,它通常在视图被创建时被创建和设置。一般情况下,不需要手动为视图设置选择模型,而是让 Qt 自动管理。

而在添加数据后问题消失,这可能是因为在添加数据时,Qt 内部完成了与选择模型的连接。这样一来,在调用ui->IReadListView->selectionModel() 时,就不再返回空指针。

然而,当删除所有数据时,Qt 可能仍然保留了选择模型,因此在调用 hasSelection() 时不会出现问题。这种情况下,虽然视图内没有实际的选中项,但选择模型仍然存在,因此不会导致空指针访问。

补充(关于 Qt 内部完成了与选择模型的连接):

在Qt中,视图类(如QListView、QTreeView等)通常有一个与之相关联的选择模型(QItemSelectionModel)。选择模型负责跟踪视图中的选择项,以及提供有关选择的信息。

当向视图中添加数据时,Qt 会在内部管理与这些数据相关的选择模型。这是为了确保当选择或取消选择视图中的项时,选择模型能够正确地跟踪这些更改。当调用 ui->IReadListView->selectionModel() 时,实际上是在请求与 ui->IReadListView 相关联的选择模型的指针。如果在添加数据时,Qt 内部已经创建并与选择模型连接,那么这个指针将不会是空的。

在删除所有数据时,选择模型可能仍然存在,即使视图中没有实际的选中项。这是因为选择模型是与视图相互独立的对象,它可能被保留下来以便在将来重新使用。关于问题的根本在于,当删除所有数据时,应用逻辑可能需要更显式地处理选择模型的状态。

继续深究可以查阅 QT 关于这两个函数的定义(selectionModelsetSelectionModel):

图3.1 selectionModel 的定义
图3.2 setSelectionModel 的定义

其中,在 setSelectionModel 函数的定义描述中我们可以看到这样的描述:“It is up to the application to delete the old selection model if it is no longer needed;(如果旧的选择模型不再需要,由应用程序负责删除它)”

所以,可以推测出这个模型关联一旦产生就不会被自动回收,除非被关联的模型本身需要回收(“This will happen automatically when its parent object is deleted.”)时才会自动删除这种关联。于是,就产生了一旦加载过数据(已经建立关联),就不会再出现崩溃(关联未被清除)的现象

至此,这个问题才算被很好的解决了!虽然我们并不清楚 QT 内部到底是怎么管理的(等有机会再去查源码再更加深入研究),但是我们研究清除了问题产生的原因和大致过程,并且已经找到了解决方案!做到了知其然并知其所以然,这已经是进步!继续加油(ง •_•)ง!

补充说明:该函数将当前选择模型设置为给定的选择模型。请注意,如果在调用此函数后调用了 setModel(),则给定的选择模型将被视图创建的一个替代。注意:如果旧的选择模型不再需要,由应用程序负责删除它;即,如果它没有被其他视图使用。当其父对象被删除时,这将自动发生。但是,如果它没有父对象,或者父对象是一个长寿命的对象,最好调用其 deleteLater() 函数来显式删除它。参见 selectionModel()、setModel() 和 clearSelection()。