Qt C++:全局对象与引用链

igsr9ssn  于 2022-11-19  发布在  其他
关注(0)|答案(2)|浏览(247)

目前我在我的应用程序(Symbian环境的Qt应用程序)中对某些全局对象使用单例模式。但是,由于一些问题(C++检查单例指针),看起来我必须改变逻辑。
我有三节课(记录器、设置和一些临时数据的容器),我需要通过多个不同的对象访问它们。目前它们都是使用单例模式创建的。记录器基本上只是一个公共方法Log当设置和容器具有多个带有一些附加逻辑的get/set方法时,带有一些内部逻辑的(例如QFileSystemWatcher)。此外,记录器和设置有一些交叉引用(例如,记录器需要一些设置,而设置记录错误)。
目前一切都“工作正常”,但仍有一些问题需要处理,似乎它们不容易实现为单例(可能的内存泄漏/空指针)。现在我有两种不同的方法来处理:
1.创建全局对象(例如extern Logger日志;)并在应用程序启动时初始化它们。
1.在我的主对象中创建对象,并将它们作为引用传递给子对象。
我有几个问题如何与这些相关:

案例1.

  • 使用堆栈还是堆更好?
  • 我将在globals.h头文件中使用extern关键字声明这些对象,可以吗?
  • 我认为在这种情况下,我必须删除双向引用(设置需要记录器,反之亦然)。
    案例2.
  • 对象应该在我的主对象中以堆栈或堆的形式创建(例如Logger *log = new Logger()vs Logger log;)
  • 长的引用链看起来不太好(例如,如果我必须将对象传递给多个子对象)。

1.孩子呢?
1.如果我像这样传递一个指针给孩子(我不想复制它,只是使用“引用”):子项(记录器 * 日志):m_Log(log)删除子节点后会发生什么?我应该将本地指针m_Log设置为NULL还是?
1.如果我使用堆栈,我会将引用发送到子对象(Children(Logger &log):m_Log(log)),其中m_Log是参考变量(Logger& m_Log;)对不对?
1.在这种情况下,在Qt内存管理方面应该注意什么?

**案例3.**继续使用singleton并在启动时初始化singleton对象(这将解决空指针问题)。然后唯一的问题是可能的内存泄漏。我的实现遵循this示例。当我使用访问类时,是否存在可能的内存泄漏。singleton销毁如何?#define LOG Logger::Instance()-〉Log

感谢阅读。

thtygnil

thtygnil1#

简单来说就是:
1.如果您使用全局对象,那么最好使用单例模式。注意,单例应该具有全局访问权限!Dan-O的解决方案并不是真正的单例模式,它击败了单例的强大功能,尽管他认为这并没有什么不同。
1.如果您使用全局对象,请使用惰性构造来避免初始化顺序问题(在首次访问它们时初始化它们)。
1.如果您使用单例,则不要将所有需要全局访问的对象都设置为单例,而是考虑将一个单例(Application)设置为单例,该单例存储其他全局可访问的对象(Logger、Settings等),但不要将这些对象设置为单例。
1.如果您使用局部变量,无论如何都要考虑#3,以避免在系统中传递太多东西。
[编辑]我犯了一个错误,把静态放在了safe_static中,Dan指出了这一点。感谢他。我一时失明,没有意识到他提出的问题中的错误,这导致了最尴尬的局面。我试图解释懒惰的结构(又名懒惰加载)的行为,他没有遵循,我犯了一个错误,我仍然没有意识到我犯了一个,直到第二天.我不感兴趣的争论,只提供最好的建议,但我必须强烈建议反对一些建议,特别是这种情况:

#include "log.h"

// declare your logger class here in the cpp file:
class Logger
{
// ... your impl as a singleton
}

void Log( const char* data )
{
    Logger.getInstance().DoRealLog( data );
}

如果你打算使用像singleton这样的全局可访问对象,那么至少要避免这样做!它可能有吸引客户端的语法,但是它违背了singleton试图缓解的许多问题。你想要一个公共可访问的singleton示例,如果你创建这样的Log函数,你想把你的singleton示例传递给它。有很多原因,但是这里只有一个场景:您可能希望创建具有通用接口的单独的记录器单例(例如,错误记录器、警告记录器和用户消息记录器)。此方法不允许客户端选择和使用公共的日志记录接口。它还强制在每次记录时检索单例示例,这使得如果您决定避开单例,将有更多代码需要重写。
创建全局对象(例如extern Logger日志;)并在应用程序启动时初始化它们。
至少对于用户定义的类型,要尽一切努力避免这种情况。给对象外部链接意味着你的记录器将在主入口点之前构造,如果它依赖于任何其他类似的全局数据,就不能保证初始化顺序(你的记录器可能正在访问未初始化的对象)。
相反,考虑访问是初始化的这种方法:

Logger& safe_static()
{
    static Logger logger;
    return logger;
}

或者在您的情况下:

// Logger::instance is a static method
Logger& Logger::instance()
{
    static Logger logger;
    return logger;
}

