> 文章列表 > 特殊类设计(单例模式)

特殊类设计(单例模式)

特殊类设计(单例模式)

文章目录

  • 设计一个类——不能被拷贝
  • 设计一个类——只能在堆上开辟空间
  • 设计一个类——只能在栈上开辟空间
  • 设计一个类——不能被继承
  • 设计一个类——只能创造一个对象(单例模式
    • 饿汉模式
    • 懒汉模式
    • 线程安全问题

今天忙活了一天写了一个线程池,写完我才发现单例模式的重要性🤗,做如下学习记录

设计一个类——不能被拷贝

  • 解决思路一:在C++11之前的C++98,我们可以把拷贝构造函数和赋值重载函数定义成private,这样外部就无法拷贝了
class A
{
private:A(const A& x) ;A& operator=(const A& x);
};
  • 在C++11之后引入了关键字delete 我们可以直接将拷贝构造和赋值重载函数删除
class A
{
public:A(const A& x) = delete;A& operator=(const A& x) = delete;
};

设计一个类——只能在堆上开辟空间

首先想明白一个问题:构造函数是否需要删除?

答案是明显的:不能,因为在堆上开辟空间new 需要调用构造函数,如果删除了就无法开辟空间了,我们这里要做的是将构造函数隐藏,并封装一个函数使其只能调用new来构造对象

class A
{
public:static A* heap_only(){return new A;}
private:A(){//....}
};

设计一个类——只能在栈上开辟空间

这个思路就是将operator new 或者将 operator delete 给删除或者放到private作用域中

class A
{
public:void* operator new (size_t a) = delete;void operator delete(void *) = delete;
};

设计一个类——不能被继承

  • 方法一:将基类的构造函数放到private作用域之中,这是因为子类的构造函数一定会调用基类的构造函数,如果基类的构造函数不可见,那么它就无法被继承
  • 方法二:使用关键字final 去修饰

class A final
{//....
};

设计一个类——只能创造一个对象(单例模式)

设计模式:
设计模式我认为是工程文件的一种技巧,是一种被反复使用、多数人知晓、经过分类的、代码经验的总结

单例模式:
一个类只能创建一个对象,这就是单例模式。单例模式的类保证系统中该类只有一个实例,并可以被所有模块访问这一个实例。

饿汉模式

我想先看饿汉模式的设计:👇

class A
{
public:static A& gey_singleton(){return singleton;}A(const A&) = delete;A& operator=(const A&) = delete;
private:A(){//....}static A singleton;
};
A A::singleton;

有如下几个要点:

  • 首先这个单例对象singleton必须是static对象,这样保证了一个类只有一个对象
  • 其次构造函数必须设置成为私有,这样类外无法构造出对象,但是类的成员singleton 能构造出对象
  • 最后我们建立一个接口函数singleton使得类外部能够拿到这个单例对象,但是这个接口函数必须设计成静态成员函数,如果不是静态的就会造成——因为你想调用这个成员函数就必须有这个类的对象,如果你像要得到这个类的对象就必须调用这个类的成员函数😅。所以我们这里定义成静态成员变量直接使用类域就可以调用
  • 拷贝构造和负值重载函数要删除

懒汉模式

class A
{
public:static A* get_singleton(){if (singleton == nullptr)singleton = new A;return singleton;}//防止拷贝A(const A&) = delete;A& operator=(const A&) = delete;
private:A(){//..}static A* singleton;
};
A* A::singleton = nullptr;

懒汉模式的设计思路与饿汉模式设计思路大体上是一致的,唯一区别就是懒汉模式的单例对象singleton是一个指针,而饿汉模式的单例对象singleton是一个对象。

区别
首先补充一点知识:静态和全局数据在编译链接之后的可执行文件之中就是存在的,而我们堆栈则是在运行的时候才生成的。可执行文件运行的时候要经历一步加载,由加载器来完成该步骤——也就是创立进程的结构、建立虚拟内存到物理内存的映射。如果一个可执行文件越大,加载的速度也就越慢。

  • 所以饿汉模式在进程加载完之后就已经实例化出了单例对象,
    • 如果单例对象空间大,会导致程序启动缓慢
    • 如果你的进程在后半段才会使用单例对象,但是进程刚开始就初始化好了单例对象,实际上是一种资源的浪费
  • 但是懒汉模式也有优点那就是简单

线程安全问题

上面的代码实际上还是有一些不足的:
以饿汉模式为例,单例模式算是一个公共资源,所有模块都能访问就会有线程安全问题,所以我们要设计一个线程安全的单例模式

class A
{
public:static A* get_singleton(){//双判断可以避免单例已经被创建,但是任然有多个线程申请锁、判断、释放锁的无用开销if (singleton == nullptr){m.lock();if (singleton == nullptr)singleton = new A;m.unlock();}return singleton;}A(const A&) = delete;A& operator=(const A&) = delete;
private:A(){//..}static A* singleton;static mutex m;
};
A* A::singleton = nullptr;