网络上有很多关于C++单例模式的帖子,其中不乏精品之作。本篇文字在吸收了精华之余,仅作了个人的一点点总结。
通过new出一个对象来实现的单例,不论单例是通过饿汉方式,还是懒汉方式来实现,都面临一个问题,即new出来的对象由谁释放,何时释放,怎么释放 ?简单的实现可以参考C++ 单例模式
如果对象没有被释放,在运行期间可能会存在内存泄露问题。有人可能会说,在进程结束时,操作系统会进行必要的清理工作,包括释放进程的所有堆栈等信息,即使存在内存泄露,操作系统也会收回的;且对于单例来讲,进程运行期间仅有一个对象实例,而且该实例有可能根本就没有进行内存的申请操作,不释放实例所占内存,对进程的运行也不会造成影响。这么说好像很有道理的样子,既然操作系统会清理一切后续工作,那么我们还有必要进行内存释放工作吗?
闲话少叙,言归正传。作为一个非著名的程序猿,我和大多数同类一样,对代码有着不一般的情结,且有强迫症。成对的使用new和delete是程序猿们最基础的素养。这么看来,单例对象的释放必须要在代码中体现出来。
很自然的,可能会有部分猿/媴们[其实就是我啦^-^]想到,把释放工作交给析构函数来处理不就行了。想法是不错,代码要怎么写呐?可能如下:
~dtor() { delete instance; }
可惜的是,一:new出来的对象,必须用与之对应的delete显示的来释放,程序并不会自动调用析构函数来析构new出来的对象;二:在delete的时候会调用析构函数,析构函数中又调用了delete,然后又调用了析构函数……这样就进入了一个无限的循环之中。
可能的代码:
int main(int argc, char ** argv)
{
//...
delete Singleton::get_instance();
//...
}
valgrind检测结果
通过valgrind工具,我们可以看到,所有内存都被释放了。这种处理完成了任务,好像无可厚非。但是,大多数情况下,这条语句会被遗忘,如果程序中存在多个单例,也很容易将某个对象的释放操作遗漏。
atexit()函数可以用来注册终止函数。如果打算在main()结束后执行某些操作,可以使用该函数来注册相关函数。
可能的代码:
void del_singleton_01()
{
if (Singleton::get_instance())
{
delete Singleton::get_instance();
}
}
int main(int argc, char **argv)
{
// ...
atexit(del_singleton_01);
// ...
}
valgrind检测结果
标准规定atexit()至少可以注册32个终止函数,如果系统中有多个单例,我们可能要注册多个函数,或者在同一个终止函数中释放所有单例对象。但是方式一中的问题依然存在。必须由程序猿/媴手工注册,且有可能遗漏某个对象。
本方式由单例类提供一个释放对象的函数,在该函数内部进行对象的释放操作。其本质与方式一并无太大差别,同样的继承了方式一的缺点。
可能的代码:
class Singleton {
public:
// ...
void del_object() {
if (instance) {
delete instance;
instance = 0;
}
}
// ...
};
int main(int argc, char ** argv)
{
// ...
Singleton::get_instance()->del_object();
// ...
}
虽然这是一种可行的释放对象方式,但是这种方式并没有明显的优点。这不是我们想要的方案。
我们知道,进程结束时,静态对象的生命周期随之结束,其析构函数会被调用来释放对象。因此,我们可以利用这一特性,在单例类中声明一个内嵌类,该类的析构函数专门用来释放new出来的单例对象,并声明一个该类类型的static对象。
可能的代码:
class Singleton {
public:
// ...
private:
// ...
static Singleton * instance;
class GarbageCollector {
public:
~GarbageCollector() {
if (Singleton::instance) {
delete Singleton::instance;
Singleton::instance = 0;
}
}
};
static GarbageCollector gc;
};
// 定义
Singleton::GarbargeCollector Singleton::gc;
// ...
valgrind检测结果:
好了,我们可以像之前一样使用单例了,不需要再关心对象的释放问题。进程结束时,操作系统会帮我们去释放的。
之所以要进行内存的释放,是因为在单例的实现过程中,我们使用了new来创建对象。如果在实现过程中,不使用new,而是使用静态[局部]对象的方式,就不存在释放对象的问题了。
可能的关键代码:
class Singleton {
// ...
static Singleton instance;
// ...
};
// ...
Singleton Singleton::instance;
// ...
或者
class Singleton {
public:
Singleton & get_instance();
// ...
};
Singleton & Singleton::get_instance()
{
static Singleton instance;
return instance;
}