设计模式之单例模式

设计模式之单例模式

单例模式是23种常用设计模式中最简单的设计模式之一,它提供了一种创建对象的方式,确保只有单个对象被创建。这个设计模式主要目的是想在整个系统中只能出现类的一个实例,即一个类只有一个对象。

代码实现

单例模式
#include <string.h>
#include <iostream>
using std::cout;
using std::endl;
class Computer{
public:
    static Computer* getInstance(){
        if(_pInstance == nullptr){
            _pInstance = new Computer("Lenovo",10000);
        }
        return _pInstance;
    }
    static void destory(){
        if(_pInstance){
            delete _pInstance;
            _pInstance = nullptr;
        }
    }
    void init(const char* brand, int price){
        if(_brand){
            delete [] _brand;
            _brand = nullptr;
        }
        _brand = new char[strlen(brand)+1]();
        strcpy(_brand,brand);
        _price = price;
    }
    void print(){
        cout << "brand:" << _brand << ", price:" <<_price << endl; 
    }
private:
    Computer() = default;
    Computer(const char* brand, int price)
        :_brand(new char[strlen(brand)+1]()),
        _price(price){
            strcpy(_brand,brand);    
        }
    ~Computer(){
        if(_brand){
            delete [] _brand;
            _brand = nullptr;
        }
    }
    Computer(const Computer & rhs) = delete;
    Computer & operator=(const Computer& rhs) = delete;
private:
    char* _brand;
    int _price;
    static Computer* _pInstance;
};
Computer* Computer::_pInstance = nullptr;
// 单元测试
void test(){
    Computer::getInstance()->print();
    Computer::getInstance()->init("Apple",20000);
    Computer::getInstance()->print();
    Computer::destory();
}
int main()
{
    test();
    return 0;
}

应用场景

1、有频繁实例化然后销毁的情况,也就是频繁的 new 对象,可以考虑单例模式;

2、创建对象时耗时过多或者耗资源过多,但又经常用到的对象;

3、当某个资源需要在整个程序中只有一个实例时,可以使用单例模式进行管理(全局资源管理)。例如数据库连接池、日志记录器等;

4、当需要读取和管理程序配置文件时,可以使用单例模式确保只有一个实例来管理配置文件的读取和写入操作(配置文件管理);

5、在多线程编程中,线程池是一种常见的设计模式。使用单例模式可以确保只有一个线程池实例,方便管理和控制线程的创建和销毁;

6、GUI应用程序中的全局状态管理:在GUI应用程序中,可能需要管理一些全局状态,例如用户信息、应用程序配置等。使用单例模式可以确保全局状态的唯一性和一致性。

单例对象自动释放

现实工作中,单例对象需要进行自动释放。程序在执行的过程中 ,需要判断有哪些地方发生了内存泄漏 ,此时需要工具valgrind的使用来确定。假设单例对象没有进行自动释放 ,那么valgrind工具会认为单例对象是内存泄漏。程序员接下来还得再次去确认到底是不是内存泄漏 ,增加了程序员的额外的工作。

那么如何实现单例对象的自动释放呢?

—— 看到自动就应该想到当对象被销毁时,析构函数会被自动调用。

方式一:利用另一个对象的生命周期管理资源

image-20241126195357119

利用对象的生命周期管理资源析构函数(在析构函数中会执行delete _p),当对象被销毁时会自动调用。

要注意:如果还手动调用了Singleton类的destroy函数,会导致double free问题,所以可以删掉destroy函数,将回收堆上的单例对象的工作完全交给AutoRelease对象

class AutoRelease{
public:
    AutoRelease(Singleton * p)
    : _p(p)
    { cout << " AutoRelease(Singleton*)" << endl; }

    ~AutoRelease(){
        cout << "~AutoRelease()" << endl;
        if(_p){
            delete _p;
            _p = nullptr;
        }
    }
private:
    Singleton * _p;
};

void test0(){
    AutoRelease ar(Singleton:: getInstance());
    Singleton:: getInstance()-> print();
}

方式二:嵌套类 + 静态对象

