C++ 单例模式

news/2024/7/24 11:07:15 标签: c++, 单例模式, 开发语言

文章目录

  • 一、简介
    • 1.1 简介
    • 1.2 特性
  • 二、多线程单例模式
    • 2.1 简介
    • 2.2 饿汉式单例模式
      • 2.2.1 c++11
      • 2.2.2 c++11以前
    • 2.3 懒汉式单例模式
    • 2.4 双重检查锁定(Double-Checked Locking)单例模式
    • 2.5 使用 std::call_once() 函数
  • 总结

一、简介

1.1 简介

在 C++ 中,单例模式是一种常用的设计模式,它可以确保某个类只有一个实例,并且提供一个全局访问点。

在 C++ 中,可以使用静态成员变量来实现单例模式。下面是一种常见的实现方式:

备注:静态局部变量是指在函数内部定义的静态变量。与普通的局部变量不同,静态局部变量只会被初始化一次,并且只能在定义它的函数内部访问。其生命周期是伴随着整个程序的运行周期,但其作用域只是在该函数内部。

以下是一个示例:

class Singleton {
public:
  static Singleton& getInstance() {
    static Singleton instance;  // 使用静态局部变量保证只会初始化一次
    return instance;
  }

private:
  // 禁止构造函数、拷贝构造函数和赋值运算符的调用
  Singleton() = default;
  Singleton(const Singleton&) = delete;
  Singleton& operator=(const Singleton&) = delete;

  // 成员变量和成员函数
};

在这个实现中,Singleton 类只有一个公有的静态成员函数 getInstance(),该函数返回一个 Singleton 类型的引用,并保证只会初始化一次。这里使用了静态局部变量来实现,在程序第一次调用该函数时会创建一个 Singleton 类的实例,之后每次调用都会返回该实例的引用。

使用时,可以通过Singleton::getInstance()来获取Singleton类的唯一实例,例如:

Singleton& s1 = Singleton::GetInstance();

为了保证单例模式的正确性,需要禁止外部通过构造函数、拷贝构造函数和赋值运算符来创建和复制对象,因此在 Singleton 类的私有部分声明了一个私有的默认构造函数,以及一个删除的拷贝构造函数和赋值运算符。

需要注意的是,虽然使用静态局部变量可以保证单例模式的正确性和线程安全性,但是在多线程环境下,还需要考虑其他线程安全问题,例如使用互斥锁或原子操作来保证线程安全。

在 C++11 中以及后,局部静态变量的初始化是线程安全的。

一个完整的示例,可以使用静态成员变量来实现单例模式

#include <mutex>
#include <thread>
#include <cassert>

class Singleton {
public:
    //获取单例对象的引用
    static Singleton& GetInstance() {
        //使用了一个静态局部变量 instance 来存储单例对象,这保证了它只被初始化一次
        //这个局部变量是在第一次调用 GetInstance() 函数时初始化的
        //在 C++11 中,局部静态变量的初始化是线程安全的
        static Singleton instance;

        return instance;
    }

private:
	//将构造函数和析构函数声明为私有的,可以防止从外部创建新的实例或销毁已有的实例
    Singleton() = default;
    ~Singleton() = default;

	//使用 delete 关键字禁止拷贝构造函数和赋值运算符重载可以防止拷贝实例
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

};


int main() {
    
    //GetInstance() 函数返回单例对象的引用
    Singleton& s1 = Singleton::GetInstance();
    Singleton& s2 = Singleton::GetInstance();

    //assert() 函数用于在程序运行时检查一个表达式是否为 true,
    //如果表达式为 false,则程序将终止运行,并输出一条错误信息。
    assert(&s1 == &s2);

    return 0;
}

将构造函数和析构函数声明为私有的,可以防止从外部创建新的实例或销毁已有的实例。
使用 delete 关键字禁止拷贝构造函数和赋值运算符重载可以防止拷贝实例。

备注:在多线程环境下,需要考虑到线程安全问题。我们可以使用锁或者其他的同步机制来保证多线程下的安全性。

1.2 特性

(1)构造函数和析构函数的可见性

在 C++ 中,如果将构造函数和析构函数声明为私有的,那么只有类中的其他成员函数和友元才能够调用它们。因此,单例模式的构造函数和析构函数应该声明为私有的,以防止外部代码对其进行实例化或销毁。例如:

class Singleton {
private:
    Singleton() {} // private constructor
    ~Singleton() {} // private destructor
};

