单例模式中的隐藏陷阱:你真的了解单例吗?

单例模式中的隐藏陷阱:你真的了解单例吗?引言  单例模式是日常开发中较常用的一种设计模式,它能够确保一个类只有一个实例,并提供一个全局访问入口。  在之前的开发过程中,观察到进程退出时偶现 crash 现象。由于该问题仅发生在生命周期末端且

单例模式中的隐藏陷阱:你真的了解单例吗?

单例模式中的隐藏陷阱:你真的了解单例吗?

引言

  单例模式是日常开发中较常用的一种设计模式,它能够确保一个类只有一个实例,并提供一个全局访问入口。

  在之前的开发过程中,观察到进程退出时偶现 crash 现象。由于该问题仅发生在生命周期末端且未影响核心功能,并未深入排查原因。

近期看到技术公众号分析单例模式潜在问题的,意识到此前的crash大致就是因此导致的。

场景列举

问题   先看代码,看看执行会发生什么?

main 函数

  • 首先获取 SingletonB 的实例。
  • 然后获取 SingletonA 的实例。
  • 最后输出退出信息。
代码语言:javascript代码运行次数:0运行复制
int main() {
    SingletonA::getInstance();
    SingletonB::getInstance();
    std::cout << "exe exit!" << std::endl;
    return 0;
}

单例类 SingletonB

  • SingletonB 类使用静态局部变量 instance 来确保只有一个实例。
  • 构造函数中,mpBuf 分配了内存,并将字符串 "hello" 复制到该内存中。
  • 析构函数中,释放了 mpBuf 所占用的内存,并置为nullptr
代码语言:javascript代码运行次数:0运行复制
// 单例B
class SingletonB {
public:
    ~SingletonB() {
        std::cout << "SingletonB destroyed." << std::endl;
        delete []mpBuf;     // 回收资源
        mpBuf = nullptr;
    }

    static SingletonB* getInstance() {
        static SingletonB instance;
        return &instance;
    }
    void doSomething() {
        printf("mpBuf[0] = %c\n", mpBuf[0]);
    }

private:
    SingletonB() {
        std::cout << "SingletonB created." << std::endl;
        mpBuf = new (std::nothrow) char[10];   // 创建资源
        memcpy(mpBuf, "hello", 5);
        mpBuf[5] = '\0';
    }

private:
    char *mpBuf;
};

单例类 SingletonA

  • SingletonA 类同样使用静态局部变量 instance 来确保只有一个实例。
  • 构造函数中,简单输出创建信息。
  • 析构函数中,调用 SingletonBdoSomething 接口。
代码语言:javascript代码运行次数:0运行复制
// 单例A
class SingletonA {
private:
    SingletonA() {
        std::cout << "SingletonA created." << std::endl;
    }
    ~SingletonA() {
        std::cout << "SingletonA destroy begin. call B" << std::endl;
        SingletonB::getInstance()->doSomething();
        std::cout << "SingletonA destroyed end." << std::endl;
    }


public:
    static SingletonA* getInstance() {
        static SingletonA instance;
        return &instance;
    }
};

输出

程序在退出时,发生crash了。

代码语言:javascript代码运行次数:0运行复制
$ ./a.out 
SingletonA created.
SingletonB created.
exe exit!
SingletonB destroyed.
SingletonA destroy begin. call B
Segmentation fault

代码分析

 上述的实现,很明显可以看出因为调用了已析构的对象,导致异常crash

 但是在实际项目开发过程中,单例模式的调用分散在各个文件中,由于初始化顺序难以控制,很容易出现调用已析构对象的情况,进而导致程序崩溃。这类问题一旦发生,往往难以迅速定位和解决。

  看看AI怎么评价这段代码:

代码语言:javascript代码运行次数:0运行复制
Me: 魔镜啊魔镜,上述代码有什么问题?

魔镜:来了,老弟!这代码毛病可不小呐~
     那 `SingletonA` 析构的时候,还跑去调用 `SingletonB` 的方法。
     程序结束的时候,静态对象是后构造的先析构,这 SingletonB 不妥妥先析构了,mpBuf也回收了。
     SingletonA 再去调用 SingletonB::doSomething,指定下机啊!
     哪个损粗写的这代码呐!回炉重造吧,赶紧的啊~

Me: 乡村爱情看多了吧你,口音听的上头。
    正经点,那怎么规避这类问题?

魔镜:闲来无事,网上爬了几集,真好看~
    有很多种方式可以避免这种问题,我就说两种,好好听,好好学!

    ① 避免单例对象析构,如下写法:
    SingletonA* SingletonA::getInstance() {
        static SingletonA *instance = new (std::nothorw) SingletonA();
        return instance;
    }

    ② 增加全局变量,检测对象是否析构。每次获取实例时,特别是析构中获取单例时,先判空。
    static std::atomic<bool> gObjAlive(true);
    
    SingletonA::~SingletonA() {
        gObjAlive = false;
    }

    SingletonA* SingletonA::getInstance() {
        if (!gObjAlive) {
            returnnullptr;
        }

        static SingletonA instance;
        return &instance;
    }

  通过与AI的友好沟通,规避的方案有很多。上述的两种方案,各有利弊:

  • 第一种方案 优点:写法简洁。 缺点:由于不会调用析构函数,除内存会随进程退出释放外,析构函数中涉及的硬件资源释放等操作无法执行,可能引发潜在问题。
  • 第二种方案 优点:安全性较高,可自动释放所持资源。 缺点:用时不够便捷,每次调用单例对象(尤其是在析构阶段)都需要进行判空操作。

总结

  • 结合全文,crash的问题根源在于单例析构函数中调用其他单例对象的接口。 进程退出阶段,全局单例对象的析构顺序未明确,可能导致调用了已释放的单例对象接口,引发crash
  • 尽管可以通过控制单例调用的顺序来规避此类问题。但这种方式并非最优解,而且大型项目中难以实施。另外,程序健壮性不应依赖这种非确定性的语言特性,而应通过设计优化从根本上避免此类问题。
  • 最后,代码运行的行为和结果应是可预见的,不应依赖特殊操作或非确定性行为。若有此类依赖,应是程序设计缺陷,该加以优化。

最后

用心感悟,认真记录,写好每一篇文章,分享每一框干货。

更多文章内容包括但不限于C/C++、Linux、开发常用神器等,可进入“开源519公众号”聊天界面输入“文章目录” 或者 菜单栏选择“文章目录”查看。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。原始发表:2025-04-16,如有侵权请联系 cloudcommunity@tencent 删除程序单例模式对象函数开发

发布者:admin,转转请注明出处:http://www.yc00.com/web/1747661299a4677265.html

相关推荐

  • 单例模式中的隐藏陷阱:你真的了解单例吗?

    单例模式中的隐藏陷阱:你真的了解单例吗?引言  单例模式是日常开发中较常用的一种设计模式,它能够确保一个类只有一个实例,并提供一个全局访问入口。  在之前的开发过程中,观察到进程退出时偶现 crash 现象。由于该问题仅发生在生命周期末端且

    8小时前
    10

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信