C++ -- 单例模式

简介: **摘要:**单例模式确保一个类仅有一个实例,并提供全局访问点。为了实现单例,构造函数通常设为私有,通过静态成员函数来创建和返回实例。两种常见实现是饿汉模式(在类加载时创建实例,线程安全但可能导致不必要的内存占用)和懒汉模式(首次使用时创建,可能需线程同步)。拷贝构造函数和赋值运算符通常被禁用来防止额外实例的创建。单例模式适用于资源管理、缓存和线程池等场景。在C++中,静态成员变量和函数用于存储和访问单例实例,保证其生命周期与程序相同。

单例模式

认识单例模式之前先认识一下几种常见的特殊类的设计。

特殊类的设计

  • 设计一个类 只能再堆上创建对象

    • 只能再堆上创建,则通过new来创建对象。

      1. 将类的构造函数设为私有,以防止外部直接创建对象。
      2. 提供一个静态成员函数,用于在堆上创建对象,并返回指向该对象的指针
      3. 在类的析构函数中释放对象的内存,以确保对象在不再需要时被正确销毁。
      class HeapOnly {
             
      public:
          // 获取堆上对象的静态成员函数
          static HeapOnly* CreateObj() {
             
              return new HeapOnly();
          }
      
          // 删除拷贝构造函数和赋值运算符,以确保只能通过 create() 创建对象
          HeapOnly(const HeapOnly&) = delete;
          HeapOnly& operator=(const HeapOnly&) = delete;
      
      private:
          // 将构造函数设为私有,以防止外部直接创建对象
          HeapOnly() {
             
              // 可以在这里进行初始化操作
          }
      
          // 在析构函数中释放对象的内存
          ~HeapOnly() {
             
              // 可以在这里进行资源释放操作
          }
      };
      
    • HeapOnly 类的构造函数是私有的,外部无法直接调用。

    • 向外提供的CreateObj方法必须定义为static成员函数。因为外部调用该接口就是为了获取对象的,而非静态成员函数必须通过对象才能调用,静态成员函数直接使用类名::函数名即可调用。

    • 将拷贝构造函数和赋值运算符重载函数之间禁用,也可以设置为私有属性,以防通过拷贝和赋值创建对象。

    • 使用示例:

      int main() {
             
          // 创建堆上的对象
          HeapOnly* obj = HeapOnly::create();
      
          // 使用对象...
      
          // 删除对象
          delete obj;
      
          return 0;
      }
      
  • 设计一个类 只能再栈上创建对象

    • 只能再栈上创建对象,说明不能通过new创建对象,也不能定义为static对象。创建方式如下:

      1. 将类的构造函数设为私有,防止外部直接调用构造函数在堆上创建对象。
      2. 在类中定义一个静态成员函数,用于创建对象,并返回该对象的引用或指针。

      ```cpp
      class StackOnly {
      public:

      // 获取栈上对象的静态成员函数
      static StackOnly& create() {
          // 在静态成员函数中创建对象,返回对象的引用
          static StackOnly instance;
          return instance;
      }
      
      // 删除拷贝构造函数和赋值运算符,以防止通过拷贝或赋值创建对象
      StackOnly(const StackOnly&) = delete;
      StackOnly& operator=(const StackOnly&) = delete;
      
private:
    // 将构造函数设为私有,防止外部直接创建对象
    StackOnly() {
        // 可以在这里进行初始化操作
    }

};
int main() {
    // 创建栈上的对象
    StackOnly& obj = StackOnly::create();
    return 0;
}


```
  • StackOnly类的构造函数私有化,外部无法直接调用。限制了通过new创建对象和static对象。

  • 静态局部对象的引用是存储在栈上的。

  • 将拷贝构造函数和赋值运算符重载函数之间禁用,也可以设置为私有属性,以防通过拷贝和赋值创建对象。

  • 设计一个类 不能被拷贝

    • 将拷贝构造函数和赋值运算符设置为私有,或者直接使用delete禁用即可。
    class NonCopyable {
         
    public:
        NonCopyable() {
         } // 默认构造函数
    
        // 禁用拷贝构造函数和拷贝赋值运算符
        NonCopyable(const NonCopyable&) = delete;
        NonCopyable& operator=(const NonCopyable&) = delete;
    };
    
    int main() {
         
        NonCopyable obj1;
        // NonCopyable obj2 = obj1; // 这行会导致编译错误,因为拷贝构造函数被删除了
        // NonCopyable obj3;
        // obj3 = obj1; // 这行会导致编译错误,因为拷贝赋值运算符被删除了
        return 0;
    }
    
  • 设计一个类 不能被继承

    • 将该类的构造函数设置为私有即可,因为子类对象构造时,必须先调用父类构造,构造父类那一部分成员。将其私有化后,子类对象创建对象时,无法调用父类构造函数进行初始化。

      class NonInherit
      {
             
      public:
          static NonInherit CreateObj()
          {
             
              return NonInherit();
          }
      private:
          //将构造函数设置为私有
          NonInherit()
          {
             }
      };
      
    • 也可以使用C++11提供的关键字final。别final修饰的类,无法被继承

      class NonInherit final
      {
             
          //...
      };
      
  • 设计一个类 只能创建一个对象

    • 一个类只能创建一个对象,将其称之为单例模式。