(2)拷贝构造函数和拷贝赋值运算符的禁用
由于单例模式只允许存在一个实例,因此复制构造函数和赋值运算符都应该被禁用。否则,复制操作将会导致多个实例的存在,破坏单例模式的定义。例如:

class Singleton {
private:
    Singleton(Singleton const&) = delete; // disable copy constructor
    void operator=(Singleton const&) = delete; // disable assignment operator
};

(3)线程安全的实现方法
在多线程环境下,需要确保单例模式的线程安全性。可以采用懒汉式单例模式或双重检查锁定(Double-Checked Locking)单例模式来实现线程安全的单例模式

(4)可以使用工厂模式
在某些情况下,需要在单例模式中创建多个实例,并且每个实例都具有不同的属性。这时候可以使用工厂模式,将单例模式作为工厂类,负责创建和管理多个实例。例如:

class SingletonFactory {
public:
    static Singleton& getInstance(std::string name) {
        static std::map<std::string, Singleton*> instances;
        auto it = instances.find(name);
        if(it == instances.end()) {
            instances[name] = new Singleton(name);
        }
        return *instances[name];
    }

private:
    class Singleton {
    public:
        std::string getName() const {
            return name_;
        }

    private:
        Singleton(std::string name) : name_(name) {}
        std::string name_;
    };

    SingletonFactory() {} // private constructor
    SingletonFactory(SingletonFactory const&) = delete; // disable copy constructor
    void operator=(SingletonFactory const&) = delete; // disable assignment operator
};

在这个示例中,SingletonFactory 类作为工厂类,负责创建和管理多个 Singleton 实例。getInstance() 方法接受一个名字参数,根据不同的名字创建不同的实例。Singleton 类的构造函数接受一个名字参数,用于标识不同的实例。getName() 方法返回实例的名字。

这种方式可以让单例模式更加灵活,可以创建多个不同的实例,并且每个实例都具有不同的属性。但是需要注意,单例模式的目的是确保只有一个实例被创建,因此在使用工厂模式时需要注意不要创建多个实例。

二、多线程单例模式

2.1 简介

C++ 中的单例模式默认情况下是非线程安全的,因为如果在多个线程同时调用 getInstance() 方法时,可能会导致创建多个实例的情况。

为了确保单例模式的线程安全性,可以采用以下几种方法:

(1)饿汉式单例模式:在程序启动时就创建单例对象,这样可以避免多线程同时创建实例的情况。但是这种方式会增加内存的开销,并且可能会导致程序启动变慢。

(2)懒汉式单例模式:在 getInstance() 方法中加锁,确保只有一个线程可以创建实例。但是这种方式会影响程序的性能,因为加锁会增加代码的开销。

(3)双重检查锁定(Double-Checked Locking)单例模式:在 getInstance() 方法中使用双重检查锁定,可以在保证线程安全的同时,避免加锁带来的性能影响。但是这种方式的实现比较复杂,容易出错。

(4)使用 std::call_once() 函数:在 getInstance() 方法中使用 std::call_once() 函数,可以确保只有一个线程可以创建实例,并且避免了加锁带来的性能开销。这种方式相对简单,但是需要 C++11 或以上的编译器支持。

2.2 饿汉式单例模式

在饿汉式单例模式中,单例对象在程序启动时就被创建。这种方式的实现比较简单,代码如下:

2.2.1 c++11

#include <iostream>
#include <mutex>
#include <thread>
#include <vector>

class Singleton {
public:
    //获取单例对象的引用
    static Singleton& GetInstance() {
        //使用了一个静态局部变量 instance 来存储单例对象,这保证了它只被初始化一次
        //这个局部变量是在第一次调用 GetInstance() 函数时初始化的
        static Singleton instance;

        return instance;
    }

private:
    Singleton() = default;
    ~Singleton() = default;

    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

};

void thread_func()
{
    //调用 Singleton::getInstance() 函数来获取单例对象的引用
    Singleton& singleton = Singleton::GetInstance();
    std::cout << "Singleton instance address: " << &singleton << std::endl;
}

int main()
{
    std::vector<std::thread> threads;
    const int num_threads = 10;

    for (int i = 0; i < num_threads; ++i)
    {
        //threads将 `thread_func` 函数作为线程函数,创建多个线程并启动它们:
        threads.emplace_back(thread_func);
    }

    for (auto& t : threads)
    {
        t.join();
    }

    return 0;
}

这种方式的优点是简单易懂,缺点是会增加内存的开销,并且可能会导致程序启动变慢。

C++11 中局部静态变量的初始化和 C++11 以前的版本是有区别的。

