简单说,一个类只能创建一个唯一的实例就叫单例模式
我们在创建一个类的时候,需要将拷贝构造函数禁用掉
单例模式分成两种模式----饿汉式和懒汉式
饿汉式----在创建类的时候就将其实例创建出来了,后面需要的时候直接用
// 饿汉模式---在创建类的时候就将实例初始化了
class taskqueue {
public:
// 禁用构造函数和拷贝构造函数
//taskqueue() = delete; // 禁用构造函数
taskqueue(const taskqueue& t) = delete; // 禁用拷贝构造函数
taskqueue& operator=(const taskqueue& t) = delete; // 禁用拷贝构造函数
static taskqueue* getinstance() { // 类特有的,不是对象特有
return m_task;
}
void print() {
cout << "这是单例模式。。。" << endl;
}
void printInstanceAddr() {
// 非静态函数访问静态成员变量instance
cout << "当前单例实例地址:" << m_task << endl;
}
private:
// 设置成私有的,外部一样访问不到(另一种方式)
taskqueue() = default; // 类的外部不能访问,类的外部不能创建对象了
//taskqueue(const taskqueue& t) = default;
//taskqueue& operator=(const taskqueue& t) = default;
// 只能通过类名访问静态属性或方法
// 静态的成员变量是不能在类的内部进行初始化的
static taskqueue* m_task; // 类特有的,不是对象特有的
};
taskqueue* taskqueue::m_task = new taskqueue; // 构造了唯一的一个实例对象
懒汉式—在类创建的时候不会将实例创建出来,而是等到用的时候再去创建
// 懒汉模式----什么时候用实例对象,什么时候创建
class taskqueue {
public:
// 禁用构造函数和拷贝构造函数
//taskqueue() = delete; // 禁用构造函数
taskqueue(const taskqueue& t) = delete; // 禁用拷贝构造函数
taskqueue& operator=(const taskqueue& t) = delete; // 禁用拷贝构造函数
static taskqueue* getinstance() { // 类特有的,不是对象特有
if (m_task == nullptr) {
m_task = new taskqueue;
}
return m_task;
}
void print() {
cout << "这是单例模式。。。" << endl;
}
void printInstanceAddr() {
// 非静态函数访问静态成员变量instance
cout << "当前单例实例地址:" << m_task << endl;
}
private:
// 设置成私有的,外部一样访问不到
taskqueue() = default; // 类的外部不能访问,类的外部不能创建对象了
//taskqueue(const taskqueue& t) = default;
//taskqueue& operator=(const taskqueue& t) = default;
// 只能通过类名访问静态属性或方法
// 静态的成员变量是不能在类的内部进行初始化的
static taskqueue* m_task; // 类特有的,不是对象特有的
};
taskqueue* taskqueue::m_task = nullptr; // 构造了唯一的一个实例对象
如果在单线程的环境下,这两种模式是一样的。
如果运行在多线程环境下,饿汉式不会造成线程安全问题,懒汉式会造成线程安全问题
懒汉式的线程安全问题
例如
static taskqueue* getinstance() { // 类特有的,不是对象特有
if (m_task == nullptr) {
m_task = new taskqueue;
}
return m_task;
}
问题:有几个线程同时运行这段代码,大家都发现指针为空,则大家都会创建一个新的对象,则会导致出现线程安全问题
解决的方法:
1、双重检查锁定
static taskqueue* getinstance() { // 类特有的,不是对象特有
if(m_task == nullptr)
{
m_mutex.lock();
if (m_task == nullptr) {
m_task = new taskqueue;
}
m_mutex.unlock();
}
return m_task;
}
只有第一批线程会进行争抢锁资源进行加锁解锁处理,往后的线程会直接返回
2、上面那种也会出现问题,实际上 m_taskQ = new TaskQueue; 在执行过程中对应的机器指令可能会被重新排序,所以需要加上用原子操作解决。
3、使用静态局部对象(只有c++11版本之后才可以用)
class TaskQueue
{
public:
// = delete 代表函数禁用, 也可以将其访问权限设置为私有
TaskQueue(const TaskQueue& obj) = delete;
TaskQueue& operator=(const TaskQueue& obj) = delete;
static TaskQueue* getInstance()
{
static TaskQueue taskQ;//静态局部队列对象,并且将这个对象作为了唯一的单例实例。
return &taskQ;
}
void print()
{
cout << "hello, world!!!" << endl;
}
private:
TaskQueue() = default;
};
int main()
{
TaskQueue* queue = TaskQueue::getInstance();
queue->print();
return 0;
}
为什么静态局部对象是可以的?
运行一段代码需要几个区域共同协作:
全局数据区,栈区,堆区,代码区,常量区;
C++11 标准明确规定:局部静态变量的初始化会在第一次个访问它的线程中完成,且初始化过程是线程安全的 —— 即多个线程同时访问时,只会有一个线程负责初始化,其他线程会阻塞等待初始化完成,且初始化仅执行一次。
当多个线程同时调用 getInstance() 时,C++11 编译器会自动为初始化过程添加隐式锁:
第一个到达的线程会执行初始化逻辑。
其他线程会在初始化完成前被阻塞,等待初始化结束。
初始化完成后,所有线程访问的都是同一个已初始化的实例。
static修饰的变量是存储在全局数据区的,全局数据区分为“显式初始化”和“未显式初始化”
显式初始化:例如:int a = 2;
未显示初始化:例如:int a; int a = 0;(因为我们默认是0,所以系统初始化为0)
所以static TaskQueue taskQ;存放在全局数据区,未显示初始化,系统就会将其“内存初始化”。
一、第一步:系统(操作系统 / 运行时)完成 “内存初始化”
全局数据区(静态存储区)的所有变量,在程序加载到内存时,操作系统或运行时环境会先执行 “零值初始化”—— 将变量所在的内存空间清零(比如指针置为 nullptr、数值类型置为 0、类对象的内存区域初始化为全 0 字节)。
这一步是系统自动完成的,无论变量是基本类型还是类类型,只要存放在全局数据区,都会经历这个过程。对于 taskQ 来说,系统会先为其分配一块符合 TaskQueue 类大小的内存,并将这块内存的所有字节初始化为 0,确保内存没有 “垃圾值”。
二、第二步:编译器确保 “类构造函数执行”(完成对象初始化)
仅完成内存清零还不够 —— 类对象的初始化核心是执行构造函数(初始化对象的成员、建立对象的合法状态)。这一步由编译器通过代码逻辑保证,具体规则如下:
初始化时机:静态局部变量 taskQ 不会在程序启动时立即执行构造函数,而是在首次调用 getInstance() 函数时才触发构造(符合懒汉模式 “延迟初始化” 的需求)。
构造函数调用:由于在 TaskQueue 类中声明了 TaskQueue() = default;(编译器生成默认构造函数),当首次访问 taskQ 时,编译器会自动插入代码调用这个默认构造函数,完成 taskQ 对象的初始化(即使类中没有成员变量,构造函数也会正常执行,标志对象 “合法创建”)。
线程安全保证(C++11+):编译器会为这个 “首次初始化” 过程添加隐式锁,确保多线程环境下只有一个线程执行构造函数,避免重复初始化 —— 这也是编译器层面对静态局部变量初始化的保障。
2458

被折叠的 条评论
为什么被折叠?