单例模式

单例模式是一种设计模式(Design Pattern),设计模式就是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式的目的就是为了可重用代码、让代码更容易被他人理解、保证代码可靠性程序的重用性。

单例模式指的就是一个类只能创建一个对象,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。

单例模式有两种实现方式,分别是饿汉模式和懒汉模式

饿汉模式

单例模式的实现方式:

  1. 私有化构造函数:确保其他类无法直接实例化该类,只能通过指定的方法获取类的实例。
  2. 私有静态成员变量:保存类的唯一对象。并在程序入口之前完成单例对象的初始化。
  3. 公有静态方法:提供对唯一实例的访问。
class Singleton
{
   
public:    
    //公有静态方法
    static Singleton* GetInstace()
    {
   
        return _inst;
    }
    //禁用拷贝构造和赋值运算符
    Singleton(const Singleton& s) = delete;
    Singleton& operator=(const Singleton) = delete;
private:
    //私有化构造函数
    Singleton()
    {
   }
    //私有静态成员变量
    static Singleton* _inst;
};
Singleton* Singleton::_inst = new Singleton;//静态成员变量的初始化

int main()
{
   
    //获取单例对象
    Singleton* ojb = Singleton::GetInstace();
}

懒汉模式

懒汉模式的实现方式:

  1. 私有化构造函数:确保其他类无法直接实例化该类,只能通过指定的方法获取类的实例。
  2. 私有静态成员变量:保存类的唯一对象,将其初始化为空。
  3. 公有静态方法:提供对唯一实例的访问。如果实例不存在,则创建一个新实例并返回;如果实例已存在,则返回现有实例
// 懒汉模式
class Singleton
{
   
public:
    //公有静态方法
    static Singleton* GetInstance()
    {
   
        if (_inst == nullptr)
        {
   
            _inst = new Singleton;
        }
        return _inst;
    }
    //禁用拷贝构造和赋值运算符
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
private:
    //私有构造函数
    Singleton()
    {
   }
    //私有静态成员变量
    static Singleton* _inst;
};
Singleton* Singleton::_inst = nullptr;

int main()
{
   
    //获取单例对象
    Singleton* obj = Singleton::GetInstance();
    return 0;
}

懒汉VS饿汉

  • 饿汉
    1. 在类加载时就创建实例。无论是否使用该实例,都会在类加载时被创建。如果这个类很大,会影响程序的启动速度。
    2. 在mian程序加载之前就会被实例化,不存在线程安全问题
    3. 使用简单
  • 懒汉
    1. 在需要时才创建实例。也就是说,实例的创建延迟到第一次使用时才发生。
    2. 在多线程环境下需要考虑线程安全性,特别是在第一次获取实例时,可能会存在多个线程同时创建实例的问题。需要通过加锁或者双重检查锁定等技术来确保线程安全性。
    3. 使用复杂

懒汉的线程安全

上面写的懒汉单例并不是线程安全的,在多线程场景下,存在线程安全的问题。GetInstance函数第一次调用时,需要对_inst进行写入操作,这个操作不是线程安全的,多个线程可能同时调用GetInstance进行写入,如果不对此过程进行加锁保护,多线程下就会各自创建出一个对象。

对于饿汉模式来讲,不存在线程安全的问题,因为在main函数之前就已经创建好对象了。

GetInstance中创建单例对象进行加锁保护。

static Singleton* GetInstance()
{
   
    if (_inst == nullptr)
    {
   
        _mtx.lock();
        if (_inst == nullptr)
        {
   
            _inst = new Singleton;
        }
        _mtx.unlock();
    }
    return _inst;
}

进行双重检查加锁来保证线程安全。双重检查会提高效率,如果是单检查,如下:

static Singleton* GetInstance()
{
   
    _mtx.lock();
    if (_inst == nullptr)
    {
   
        _inst = new Singleton;
    }
    _mtx.unlock();
    return _inst;
}

