C++开发 | Qt项目踩坑记录(一)(框架原理篇)

294 阅读5分钟

前言

笔者最近接到一个需求,需要做一个界面来展示模型训练的数据。不同于笔者以往接触的网站开发中前端调用后端接口获取json数据并进行展示的做法,新项目中给的是后端的部分源码,以及一些已封装好的库、头文件等。展示界面需要对原有的后端逻辑代码进行解耦,并涉及一些音视频处理的基础知识、多线程应用等。笔者决定用Qt作为技术栈。

接下来的内容是对项目开发过程中遇到的一些bug的排查思路和解决方案,将不会对项目具体细节展开讨论。如果读者也有被类似的问题困扰,希望这篇文章可以帮到你。不过,由于笔者也是初次接触Qt开发,所以遇到的很多问题在老手看来也许是“常识性错误”,读者也可以仅把这篇文章作为消遣,不必过分较真。

exec阻塞事件

  • 起因:

笔者在阅读Qt项目的示例代码时,总是看到在main()函数中最后return a.exec();的写法,不过并没有细想exec的执行机制,只以为它是开始执行application的代码;再加上后端代码逻辑中原本也有一个main函数,于是我就简单地把main函数中的代码复制粘贴过来,最后再return a.exec().

  • 遇到的问题:

我发现当逻辑代码在a.exec()之前时,终端输出显示模型已经加载并运行完了,窗口还没有弹出来;当逻辑代码在a.exec()之后时,窗口出来了,逻辑代码却迟迟不见有动静。

  • 原因:通过查阅资料得知,exec()实际上是一种阻塞机制,它以循环等待的方式,让窗体能不断地接收来自用户的指令、并执行与Qt相关的操作等。因此,直到窗口被关闭之前,exec()之后的代码不会被执行。

  • 解决方案:

我的解决方案是:把后端逻辑代码中main的相关代码按逻辑和功能重新拆分成不同的类及其中的成员变量和成员函数,并通过信号与槽机制将其与界面相关联。

信号与槽机制

上面提到,笔者了解到了Qt的核心特性:信号与槽机制。据说,现在的桌面程序框架对于对象间的通信处理有两种基本思路:

  • 回调函数(如MFC会用到)
  • 信号与槽

我在做项目的过程中也接触了一些回调函数的代码,它们以lambda表达式形式出现的时候居多(当然并不是说回调函数只能是lambda表达式)。相反,回调函数其实指的是把一个函数作为参数传入另一个函数的情况,但当另一个函数执行完了才执行这个函数。(类似于栈,B函数要调用A函数,作为参数的A先入栈了,B再入栈,但当B执行完出栈了,才轮到A执行,在代码执行的过程中实现了一个往回调用的效果)

回调函数多用于外部事件实时触发某一函数功能的场景。

那么什么是信号(signal)?什么是槽(slot)?它们之间的关系又是如何呢?

我对此的理解是:

  • 信号被发出后,对应的槽会执行相应的操作。这个机制和回调函数中的“触发”有异曲同工之处。
  • 然而,信号与槽比回调函数更加灵活。因为在回调函数中,被调用和调用的函数是一一对应的、绑定好的;而信号与槽之间则是相互独立的,是一种多对多的关系:一个信号可以同时关联多个槽,同一个槽也可以在接收多个信号时都作出响应。信号并不关心有哪些槽接收了它,槽函数内部也不关心发过来的是什么信号,他们各司其职,只是被一个connect关联起来,然后在某个时机各自被触发了而已(信号被发送,槽函数开始执行)

理论上讲,信号与槽的响应速度是比回调函数慢的。但是在灵活性和可控性面前,牺牲一点点性能也是可以接受的事。

接下来讲讲笔者在用信号与槽时踩的坑。

  • 现象: 在为窗体新添加了一些按钮和槽函数后,再次启动,程序突然出现了一些奇怪的bug。包括但不限于:
    • 打开文件目录的窗口在用户已选择文件后仍未消失,需再次选择
    • 原来正常显示的图像界面变得很奇怪,初步判定是后端部分未能正确获取某个参数所致
    • 程序执行一次后就异常阻塞,疑似资源未得到正确释放
  • 原因排查:

梳理了一遍代码逻辑,并未发现异常。最后请来我当时的导师,他认为问题可能出在信号与槽的连接上。于是他重新检查了各个信号与槽连接的情况,最终突然发现我的槽函数命名和按钮默认的方式一样(格式:on_xxx),但却是自定义的一个槽函数,而不是默认槽。

简单将名字更换掉之后再运行,上述bug就都消失了。

导致这种情况更核心的原因是:on_xxx是QT通过对所有给定的子对象进行递归检索自动生成的,如果自定义的和它重名,在编译链接时更是会被系统理解为是要重复执行。但是由于这两个槽的内容终究是不一样的,所以会因冲突引发一些bug。

  • 启示:

自定义槽函数时要注意规避默认写法。