在 C++11 之前,局部静态变量的初始化是不可重入的,也就是说在多线程环境下可能会引发竞态条件。如果一个线程正在初始化一个静态局部变量,而另一个线程同时调用了相同的函数,那么它们都会尝试初始化这个静态局部变量,从而可能导致不可预期的结果。

而在 C++11 中,局部静态变量的初始化是线程安全的,编译器会使用互斥锁来保证只有一个线程可以进行初始化操作,其他线程会被阻塞等待初始化完成。这种实现方式可以保证线程的安全性,避免了多线程环境下的竞态条件问题。

因此,在 C++11 中使用局部静态变量实现饿汉式单例模式是线程安全的,而在 C++11 之前,需要使用其他机制来保证线程安全,比如使用懒汉式单例模式,并使用互斥锁等同步机制来避免竞态条件问题。

2.2.2 c++11以前

#include <iostream>
#include <pthread.h>

class Singleton {
public:
/*     static Singleton& GetInstance() {
        return instance_;
    } */

        static Singleton& GetInstance() {
        return *instance_;
    }

private:
    Singleton() {}

    //static Singleton instance_;
    static Singleton *instance_;
};

//单例对象在程序启动时就被创建
//Singleton Singleton::instance_;
Singleton *Singleton::instance_  = new Singleton;

void* thread_func(void*) {
    Singleton& singleton = Singleton::GetInstance();
    std::cout << "Singleton instance address: " << &singleton << std::endl;
    return NULL;
}

int main() {
    const int num_threads = 10;
    pthread_t threads[num_threads];

    for (int i = 0; i < num_threads; ++i) {
        pthread_create(&threads[i], NULL, thread_func, NULL);
    }

    for (int i = 0; i < num_threads; ++i) {
        pthread_join(threads[i], NULL);
    }

    return 0;
}

在这个例子中,单例对象在程序启动时就被创建,Singleton 类使用指针 instance_ 存储单例对象,并在 GetInstance() 函数中进行判断,如果单例对象还没有被创建,则创建一个新的对象并返回其引用。在 thread_func() 函数中,调用 Singleton::GetInstance() 函数来获取单例对象的引用,并输出其地址。

需要注意的是,由于使用了指针来存储单例对象,需要注意在程序结束时进行清理,以避免内存泄漏问题。

2.3 懒汉式单例模式

在懒汉式单例模式中,单例对象在第一次被访问时才被创建。这种方式需要在 getInstance() 方法中加锁,以确保只有一个线程可以创建实例。代码如下:

#include <iostream>
#include <mutex>
#include <thread>
#include <vector>

class Singleton {
public:
    static Singleton& GetInstance() {
        std::lock_guard<std::mutex> lock(mutex_);
        if(!instance_) {
                instance_ = new Singleton();
            }
            return *instance_;
        }

private:
    Singleton() {} // private constructor
    Singleton(Singleton const&) = delete; // disable copy constructor
    void operator=(Singleton const&) = delete; // disable assignment operator

    static Singleton* instance_;
    static std::mutex mutex_;
};

Singleton* Singleton::instance_ = nullptr;
std::mutex Singleton::mutex_;

void thread_func()
{
    //调用 Singleton::getInstance() 函数来获取单例对象的引用
    Singleton& singleton = Singleton::GetInstance();
    std::cout << "Singleton instance address: " << &singleton << std::endl;
}

int main()
{
    std::vector<std::thread> threads;
    const int num_threads = 10;

    for (int i = 0; i < num_threads; ++i)
    {
        //threads将 `thread_func` 函数作为线程函数,创建多个线程并启动它们:
        threads.emplace_back(thread_func);
    }

    for (auto& t : threads)
    {
        t.join();
    }

    return 0;
}

在这种实现方式中,我们使用了 std::mutex 类型的互斥锁来保证线程安全性。在 getInstance() 方法中,首先使用 std::lock_guard和std::mutex 对互斥锁进行加锁,然后检查是否已经创建了实例,如果没有,则在加锁的情况下创建实例。最后返回实例的引用。

这种方式的优点是比较简单易懂,缺点是加锁会影响程序的性能,尤其是在高并发的场景下。

其中 class 静态成员变量:
在 C++ 中,类的静态成员变量是被所有类对象所共享的。静态成员变量与类的成员函数一样,不属于任何一个类对象,而是属于整个类,在内存中只有一份存储空间,因此可以通过类名来访问它们。

静态成员变量可以在类的内部声明,但不能在类的内部初始化,必须在类的外部进行定义和初始化。定义和初始化的方式与普通的全局变量类似,形式为 类型名 类名::变量名 = 初始值;。例如:

class MyClass {
public:
    static int count;
};

int MyClass::count = 0;

在这个例子中,类 MyClass 声明了一个名为 count 的静态成员变量,类型为 int。静态成员变量的初始化是在类的外部进行的,初始化方式为 int MyClass::count = 0;。

静态成员变量的访问方式与普通的成员变量不同,需要使用类名和作用域解析运算符 :: 来访问。例如:

std::cout << MyClass::count << std::endl;

静态成员变量的主要特点如下:

(1)静态成员变量属于整个类,而不是某个类对象;

(2)静态成员变量在内存中只有一份存储空间,所有类对象共享这份存储空间;

(3)静态成员变量必须在类的外部进行定义和初始化;

(4)静态成员变量可以通过类名和作用域解析运算符来访问。

静态成员变量的作用:
(1)共享数据:静态成员变量可以用来存储所有对象共享的数据,这些数据可以在类的各个对象之间共享,而不必为每个对象单独分配存储空间。这样可以节省内存空间,并提高程序的效率。

(2)维护类的状态:静态成员变量可以用来维护类的状态,例如记录类的实例数、全局的计数器等。这些状态可以在所有类对象之间共享,而不必为每个对象单独维护状态。

(3)常量数据:静态成员变量可以用来存储常量数据,例如 PI 常量、常量数组等。这些数据可以在类的所有对象之间共享,而且不会被修改。

(4)全局变量:静态成员变量可以在类中实现全局变量的功能。由于静态成员变量只有一个存储空间,所以可以用来在类中实现全局变量的功能,而不必在程序中定义全局变量。

总的来说,类的静态成员变量可以用来实现数据共享、状态维护、常量数据存储和全局变量等功能,它们在面向对象编程中具有重要的作用。需要注意的是,在使用静态成员变量时,需要注意它们的生命周期和作用域,以避免出现不必要的问题。

2.4 双重检查锁定(Double-Checked Locking)单例模式

双重检查锁定(Double-Checked Locking)单例模式是一种比较常用的线程安全的单例模式实现方式。它可以在保证线程安全的同时,避免加锁带来的性能开销。代码如下:

#include <iostream>
#include <mutex>
#include <thread>
#include <vector>

class Singleton {
public:
    static Singleton& GetInstance() {
        if(instance_ != nullptr) {
            std::lock_guard<std::mutex> lock(mutex_);
            if(instance_ != nullptr) {
                instance_ = new Singleton();
            }
        }
        return *instance_;
    }

private:
    Singleton() {} // private constructor
    Singleton(Singleton const&) = delete; // disable copy constructor
    void operator=(Singleton const&) = delete; // disable assignment operator

    static Singleton* instance_;
    static std::mutex mutex_;
};

Singleton* Singleton::instance_ = nullptr;
std::mutex Singleton::mutex_;

void thread_func()
{
    //调用 Singleton::getInstance() 函数来获取单例对象的引用
    Singleton& singleton = Singleton::GetInstance();
    std::cout << "Singleton instance address: " << &singleton << std::endl;
}

int main()
{
    std::vector<std::thread> threads;
    const int num_threads = 10;

    for (int i = 0; i < num_threads; ++i)
    {
        //threads将 `thread_func` 函数作为线程函数,创建多个线程并启动它们:
        threads.emplace_back(thread_func);
    }

    for (auto& t : threads)
    {
        t.join();
    }

    return 0;
}

在这种实现方式中,getInstance() 方法首先检查是否已经创建了实例,如果没有,则在加锁的情况下再次检查实例是否已经被创建。如果未被创建,则在加锁的情况下创建实例。最后返回实例的引用。

这种方式的优点是比较高效,缺点是实现比较复杂,容易出错。

2.5 使用 std::call_once() 函数

std::call_once() 函数是 C++11 中提供的一种线程安全的方式来实现单例模式。这种方式相对简单,可以保证线程安全,并且不会带来加锁的性能开销。

#include <iostream>
#include <thread>
#include <vector>
#include <mutex>

class Singleton
{
public:
    //使用了 std::call_once 函数,因此在多个线程同时调用时,只有一个线程会创建单例对象 instance_,即只有一个线程执行函数init()
    //其他线程会直接返回之前创建的单例对象 instance_,从而保证单例对象只被创建一次
    static Singleton& getInstance()
    {
        std::call_once(flag_, &Singleton::init);
        return *instance_;
    }

    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

private:
    Singleton() { std::cout << "Singleton instance created.\n"; }

    static void init()
    {
        instance_ = new Singleton();
    }