这样每次访问单例对象时都要进行加锁,加锁之后才能进行判断。双检查之后,只有第一次访问单例对象时才需要加锁。后续访问时无需加锁了。提高了效率。

单例对象的释放

单例对象创建后一般在整个程序运行期间都可能会使用,所以我们可以不考虑单例对象的释放,程序正常结束时会自动将资源归还给操作系统。

有些场景下可能需要提前释放单例对象,可以参考一下方式:

  • 提供一个公有的DelInstance函数,在该函数中进行单例对象释放的操作,当不需要单例对象时,调用此函数进行单例对象的释放。函数如下:
static void DelInstance()
{
   
    _mtx.lock();
    if (_inst != nullptr)
    {
   
        delete _inst;
        _inst = nullptr;
    }
    _mtx.unlock();
}

释放操作只能调用一次,单检查加锁足够了,无需双检查加锁。

注意:单例模式中的单例对象和互斥锁,和提供的公有方法都是static的。

首先要明确static修饰成员变量和成员函数的特征:

  1. 静态成员变量

    • 所有类的对象共享静态成员变量的值。
    • 在类声明中声明为静态成员变量,但在类外部必须在类外进行定义和初始化。
    • 静态成员变量的内存只分配一次,直到程序结束才会被释放。
    • 可以在类外部通过类名和作用域解析运算符(::)来访问静态成员变量。
  2. 静态成员函数

    • 不与任何特定的对象相关联,可以直接通过类名调用。
    • 静态成员函数没有隐含的this指针。
    • 静态成员函数不能访问非静态成员变量和非静态成员函数(除非它们是同一个类中的静态成员)。
    • 由于它们不与特定对象相关联,因此无法在静态成员函数中使用this指针。

在单例模式中,需要将实例保存在静态成员变量中,是因为:

  1. 全局可访问性:静态成员变量属于类而不是对象,因此可以被该类的所有对象访问。这符合单例模式的要求,即只有一个实例,并且可以在任何地方访问到该实例。
  2. 生命周期与类相同:静态成员变量随着类的加载而初始化,而不是随着对象的创建而初始化。这意味着无论是否存在对象,静态成员变量都会在类加载时创建,从而保证了在整个应用程序生命周期内只有一个实例存在。

因此,将单例实例保存在静态成员变量中能够满足单例模式的要求,确保了实例的全局可访问性和唯一性。

相关文章
|
4月前
|
设计模式 安全 测试技术
【C/C++ 设计模式 单例】单例模式的选择策略:何时使用,何时避免
【C/C++ 设计模式 单例】单例模式的选择策略:何时使用,何时避免
120 0
|
4月前
|
设计模式 安全 测试技术
【C++】—— 单例模式详解
【C++】—— 单例模式详解
|
4月前
|
C++
C++实现单例模式-多种方式比较
单例模式,面试中经常被问到,但是很多人只会最简单的单例模型,可能连多线程都没考虑到,本文章从最简单的单例,到认为是最佳的单例模式实现方式,单例模式没有什么知识点,直接上源码
76 0
|
11月前
|
设计模式 安全 Java
特殊类设计及单例模式(C++)
特殊类设计及单例模式(C++)
73 1
|
4月前
|
设计模式 安全 算法
【C++入门到精通】特殊类的设计 | 单例模式 [ C++入门 ]
【C++入门到精通】特殊类的设计 | 单例模式 [ C++入门 ]
34 0
|
30天前
|
安全 C++
C++ QT 单例模式
C++ QT 单例模式
31 0
|
1月前
|
设计模式 安全 IDE
C++从静态类型到单例模式
C++从静态类型到单例模式
26 0
|
2月前
|
设计模式 安全 C++
C++一分钟之-C++中的设计模式:单例模式
【7月更文挑战第13天】单例模式确保类只有一个实例,提供全局访问。C++中的实现涉及线程安全和生命周期管理。基础实现使用静态成员,但在多线程环境下可能导致多个实例。为解决此问题,采用双重检查锁定和`std::mutex`保证安全。使用`std::unique_ptr`管理生命周期,防止析构异常和内存泄漏。理解和正确应用单例模式能提升软件的效率与可维护性。
33 2
|
4月前
|
安全 程序员 C语言
从C语言到C++_37(特殊类设计和C++类型转换)单例模式(下)
从C语言到C++_37(特殊类设计和C++类型转换)单例模式
44 5
|
4月前
|
设计模式 编译器 Linux
从C语言到C++_37(特殊类设计和C++类型转换)单例模式(中)
从C语言到C++_37(特殊类设计和C++类型转换)单例模式
35 0