在这个函数中,只有在safe_static方法被调用后才会创建记录器。如果你将这个方法应用于所有相似的数据,你不必担心初始化顺序,因为初始化顺序将遵循访问模式。
请注意,尽管它的名字是超级安全的,但它并不是超级安全的。如果两个线程同时第一次并发调用safe_static,仍然容易出现与线程相关的问题。避免这种情况的一种方法是在应用程序开始时调用这些方法,这样就可以保证数据在启动后被初始化。
在我的主对象中创建对象,并将它们作为引用传递给子对象。
以这种方式传递多个对象可能会变得很麻烦,并显著增加代码大小。请考虑将这些对象合并到一个包含所有必要上下文数据的聚合中。
使用堆栈还是堆更好?
从一般的Angular 来看,如果你的数据很小,可以很好地放入堆栈中,堆栈通常是首选。堆栈分配/解除分配非常快(只是增加/减少堆栈寄存器),并且没有任何线程争用的问题。
然而,由于您是针对全局对象提出这个问题的,所以堆栈没有多大意义。也许您会问是应该使用堆还是数据段。后者在很多情况下都是可以的,并且不会出现内存泄漏问题。
我将在globals.h头文件中使用extern关键字声明这些对象,可以吗?
否。@请参阅上面的safe_static。
我认为在这种情况下,我必须删除双向引用(设置需要记录器,反之亦然)。
尝试从代码中消除循环依赖总是好的,但是如果你不能,@请参见safe_static。
如果我像这样传递一个指针给孩子(我不想复制它,只是使用“引用”):子项(记录器 * 日志):m_Log(log)删除子节点后会发生什么?我应该将本地指针m_Log设置为NULL还是?
没有必要这样做。我假设记录器的内存管理没有在子进程中处理。如果你想要一个更健壮的解决方案,你可以使用boost::shared_ptr和引用计数来管理记录器的生存期。

如果我使用堆栈,我会将引用发送到子对象(Children(Logger &log):m_Log(log)),其中m_Log是参考变量(Logger& m_Log;)对不对?
无论使用堆栈还是堆,都可以通过引用传递。然而,将指针存储为成员而不是引用的好处是,编译器可以在需要的情况下生成一个有意义的赋值运算符(如果适用),而不需要自己显式定义一个。
例3.继续使用单例,并在启动时初始化单例对象(这将解决空指针问题)。然后,唯一的问题可能是内存泄漏。我的实现遵循这个例子。当我使用访问类时,是否可能存在内存泄漏。单例销毁呢?
使用boost::scoped_ptr或者只是将类作为静态对象存储在访问器函数中,就像上面的safe_static一样。

s6fujrry

s6fujrry2#

我发现另一个答案有点误导。下面是我的答案,希望SO社区能够确定哪个答案更好:
使用堆栈还是堆更好?
假设你所说的“栈”是指全局的(也就是说在数据段中),不要担心它,做任何对你来说更容易的事情。记住,如果你在堆上分配它,你就必须调用delete。
我将在globals.h头文件中使用extern关键字声明这些对象,可以吗?
为什么要这样做呢?需要访问全局变量的类只有单例本身。全局变量甚至可以是静态局部变量,例如:

class c_Foo
{
    static c_Foo& Instance()
    {
        static c_Foo g_foo; // static local variable will live for full life of the program, but cannot be accessed elsewhere, forcing others to use cFoo::Instance()
        return g_foo;
    }
 };

如果您不想使用静态局部变量,那么使用c_Foo类型的私有静态成员变量(在我的示例中)比直接使用全局变量更合适。
请记住,您希望类的生存期是“全局”的(即在应用程序退出之前不会被破坏),而不是示例本身。
我认为在这种情况下,我必须删除双向引用(设置需要记录器,反之亦然)。
所有的外部全局变量都必须向前声明,但是正如我上面所说的,您不需要这个头文件。
对象应该在我的主对象中以堆栈或堆的形式创建(例如Logger *log = new Logger()vs Logger log;)
这个我真的回答不了,别再担心了。
长的引用链看起来不太好(例如,如果我必须将对象传递给多个子对象)。
孩子呢?
很好的观点,你已经意识到这将是一个巨大的痛苦。在像一个日志记录器的情况下,它将地狱传递引用到每个模块,只是为了他们可以吐出日志信息。更合适的是一个单一的静态“日志”函数ala C,如果您记录器具有有用的状态,则将其设置为仅对您的记录器函数可见的单例。您可以在实作记录函式的同一个.cpp档案中宣告和实作整个记录程式类别。这样就不会有其他人知道这个特殊的功能。
我的意思是:
文件:log.h

#ifndef LOG_H
#define LOG_H

void Log( const char* data ); // or QString or whatever you're passing to your logger

#endif//LOG_H

文件:log.cpp

#include "log.h"

// declare your logger class here in the cpp file:
class Logger
{
// ... your impl as a singleton
}

void Log( const char* data )
{
    Logger.getInstance().DoRealLog( data );
}

这是一个很好的记录器接口,你的单例的所有功能,有限的大惊小怪。
当我使用访问类时,是否有可能出现内存泄漏?单例销毁呢?
在多线程环境中,如果你在堆上懒洋洋地分配单例,就有双重示例化的风险,这会导致多余的示例泄漏。
如果您使用静态局部变量,也会有类似的风险:Link
在我看来,在执行开始时显式构造是最好的选择,但是无论是在数据段(类中的一个静态变量或全局变量)还是在堆中进行构造,在你的情况下都不太可能有关系。如果你在堆中分配了它,你也应该删除它。

相关问题