Qt - 保持指向存储在 QList 中的内容的指针的最佳方法是啥?

Posted

技术标签:

【中文标题】Qt - 保持指向存储在 QList 中的内容的指针的最佳方法是啥?【英文标题】:Qt - What is the best way to keep a pointer to something stored in a QList?Qt - 保持指向存储在 QList 中的内容的指针的最佳方法是什么? 【发布时间】:2018-11-30 16:20:43 【问题描述】:

我对 Qt 和 C++ 很陌生(我一生都在研究 C# 和 Java),而且我几乎一直在阅读我不应该再使用原始指针的所有内容(除了构造函数和析构函数中的指针,遵循 RAII 原则)。

我没问题,但有一个问题,如果没有指针,我无法轻松解决。

我会尽力解释我的问题。

我正在创建一个由我创建的自定义类 (MyClass) 的 QList,它将充当我的应用程序的模型:

QList<MyClass> modelList;

此 QList 由工作线程根据来自网络的信息进行更新(添加、删除或更新项目)。

到目前为止一切顺利。

现在我有另一个线程(GUI 线程),它有一个可绘制项目的 QList(MyDrawableItem)。这些项目中的每一个都有一个成员,该成员必须指向第一个 QList (modelList) 中的一个项目,如下所示:

QList<MyDrawableItem> listToDraw;

地点:

class MyDrawableItem

private:
    MyObject *pointedObject;

    //List of many other members related to drawing

因为每次 GUI 线程中的计时器到期时,我都必须根据指向的对象更新绘图相关成员。必须注意,按照文档中的说明,保留指向 QList 成员的指针是安全的:

注意:QLinkedList 中的迭代器和堆分配中的引用 只要引用的项目仍在 容器。这不适用于迭代器和对 a 的引用 QVector 和非堆分配 QLists。

在内部,如果 sizeof(T)

如果不使用指针,这怎么可能可行?或者更好的是,做这种工作的最佳方式应该是什么?

我找到了三种可能的解决方案:

    不要将pointedObject 保留为MyDrawableItem 的成员,而是将指向对象的索引保留在modelList 中。然后在更新中查看 QList 内部。但是如果列表中的项目被删除了怎么办?我可以发出信号,但如果更新发生在调用插槽之前?

    不要将任何东西保留为成员,并将两个信号连接到 MyDrawableItem。一个信号是updateDrawing(const MyObject&amp;),它更新与绘图相关的成员,另一个信号是removeDrawing(),它只是删除对象。这样我就不必担心线程之间的同步,因为 MyDrawableItem 对象永远不会查看 QList 的内容。在这种情况下,我可能会经常使用大量信号更新项目,而我只想偶尔更新一次 GUI(500 毫秒的计时器)。

    只需使用指针。

    发展并使用 QSharedPointers,但我从未使用过这些,我不知道这是否是最佳选择。

我认为这些解决方案都不是完美的,所以我在这里寻求您的帮助。

Qt 中的标准视图(tableview、listview、ecc.)如何处理这个问题?

我应该如何处理?

请帮帮我,我从昨天开始就在想这个,我不想弄得一团糟。

非常感谢!

【问题讨论】:

一般来说,原始指针没有问题。你应该避免的是原始的 owning 指针。指向容器中元素的指针的问题是,一些容器在添加新元素时必须重新分配它们的内存,呈现指向元素的指针无效(实际上我不知道QList是否是这种情况) @user463035818 QList&lt;T&gt; 是指向T 的指针向量。当调整QList 的大小时,只有向量被重新分配,而不是指向的Ts。然而对于小类型,QList 被优化为直接将Ts 存储在向量中。这就是为什么引用的 Qt 文档在“堆分配和非堆分配 QList”之间有所不同。 【参考方案1】:

@Benjamin T 回答的补充:

如果您决定选择选项 4 并使用 QSharedPointer(或 std 变体),您实际上可以使用“弱指针”在一定程度上控制所有权。弱指针存储对对象的引用,但不拥有它。一旦最后一个“强指针”被销毁,所有弱指针都会放弃引用并将自己重置为nullptr

对于您的具体示例,您可以为模型使用QList&lt;QSharedPointer&lt;MyClass&gt;&gt;,并在可绘制类中使用QWeakPointer&lt;MyClass&gt;。在绘图之前,您将在绘图函数中创建一个本地QSharedPointer&lt;MyClass&gt;并检查它是否有效。

我仍然建议您按照 Benjamin 的建议使用 2/3。只是一个小小的补充。

【讨论】:

我没有谈论这个,因为如果你有这样的情况,在你从列表中删除一个共享指针后,它永远不会被销毁,因为你总是有一个持有共享指针的消费者。如果您希望以确定性的方式快速销毁对象,这可能是一个很大的缺点。但如果您知道您的消费者很少持有共享指针,它仍然是一个可行的解决方案。 如果我的消费者只在更新函数期间持有一个共享指针,因为他们按照 Felix 的建议从弱指针创建了一个共享指针,那么对象不会在更新函数结束后立即销毁如果物品在此期间被销毁? 如果是这样,这不是最好的解决方案吗?更重要的是,这个解决方案线程安全吗?还是我必须用互斥锁控制对指针的访问? @Lulanz std::shared_ptrQSharedPointer 是线程安全的,但它只保证对象生命周期以线程安全的方式处理。它不保证访问指向的对象成员或值。另外我要说的是,如果您有 2 个使用者线程,每个线程都持有指向同一个对象的弱指针,那么当更新函数在一个线程中终止时,它可能已经在另一个线程中再次启动,因此延长了对象的生命周期。这意味着您无法保证该对象将被有效销毁。【参考方案2】:

这是 C++ 社区的当前趋势,特别是如果您查看所有 CppCon 会议来劝阻使用原始指针。

这种趋势并非没有销售基础,因为使用std::shared_ptrstd::unique_ptr 以及std::make_uniquestd::make_shared 可以帮助很多内存管理并防止一些陷阱(比如在运行期间抛出异常)调用构造函数)。