    //在类的定义中,一个静态成员变量必须由该类声明为static
    //并且通常还需要在类外初始化,这意味着在类的定义中仅指定其类型和名称
    static std::once_flag flag_;
    static Singleton* instance_;
};

//在 class 外初始化 static 成员变量
std::once_flag Singleton::flag_;
Singleton* Singleton::instance_ = nullptr;

void thread_func()
{
    //调用 Singleton::getInstance() 函数来获取单例对象的引用
    Singleton& singleton = Singleton::getInstance();
    std::cout << "Singleton instance address: " << &singleton << "\n";
}

int main()
{
    std::vector<std::thread> threads;
    const int num_threads = 10;

    for (int i = 0; i < num_threads; ++i)
    {
        //threads将 `thread_func` 函数作为线程函数,创建多个线程并启动它们:
        threads.emplace_back(thread_func);
    }

    for (auto& t : threads)
    {
        t.join();
    }

    return 0;
}

在这种实现方式中,getInstance() 方法使用 std::call_once() 函数来确保 init() 方法只会被调用一次。在 init() 方法中,创建了单例对象的实例。最后返回实例的引用。

总结

代码参考 chatgpt


http://www.niftyadmin.cn/n/354572.html

相关文章

【NovelAI 小说SD批量生成 视频克隆】环境配置和使用方法

【样品】《我在东北立堂口》图生图半自动版SD一键成片 操作步骤&环境配置地址&#xff1a; 【NovelAI】月产10000全自动批量原创小说短视频支持文生图和视频克隆 该文章面向购买脚本的付费用户&#xff0c;提供所有问题以及解决办法。使用 notepad 打开对应的文件即可&…

Allegro模块化布局常用技巧(group)

利用group功能&#xff0c;可以方便的进行快速的布局调整。 在placement模式下&#xff0c; 通常&#xff0c;是在板框外&#xff0c;将某个单元模块的电路&#xff0c;进行模块内布局后&#xff0c; 再框选为一个temp group&#xff0c; 或者手工进行temp group创建&#xff…

【医学图像】图像分割系列.1

医学图像分割是一个比较有应用意义的方向&#xff0c;本文简单介绍三篇关于医学图像分割的论文&#xff1a; UNeXt&#xff08;MICCAI2022&#xff09;&#xff0c;PHTrans&#xff08;MICCAI2022&#xff09;&#xff0c;DA-Net&#xff08;MICCAI2022&#xff09;。 目录 …

深度学习基础入门篇-序列模型[11]:循环神经网络 RNN、长短时记忆网络LSTM、门控循环单元GRU原理和应用详解

【深度学习入门到进阶】必看系列&#xff0c;含激活函数、优化策略、损失函数、模型调优、归一化算法、卷积模型、序列模型、预训练模型、对抗神经网络等 专栏详细介绍&#xff1a;【深度学习入门到进阶】必看系列&#xff0c;含激活函数、优化策略、损失函数、模型调优、归一化…

【国产虚拟仪器】基于 ZYNQ 的电能质量系统高速数据采集系统设计

随着电网中非线性负荷用户的不断增加 &#xff0c; 电能质量问题日益严重 。 高精度数据采集系统能够为电能质 量分析提供准确的数据支持 &#xff0c; 是解决电能质量问题的关键依据 。 通过对比现有高速采集系统的设计方案 &#xff0c; 主 控电路多以 ARM 微控制器搭配…

全志Tina Linux 启动优化

本文转载自全志V853在线文档&#xff1a;https://v853.docs.aw-ol.com/soft/tina_boottime/ Tina Linux 启动优化 启动速度是嵌入式产品一个重要的性能指标&#xff0c;更快的启动速度会让客户有更好的使用体验&#xff0c;在某些方面还会节省能耗&#xff0c;因为可以直接关机…

NetApp 数据存储系统 AFF A 系列的优势及应用行业

AFF A 系列阵列&#xff1a;云集成、性能极强、蓄势待发 需要小幅&#xff08;或大幅&#xff09;提升您的关键业务应用程序的性能吗&#xff1f;我们的 AFF A 系列阵列具备屡获殊荣的速度和响应能力&#xff0c;能满足性能敏感型工作负载的需求 为什么选择 NetApp AFF A 系列…

Android Qcom USB Driver学习(十一)

该系列文章总目录链接与各部分简介&#xff1a; Android Qcom USB Driver学习(零) 基于TI的Firmware Update固件升级的流程分析usb appliction layers的数据 USB Protocol Package ①/② map to check password correct Package Format: Byte[0] Report Id Byte[1] Valid L…