image-20241126212112160

AutoRelease类对象_ar是Singleton类的对象成员,创建Singleton对象,就会自动创建一个AutoRelease对象(静态区),它的成员函数可以直接访问 _pInstance

class Singleton
{
    class AutoRelease{
    public:
        AutoRelease()
        {}
        ~AutoRelease(){
          //...
        }
    };
    //...
private:
   //...
    int _ix;
    int _iy;
    static Singleton * _pInstance;
    static AutoRelease _ar;
};
Singleton* Singleton::_pInstance = nullptr;
//使用 AutoRelease 类的无参构造对_ar 进行初始化
Singleton:: AutoRelease Singleton::_ar;


void test1(){
    Singleton:: getInstance()-> print();
    Singleton:: getInstance()-> init(10,80);
    Singleton:: getInstance()-> print();
}
image-20240315154821407

程序结束时会自动销毁全局静态区上的_ar,调用AutoRelease的析构函数,在这个析构函数执行delete _pInstance的语句,这样又会调用Singleton的析构函数,再调用operator delete,回收掉堆上的单例对象。

利用嵌套类实现了一个比较完美的方案,不用担心手动调用了destroy函数。

方式三:atexit + destroy

很多时候需要在程序退出的时候做一些诸如释放资源的操作,但程序退出的方式有很多种,比如main()函数运行结束、在程序的某个地方用exit()结束程序、用户通过Ctrl+C操作来终止程序等等,因此需要有一种与程序退出方式无关的方法来进行程序退出时的必要处理。

方法就是用atexit函数来注册程序正常终止时要被调用的函数(C/C++通用)。

如果注册了多个函数,先注册的后执行。

class Singleton
{
public:
    static Singleton * getInstance(){
        if(_pInstance == nullptr){
            atexit(destroy);
            _pInstance = new Singleton(1,2);
        }
        return _pInstance;
    }
    //...
};

atexit注册了destroy函数,相当于有了一次必然会进行的destroy(程序结束时),即使手动调用了destroy,因为安全回收的机制,也不会有问题。

但是还遗留了一个问题,就是以上几种方式都无法解决多线程安全问题。以方式三为例,当多个线程同时进入if语句时,会造成单例对象被创建出多个,但是最终只有一个地址值会由_pInstance指针保存,因此造成内存泄漏。

可以使用饿汉式解决,但同时也可能带来内存压力(即使不用单例对象,也会被创建)

//对于_pInstance 的初始化有两种方式

//饱汉式(懒汉式)—— 懒加载,不使用到该对象,就不会创建
Singleton* Singleton::_pInstance = nullptr; 

//饿汉式 —— 最开始就创建(即使不使用这个单例对象)
Singleton* Singleton::_pInstance = getInstance();

方式四:atexit + pthread_once

Linux平台可以使用的方法(能够保证创建单例对象时的多线程安全)

pthread_once函数可以确保初始化代码只会执行一次, 传给pthread_once函数的第一个参数比较特殊,形式固定,第二个参数需要是一个静态函数指针

image-20241127093539169
class Singleton{   
public:
    static Singleton * getInstance(){
        pthread_once(&_once, init_r);
        return _pInstance;
    }

    static void init_r(){
        _pInstance = new Singleton(1,2);
        atexit(destroy);
    }
    //...
private:
	int _ix;
    int _iy;
    static Singleton * _pInstance;
    static pthread_once_t _once;
};
Singleton* Singleton::_pInstance = nullptr;
pthread_once_t Singleton::_once = PTHREAD_ONCE_INIT;

注意:如果 destroy 是公有的,外部代码可以随意调用,可能导致单例对象被销毁后又尝试使用的情况,造成未定义行为。同时因为会使用atexit注册destroy函数实现资源回收,所以也不能将destroy删掉,应该将其私有,避免在类外手动调用。

posted @   Invinc-Z  阅读(23)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 零经验选手,Compose 一天开发一款小游戏!
· AI Agent开发,如何调用三方的API Function,是通过提示词来发起调用的吗
点击右上角即可分享
微信分享提示