然而,在这种“不使用原始指针”的趋势中并没有什么绝对的,原始指针仍然可以使用,而智能指针并不能解决所有的内存管理问题。

关于 Qt 的情况,如果您正在处理 QObject 派生类并且您正确地为您的对象提供父级,那么您是安全的,因为您有一个 owning hierarchy 并且父母 QObjects 会破坏他们的孩子。如果您还使用QThread,这一点非常重要,因为QObject 实例与QThread 相关联,并且QObject 层次结构中的所有实例都与同一线程相关联。

关于您的解决方案:

    坏主意,您需要一直同步列表和索引。 这可能是一个想法,但它会改变您软件的架构。 MyObject 真的需要了解 MyObject 吗?如果答案是否定的,那么信号/插槽似乎是一个更好的解决方案。确实可能需要一些工作来解决您的刷新率问题,但这是题外话。 这看起来是个好主意,尤其是当您的对象继承QObject 时。此外,您可能希望使用QVector&lt;T *&gt;std::vector&lt;T *&gt; 并让QObject 层次结构处理对象的生命周期。您可以收听destroyed() 信号以了解对象何时被销毁,或使用QPointer。但是,它并不能解决消费者端的线程安全问题,因为该对象可能随时从另一个线程中删除。 如果您不能执行 2,则可以选择此选项。你只需要注意不要有循环依赖,否则你会泄漏内存。但是,如果您希望在删除列表中的对象时将其从 GUI 对象中删除,这可能不是一个好主意。如果您只是保留一个共享指针,您将永远不会知道它已从列表中删除。正如@Felix 所说,您可以存储弱指针并仅在需要时将它们转换为共享指针,但如果您在多个线程中有消费者,那么您可能永远不会删除该对象。此外,您必须使 MyObject 线程安全,因为您正在从一个线程改变它的状态并从另一个线程读取它。使用选项 4 确实会改变软件的拥有模型:在当前状态下,QList 拥有实例,使用选项 4,所有权与任何获得共享指针的人共享。

我个人的选择是将 QList 替换为 QVector 指针。 如果您的对象继承自 QObject,那么我会将 QVector 中的所有对象作为拥有 QList/QVector 的对象的父级。这是为了确保正确的父级和线程关联。 然后我会尝试选项2,只有在选项2失败时才出于选项4。

这是一个例子:

class MyClass : public QObject

    Q_OBJECT
public:
    explicit MyClass(QObject *parent = nullptr);

signals:
    void stateChanged(int state);
;

class Owner : public QObject

    Q_OBJECT
public:
    explicit Owner(QObject *parent = nullptr);
    void addObject(MyClass *obj) 
        obj->setParent(this);
        m_objects.append(obj);
    

    void removeObject() 
        delete m_objects.takeFirst();
    

private:
    QVector<MyClass *> m_objects;
;



class Consumer : public QObject

    Q_OBJECT
public:
    explicit Consumer(QObject *parent = nullptr);

public slots:
    void onStateChanged(int state) 
        m_cachedState = state;
    

private:
    int m_cachedState = 0;
;

int main(int argc, char *argv[])

    QCoreApplication app(argc, argv);

    QThread thread;
    thread.start();

    Owner owner;
    Consumer consumer;
    consumer.moveToThread(&thread);

    MyClass *object = new MyClass();
    owner.addObject(object);
    QObject::connect(object, &MyClass::stateChanged, &consumer, &Consumer::onStateChanged);

    return app.exec();

【讨论】:

非常感谢您的回答和所有详细信息。我有一些问题。在第二种情况下,我的可绘制对象需要知道 myobject 的最后状态。但是如果我用我的信号通过状态不是可以吗?相反,在情况 3 中,如果我使用 qobject 层次结构,我会遇到问题,因为 qlist 的所有者将在整个应用程序生命周期内保持活动状态。在这种情况下,列表中的对象永远不会被销毁,对吧?如果不是这种情况,如果计时器在销毁之前发出信号但项目已经被删除,那么监听 qobject::destroyed() 可能会导致错误? 在不使用 qobject 层次结构的情况下如何实现解决方案 3? @Lulanz 我没有考虑选项 3 的未决信号,这确实是个问题。是的,当你从向量中移除一个对象时,你需要在它上面调用delete,否则只要父对象存活,它就会一直存活。 @Lulanz 我编辑了我的答案。我现在推荐选项 2。要使用选项 3,您需要有一种线程安全的方式来确保两个线程在删除对象之前都停止使用它,这可能很难做到。选项 4 是选项 2 之后的下一个最佳选择,但需要确保您的共享类线程安全。 感谢您为我花费的时间。我真的很感激。

以上是关于Qt - 保持指向存储在 QList 中的内容的指针的最佳方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章

qt 中能不能用一个指向数组的指针对另一个数组赋值

Qt4 C++ Pointer to const QList of pointers

指向 QList 中元素的指针

QList介绍(QList比QVector更快,这是由它们在内存中的存储方式决定的。QStringList是在QList的基础上针对字符串提供额外的函数。at()操作比操作符[]更快,因为它不需要深度

Qt学习总结(C鱼)之QList和QMap容器类

Qt ListView 不显示 C++ 模型内容