singleton

今天设计一个类时,根据需求在网上学到了单例模式,重新梳理了private构造析构函数的作用。

构造函数和析构函数

private

私有成员只能在类域内被访问,不能在类域外进行访问。
无论将构造函数还是虚构函数设置为private,都可以防止外部创建栈对象,而只能由类的成员函数创建(实际上只能由类的静态成员函数创建,因为对象通过成员函数创建,而非静态成员函数需要对象才能调用,陷入了逻辑循环)。
分开来说的话,构造函数设置为private,可以防止外部创建对象;而虚构函数设置为private,可以禁止外部栈对象(因为自动调用析构函数),但仍可以使用new创建堆对象,此时不能再外部调用delete,只能由类的成员函数调用delete,可以自由控制对象生存周期,并防止内存泄漏。
以下是一个例子

#include<iostream>
class A{
public:
	A(){}
	void destroy(){
		std::cout << "destroy begin" << std::endl;
		delete this;
		std::cout << "destroy end" << std::endl;        
	}
private:
	~A(){
		std::cout << "destructor" << std::endl;
    }
};
int main(){
	A* p = new A;
	p->destroy();
	return 0;
}

最初看到使用非静态成员函数进行delete时,感到很奇怪,因为在类的成员函数中,this指针是指向调用该函数的对象的指针,而delete this会导致对象被销毁,那么函数还能正常运行?但其实this指针指向的是调用该成员函数的对象,而不是成员函数所属的类的对象,所以是安全的。把this看做destroy函数的参数,就能理解了。

virtual

这一部分是突然想到了就在此记录了。
C++中,构造函数不可以是虚函数,而析构函数可以且常常是虚函数。

C++多态是通过虚函数实现的,而虚函数记录在虚函数表中,实际上类每个对象中包含了vptr指针,指向虚函数表。

构造

对象创建时,由编译器对vptr初始化,如果构造函数是虚函数,那么调用构造函数就要查找vptr,而此时vptr还没有初始化(实际上也是在构造函数中初始化,只是早于其他成员),所以构造函数不能是虚函数。而且从语义上讲,构造函数是用来初始化对象的,使用虚函数也没有意义。

析构

相反,析构函数是用来销毁对象的,通常是虚函数。如果析构函数不是虚函数,那么在销毁基类指针指向对象时,只会调用基类对象的析构函数,而不会调用派生类的析构函数,这样就会导致内存泄漏,派生类独有的成员无法被析构。

#include <iostream>
using namespace std;

class base {
public:
    base(int x) {
        cout << "base constructor" << x << endl;
    }
    virtual ~base() {
        cout << "base destructor" << endl;
    }
};

class derived : public base {
public:
    derived(int x):base(x) {
        cout << "derived constructor" << x << endl;
    }
    virtual ~derived() {
        cout << "derived destructor" << endl;
    }
};

int main()
{
    base *pBase = new derived(1);
    delete pBase;
	return 0;
}

注意基类和派生类构造析构顺序:构造时,先调用基类构造函数,再构造派生类;析构时,先调用派生类析构函数,再调用基类析构函数。

单例模式

铺垫了这么多,总算进入正题了。

单例对象属于创建型模式的一种,必须保证只有一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为,比如c++里的cout对象。

单例模式的实现通常有两种方式:懒汉式和饿汉式。懒汉式是在第一次调用时创建对象,而饿汉式是在程序启动时创建对象。

懒汉

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
template <typename T>
class singleton
{
    public:
        static T * request();
        static void release();

    private:
        singleton(){}
        ~singleton(){
            release();
        }
		singleton(const singleton&) = delete;
		singleton& operator=(const singleton&) = delete;
        static T * pointer;
        static mutex m;
};

template <typename T>
T* singleton<T>::pointer = nullptr;

template <typename T>
mutex singleton<T>::m;

template <typename T>
T * singleton<T>::request()
{
	if (pointer == nullptr)
	{
		lock_guard<mutex> lock(m);
		if (pointer == nullptr)
		{
			pointer = new T();
		}
	}
	return pointer;
}
template <typename T>
void singleton<T>::release()
{
	if (pointer != nullptr)
	{
		lock_guard<mutex> lock(m);
		if (pointer != nullptr)
		{
			delete pointer;
			pointer = nullptr;
		}
	}
}
int main()
{
	auto p = singleton<int>::request();
    cout << p << endl;
	singleton<int>::release();
	return 0;
}

上面代码根据维基百科修改得来,使用了双检测锁模式,对pointer两次判断的原因:多线程环境下,不加锁的话两个线程同时判断pointer为空,然后都创建对象,这样就不是单例了。但是加锁会影响性能,所以需要两次判断。第一次判断是为了避免不必要的加锁,第二次判断是为了避免多线程同时创建对象。
代码使用模板,可以创建任意类型的单例对象,但对象本身的构造函数和析构函数必须是public的。并不是通过禁止类外创建对象来实现单例,而是通过控制对象的创建和销毁来实现单例。

C++11规定了local static在多线程条件下的初始化行为,要求编译器保证了函数内部静态变量的线程安全性: 在一个线程开始local static 对象的初始化后到完成初始化前,其他线程执行到这个local static对象的初始化语句就会等待,直到该local static 对象初始化完成。
因此有一种更简单的方式,即Meyers'Singleton

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
class Singleton
{
private:
	Singleton(){}
	~Singleton(){}
	Singleton(const Singleton&);
	Singleton& operator=(const Singleton&);
public:
	static Singleton& getInstance() 
    {
		static Singleton instance;
		return instance;
	}
};
int main()
{
	Singleton& s1 = Singleton::getInstance();
	return 0;
}

饿汉

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
class Singleton
{
private:
	static Singleton instance;
private:
	Singleton(){}
	~Singleton(){}
	Singleton(const Singleton&);
	Singleton& operator=(const Singleton&);
public:
	static Singleton& getInstance() {
		return instance;
	}
};
// non local static
Singleton Singleton::instance;
int main()
{
	Singleton& s = Singleton::getInstance();
	return 0;
}

这种情况下,初始化在main函数之前,所以不需要考虑多线程问题。但是存在隐藏风险,non local static的初始化顺序是未定义的,如果其他全局对象(甚至来自其他编译文件)依赖于Singleton对象(使用getInstance),就会出现问题,不过引用貌似会按正确顺序初始化,但是我们推荐的是使用函数获取静态对象,可以自己决定初始化顺序。

总结起来还是用Meyers'Singleton比较好,简单,安全。

posted @ 2024-02-28 14:01  trashwin  阅读(6)  评论(0编辑  收藏  举报