单例模式的陷阱

公司编辑UI有个编辑器,是个MFC框架的窗口。长时间以来,每次点关闭按钮关闭的时候都会报windows的调试,从而再启动一次。

单例模式的陷阱

平时大家工作都是赶任务,这个问题一直能忍则忍。后来这个问题导致我其他vs解决方案链接的时候老是失败。

由于这个编辑器会引用到另一个项目的lib,如果编辑器不能正常关闭,那么另一个项目链接的时候就会报无法打开某个lib。

于是我就想着怎么解决编辑器不能正常关闭的问题。

编辑器有源码,是MFC框架的。于是我首先简单找了一本侯捷的《深入浅出MFC》看了一下MFC的关闭流程。发现没有什么特别的地方。

为了保持环境干净,我重新clone了一份编辑器的代码,再重新生成。偶然发现每次关闭之后,工作目录下都会有个文件的修改时间变成新的。

单例模式的陷阱

难道有内存泄漏?而且我们的代码里有内存追踪模块?

打开文件看了之后,确认应该是有内存泄漏。而且每次几乎是一样的,泄漏的地方相同。这可能就好找了。

不过我好奇怎么做的内存追踪,于是我读了一下我们引擎里的内存追踪模块。这块另写一篇来讲。

这里讲一个单例模式的内存泄漏。还有怎么写能不内存泄漏。

直接看一下简化后的代码:

 1 class Singleton
 2 {
 3 public:
 4     static Singleton* Instance();
 5     Singleton() = default;
 6     ~Singleton() = delete;
 7 
 8     int mem = 1;
 9 
10     void printInstance() { cout << "print Instance" << endl; }
11 };
12 
13 Singleton * Singleton::Instance()
14 {
15     static Singleton* instance = new Singleton;
16     return instance;
17 }
1 int main()
2 {
3     Singleton::Instance()->printInstance();
4     Singleton::Instance()->printInstance();
5 
6     return 0;
7 }

 运行结果:

单例模式的陷阱

注意到几点:1.析构函数=delete。2.Instance()里有一个static的Singleton*,有new操作,但是没有对应的delete操作。

1.析构函数=delete。

这个c++11的新写法。代表该函数为删除函数,也就是不能重载,不会被调用。这类函数可以声明,但是没有定义。编译器会阻止对它们进行定义。

类似的如果想要阻止拷贝,阻止赋值拷贝,也可以把拷贝构造函数和赋值拷贝构造函数声明为删除函数。

Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;

但是这里把析构函数声明为删除函数??这样就阻止了对象的析构了。
在《c++ primer》第五版的13.1.5节里有提到,析构函数不能是删除函数。

这里有什么其他的考量?不得而知。

2.static的Singleton*。

这里是一个已初始化局部的static变量,我们知道已初始话的static变量会放在.data区,而且会在程序结束的时候自动清理。

但是这里是new出来的对象,所以程序结束的时候是不会清理的。

怎么改比较好呢?

1.首先析构函数当然不能是删除函数。

2.new出来的static对象指针在程序结束不会自动清理,但是static对象会自动析构。可以利用这一点写一个static自动清理的类对象。

 1 #include <iostream>
 2 
 3 using namespace std;
 4 
 5 class Singleton
 6 {
 7 public:
 8     Singleton() { cout << "Singleton Constructor " << endl; }
 9     ~Singleton() { cout << "Singleton Destructor " << endl; }
10 
11     static Singleton* Instance();
12     static Singleton* m_instance;
13 
14     int mem = 1;
15 
16     void printInstance() { cout << "print Instance" << endl; }
17 
18     class AutoRelease
19     {
20     public:
21         AutoRelease() { cout << "AutoRelease Constructor " << endl; }
22 
23         ~AutoRelease()
24         {
25             if (m_instance != nullptr)
26             {
27                 delete(m_instance);
28                 m_instance = nullptr;
29                 cout << "AutoRelease Destructor" << endl;
30             }
31         }
32     };
33 };
34 
35 Singleton* Singleton::m_instance = nullptr;
36 
37 Singleton * Singleton::Instance()
38 {
39     if (m_instance == nullptr)
40     {
41         m_instance = new Singleton;
42         static AutoRelease auto_release;
43     }
44     return m_instance;
45 }
46 
47 int main()
48 {
49     Singleton::Instance()->printInstance();
50     Singleton::Instance()->printInstance();
51 
52     return 0;
53 }

运行结果:
单例模式的陷阱

如此就能正常析构了。

如此思路重构了代码,memeryleak里面便少了这部分的内存泄漏记录。