Qt5中的内存泄漏?如何删除QMimeData?

6

我刚刚回答了这个问题(链接),并且想要提供一个可行的示例,但当我注意到由QListModel::mimeData()返回的新创建的QMimeData实例在应用程序终止之前不会被删除时,我决定还是说明一下这个问题。

所以这并不是一个真正的内存泄漏,因为Qt会在关闭时处理所有QMimeData实例,但您只需要足够长时间地拖放并将正确的内容放入mime数据中,就可以让内存耗尽。

我错过了什么吗?有没有办法告诉Qt在不再需要QMimeData实例时立即删除它们?

请注意:

我知道每个QMimeData实例都会在程序终止时由Qt自动删除。我的问题不是像valgrindcppcheck报告的那种真正的内存泄漏,而是多个、可能非常大的QMimeData实例在运行时似乎没有被清除,从而导致内存消耗过多。

示例代码:

#include <QtWidgets>
#include <iostream>

struct TrackedMimeData : public QMimeData {
   TrackedMimeData(const QString & text) {
      std::cout << this << std::endl;
      setText(text);
   }
   ~TrackedMimeData() {
       std::cout << "~" << this << std::endl;
   }
};

struct MyListWidget : QListWidget {
   MyListWidget() {
      setDragEnabled(true);
      addItem("item1");
      addItem("item2");
   }
   QMimeData * mimeData(const QList<QListWidgetItem *>) const override {
      return new TrackedMimeData("hello");
   }
};

int main(int argsc, char *argsv[]) {
   QApplication application(argsc, argsv);
   MyListWidget gui;
   gui.show();
   return application.exec();
}

示例输出如下:

0xa58750
0xa4e0f0
~0xa4e0f0
0xa3c6c0
~0xa3c6c0
0xa51880
0xa5ecd0
0xa31f50
0xa57db0
0xa5afc0
~0xa5afc0
0xa5aa70
~0xa5aa70
------ CLOSE WINDOW
~0xa58750
~0xa51880
~0xa5ecd0
~0xa31f50
~0xa57db0

只有在确认删除时,析构函数才会在关闭应用程序之前被调用。

顺便提一下,我使用的是自制的Qt 5.6 @1fcdb6cafcf - 在一台计算机上,另一台计算机上预编译了5.6.0-19.fc23 Fedora 23。因此,我怀疑这不仅仅是一个临时的开发状态。


QMimeData 继承自 QObject,并将其数据内部存储为字符串和 QVariant 对的 QVector。它具有任何其他 QObject 派生类所经历的相同内存管理限制。 - jonspaceharper
cppcheck 在这个例子中找不到任何问题,因为 QMimeData 实例在结束时被删除了。但在那之前,我可能已经存储了大量数据在成千上万个新的 QMimeData 实例中 - 这也就是我承认的 - 不是一个经典的内存泄漏问题。 - frans
1
您是否实际观察到了过多的内存消耗,还是只是担心可能会发生这种情况? - jonspaceharper
女士们先生们,这就是你们如何因为提出明确的问题并附带自包含测试用例而获得应得的甜蜜回报。这应该成为每个人的案例研究,特别是那些问题被投票下降和/或关闭的人。我认为,如果值得评论缺点,那么也值得评论优点,以加强良好行为的重要性。做得好! - Kuba hasn't forgotten Monica
@Jon:我相当确定存在内存泄漏问题,但我最初想要追踪QMimeDataQDrag实例的生命周期,以了解何时会中止拖放操作。目前我无法知道何时隐藏某些“按需放置小部件”,这是令人困扰的。 - frans
只有在您没有将“QMimeData”传递给适当的处理程序时才会出现泄漏。如果新创建的“QMimeData”对象没有由创建它的对象分配父级(或者您没有直接删除它),则看起来像是潜在的泄漏情况。 - jonspaceharper
2个回答

2
只有当您忘记删除由 mimeData() 返回的指针时,才会出现内存泄漏。您必须像处理任何指针一样管理所有权。
例如,如果您将 mimeData() 返回的指针传递给使用 setMimeData()QDrag 对象,则它将拥有该指针,并将在拖动操作结束时负责删除它。在这种情况下,没有内存泄漏。
参见: http://doc.qt.io/qt-5/qdrag.html#setMimeData

我使用一个派生自QAbstractItemViewQListWidget,并创建/处理QDrag对象。我只创建QMimeData,然后由QDrag对象拥有,而我无法访问该对象。 - frans
1
是的,但文档说QDrag::setMimeData会将所有权传递给QDrag对象。这意味着当QDrag被删除时,QMimeData对象也会被删除。 - galinette
好的,但是QDrag对象就来不及被删除了 - 我会在我的问题中纠正这个细节,但问题仍然存在。 - frans
QDrag 对象由私有单例类 QDragManager 内部管理,因此不会发生泄漏。 - jonspaceharper
1
这不是一个很有用的泄漏定义。我可以启动/中止任意数量的拖放操作,尽管它们不再使用,但底层的QDragQMimeData实例在程序生命周期内将永远不会被清理。对我来说,这就是一个泄漏。 - frans
显示剩余3条评论

1

如果这件事情应该发生,但实际上没有发生,那么这是一个 bug - 除了报告它(如果还没有被报告),你无法做任何事情。

我在 OS X 上使用 Qt 5.6 无法重现此问题。它可能是平台特定的问题,或者是旧版 Qt 中已经修复的 bug。当我在拖动结束时释放鼠标按钮时,mime 数据被删除。析构函数执行时的调用堆栈中,在某个地方通过事件循环调用 deleteLater 删除了 QDrag 析构函数。我已经逐字使用了你的代码。

侧边栏:如果你真的想让它尽可能短小,它可以更简约一些。以下是我得到的内容,尽管我认为这主要是无关紧要的细节。你的问题通过提供一个可工作的示例 - 对此给予高度赞扬!超过了 99% 的其他问题。我认为影响简洁性的因素是:

  1. 包含所需的Qt模块。
  2. 使用qDebug而不是std::cout等,这样可以节省一个include,而且更符合Qt风格。
  3. 如果您使用struct,则访问说明符无关紧要。
  4. 一般来说,在公共基类中析构函数要么是虚拟的,要么就不能避免被切片。因此,您不必详细解释。

当然,我有不遵循这些规则的示例代码。我喜欢称之为遗留代码 :)

#include <QtWidgets>

struct TrackedMimeData : QMimeData {
   TrackedMimeData(const QString & text) {
      qDebug() << this;
      setText(text);
   }
   ~TrackedMimeData() {
      qDebug() << "~" << this;
   }
};

struct MyListWidget : QListWidget {
   MyListWidget() {
      setDragEnabled(true);
      addItem("item1");
      addItem("item2");
   }
   QMimeData * mimeData(const QList<QListWidgetItem *>) const override {
      return new TrackedMimeData("hello");
   }
};

int main(int argsc, char *argsv[]) {
   QApplication application(argsc, argsv);
   MyListWidget gui;
   gui.show();
   return application.exec();
}

不幸的是,我无法从qDebug中看到任何输出 - 无论是在终端还是在QtCreator中。但是使用cout的输出证明了析构函数没有被调用(在我的情况下)。 - frans
@frans 奇怪...这是在哪个平台和Qt版本上?这绝对是一个bug。如果还没有报告,请报告一下。 - Kuba hasn't forgotten Monica

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接