更好的阅读体验:【前往作者个人站点 - AriesfunのBlog】
文章内容若有疑问或错误,欢迎交流、指正,互相学习哈。
推荐一个讲解单例视频,整体概述比较全面:【视频 C++单例模式:从设计到实现】
工作具体项目中的应用,可以看看这个视频,讲了单例的各种用法:【视频 C++单例模式】
下面是我自己从上面视频中的整理的一些讲解的理解和代码实现,主要是懒汉式单例模式。
通过单例模式的方法创建的类在当前进程中有且仅有一个实例。单例模式,属于创建类型的一种常用的软件设计模式。
具体细分:
懒汉式单例,会提供一个创建对象的方法(需要使用时创建)
饿汉式单例,在类加载的时候就创建对象(程序运行时创建,比较着急)
共同点:
01.要声明一个静态的类引用变量
02.类的构造函数要私有
03.提供一个公有的创建对象的方法,能够全局访问
区别:
懒汉式单例是在方法调用时创建对象,而饿汉式是在类加载是创建对象;
多线程情况下,懒汉式单例存在线程安全问题,饿汉式不存在线程安全问题。
对程序运行期间对全局唯一资源的统一访问。
比如,配置管理、日志记录、线程池、连接池、内存池、对象池、消息队列等。
自己最近学习的tcp服务端,就用到了单例模式(懒汉式),这种写法是使用局部静态变量方式(与进程生命周期同步);
可以避免进行释放内存操作,这种单例模式的写法比较推荐;
这是因为静态局部变量在C++11标准之后的编译器实现中会进行线程安全的初始化,保证局部变量初始化严格发生一次。
在初始化过程中,编译器会确保只有一个线程能够执行该静态局部变量的初始化代码,从而避免了多线程竞争的问题。
具体的说明,可以看这篇文档,【文档 静态局部变量】
//通过静态成员变量实现单例
//懒汉式 (多数情况下,这种单例模式是安全的)
class Singleton
{
private:
Singleton(){}
Singleton(const Singleton &) = delete;
Singleton &operator=(const Singleton &) = delete;
public:
static Singleton &GetInst()
{
static Singleton single;
return single;
}
};
具体项目中应用的例子,
在大的实际项目中可能有多个类用到了单例,不可能把他们单独放在一起集中来初始化,所以多用懒汉式单例,具体使用时来创建单例。
实现步骤
- step1.将类的构造方法定义为私有。
- step2.定义一个私有类的静态实例。
- step3.提供一个公有的获取实例的静态方法。
涉及的知识点
static静态成员变量,fiend友元类,template模板类
懒汉式单例,比较常用
代码实现:a.h
#pragma once
#include
#include
class A{
private: // step1.让类的构造函数私有(或受保护)
A() : m_name("A") {}
A(const A &other) {}
~A() {}
A& operator = (const A &other);
private: // step2.定义一个私有静态类的实例
static A* m_instance;
std::string m_name;
public: // step3.提供公有的获取实例的静态方法
static A* instance() {
if(m_instance == nullptr) {
m_instance = new A();
}
return m_instance;
}
void show() {
std::cout << "m_name: " << m_name << std::endl;
}
};
A* A::m_instance = nullptr; // 静态变量初始化
主程序,main.cpp
#include
#include "a.h"
// #include "b.h" // 可仿照"a.h"可以写一个B类来实现单例模式
/*
单例模式:保证全局有且仅有一个实例
*/
int main() {
// 编写两个头文件让A类,B类都实现单例
A* a1 = A::instance();
a1->show();
cout << "a1: " << a1 << endl;
auto a2 = A::instance();
a2->show();
cout << "a2: " << a2 << endl; // a1,a2都指向同一地址(表示是同一实例)
return 0;
}
代码运行结果,
m_name: A
a1: 0x55b10376d270
m_name: A
a2: 0x55b10376d270
饿汉式单例(不常用),是线程安全的
代码实现:Singleton.h
# pragma once
class Singleton {
private:
Singleton() {} // step1.将类的构造方法定义为私有。
// 定义一个私有类的静态实例。(饿汉式单例类的实例会在类被加载时就创建好,多线程下是安全的)
static Singleton* instance;
public:
static Singleton* getSingleton() { // 提供一个公有的获取实例的静态方法。
return instance;
}
};
Singleton* Singleton::instance = new Singleton(); // 在类外部进行初始化,确保了实例只会在程序启动时被创建一次
具体使用,main.cpp
#include
#include "Singleton.h"
int main() {
// 单例模式测试
Singleton* a1 = Singleton::getSingleton();
Singleton* a2 = Singleton::getSingleton();
std::cout << "a1's addr: " << a1 << std::endl;
std::cout << "a2's addr: " << a2 << std::endl;
return 0;
}
运行结果如下,
a1's addr: 0x6c1780
a2's addr: 0x6c1780
定义一个类模板,singleton.h
#pragma once
namespace ariesfun {
namespace utility {
// 使用模板类来实现单例模式
template
class Singleton {
public: // 提供公有访问实例的方法
static T* instance() // 多线程环境下,可能会出安全问题
{
if(m_instance == nullptr) {
m_instance = new T(); // 这里要访问对应私有类的构造函数,需要将当前类在类A中声明为友元类
}
return m_instance; // 返回全局实例的指针
}
private:
Singleton() {}
Singleton(const Singleton &another);
~Singleton() {}
Singleton& operator = (const Singleton &another);
private:
static T* m_instance;
};
// 注意模板类的static成员要放在.h文件里初始化
template
T* Singleton::m_instance = nullptr; // 模板类的静态成员初始化
}
}
改写a.h
#pragma once
#include
#include
#include "singleton.h" // 引入头文件和对应作用域
using namespace ariesfun::utility;
class A{
private: // step1.让类的构造函数私有(或受保护)
A() : m_name("A") {}
A(const A &other) {}
~A() {}
A& operator = (const A &other);
private: // step2.定义一个私有静态类的实例
// static A* m_instance; (在类模板中已经实现)
std::string m_name;
public: // step3.提供公有的获取实例的静态方法
// static A* instance() {...} (在类模板中已经实现)
void show() {
std::cout << "m_name: " << m_name << std::endl;
}
friend class Singleton; // 让Singleton能访问A的私有成员
};
// A* A::m_instance = nullptr; // 静态变量初始化
新的main.cpp
#include
#include "a.h"
// #include "b.h"
using namespace std;
#include "singleton.h"
using namespace ariesfun::utility;
int main() {
// 编写类模板实现单例
auto sa1 = Singleton::instance();
sa1->show();
cout << "sa1: " << sa1 << endl;
auto sa2 = Singleton::instance();
sa2->show();
cout << "sa2: " << sa2 << endl;
return 0;
}
代码运行结果,
m_name: A
sa1: 0x5568f0b07270
m_name: A
sa2: 0x5568f0b07270
你可能见到过有些官方源码还有其他人,写法是宏定义或者在使用默认构造、拷贝、赋值后,加上了一些关键字(defalut,delete),这是一种优化的写法。
比如,
#define SINGLETON(classname) \
friend class Singleton; \
private: \
classname() = default; \ // 使用默认构造
classname(const classname &) = delete; \ // 禁用拷贝构造
classname & operator = (const classname &) = delete; \ // 禁用赋值操作符
~classname() = default // 使用默认析构
(1).private:
: 这是一个访问权限标识符,表示以下成员都将是私有的,只能在类的内部访问。
(2).classname() = default;
: 表示使用编译器生成默认的构造函数实现。在单例模式中,通常会将构造函数设为私有,以防止从外部直接创建类的实例。
(3).classname(const classname &) = delete;
: 使用 = delete
表示禁用拷贝构造函数。这样做是为了防止通过拷贝构造函数创建多个实例,从而维护单例的唯一性。
(4).classname & operator=(const classname &) = delete;
: 使用 = delete
表示禁用赋值操作符。这也是为了避免通过赋值操作创建多个实例。
(5).~classname() = default;
: 使用 = default
表示使用编译器生成默认的析构函数实现。在单例模式中,通常不需要特殊的析构函数逻辑。
这段代码的目的是创建一个单例模式的类,其中通过私有化构造函数、拷贝构造函数和赋值操作符,以及声明友元类,来确保只有 Singleton
类能够创建和管理classname
类的唯一实例。
使用 = default
和 = delete
是一种简洁的方式来指定默认的函数实现或禁用特定的函数。
最佳解决是直接规避掉这个问题,而不是找方法去解决这个问题(加锁等)。 乱码哥牛啊,哈哈
其他加锁的方法,实际有需求或遇到线程安全问题,可以参考一下这篇博客介绍的解决方案:【文档 再谈单例模式】
乱码哥建议,可在main函数中这样使用,
// 解决多线程下的安全问题,可以使用如下的方案(来规避安全问题)
// 主线程中 (创建实例)
Singleton::instance();
Singleton::instance();
// 子线程中 (使用)
Singleton::instance()->show();
Singleton::instance()->show();
工厂模式:原来是要自己创建对象,现在是不需要自己创建对象,而是创建这个对象的工厂。
工厂模式是我们最常用的实例化对象模式了,是用工厂方法代替new操作的一种模式。
百度百科-工厂模式就相当于创建实例对象的new,我们经常要根据类class
生成实例对象,
如A a=new A();
工厂模式也是用来创建实例对象的,所以以后new时就要多个心眼,是否可以考虑使用工厂模式,
虽然这样做,可能多做一些工作,但会给你系统带来更大的可扩展性和尽量少的修改量。
工厂模式的主要目的,是封装对象的创建过程,提供更高层次的抽象,降低代码之间的耦合度,使代码更加可扩展和可维护。
让我们来学习一下ChatGpt的回答,讲得倒是很全面。
下面用创建一个车的工厂来举例,
先定义两个父类的头文件,车类和车工厂类,并尝试用CMake管理,采用多文件编写。
项目目录如下,
.
├── CMakeLists.txt
├── docs
├── include
│ ├── Baoma.h
│ ├── BaomaFactory.h
│ ├── Benchi.h
│ ├── BenchiFactory.h
│ ├── Car.h
│ ├── CarFactory.h
│ └── singleton.h # 可以忽略,这是测试单例模式时使用的类
└── src
├── Baoma.cpp
├── BaomaFactory.cpp
├── Benchi.cpp
├── BenchiFactory.cpp
└── main.cpp
Car.cpp
#ifndef FACTORY_PATTERN_CAR_H
#define FACTORY_PATTERN_CAR_H
#include
class Car {
public:
virtual std::string get_name() = 0; // 定义虚函数,让子类来实现
Car() = default;
};
#endif //FACTORY_PATTERN_CAR_H
CarFactory.h
#ifndef FACTORY_PATTERN_CARFACTORY_H
#define FACTORY_PATTERN_CARFACTORY_H
#include "Car.h"
class CarFactory {
public:
virtual Car* getCar() = 0;
};
#endif //FACTORY_PATTERN_CARFACTORY_H
当我们需要创建一个车对象(奔驰车)时,我们需要创建这个车和对于这个车工厂(继承各自父类),当我们要使用这个车时,直接从它工厂里拿就行。
下面以创建奔驰车对象为例,
奔驰车类,Benchi.h
#ifndef FACTORY_PATTERN_BENCHI_H
#define FACTORY_PATTERN_BENCHI_H
#include "Car.h"
class Benchi :public Car {
public:
std::string get_name() override; // 加override,可帮助编译器检查是否正确地重写了父类的虚函数
};
#endif //FACTORY_PATTERN_BENCHI_H
// 在对应的源文件里写函数声明的实现
"Benchi.cpp"
#include "Benchi.h"
std::string Benchi::get_name() {
return "benchi";
}
奔驰车工厂,BenchiFactory.h
#ifndef FACTORY_PATTERN_BENCHIFACTORY_H
#define FACTORY_PATTERN_BENCHIFACTORY_H
#include "CarFactory.h"
class BenchiFactory :public CarFactory {
public:
Car* getCar() override;
};
#endif //FACTORY_PATTERN_BENCHIFACTORY_H
// 在对应的源文件里写函数声明的实现
"BenchiFactory.cpp"
#include "BenchiFactory.h"
#include "Benchi.h"
Car* BenchiFactory::getCar() {
return new Benchi();
}
编写测试类,main.cpp
#include
#include "Car.h"
// #include "Benchi.h"
// #include "BenchiFactory.h"
#include "Baoma.h"
#include "BaomaFactory.h"
int main() {
/*
// 原来创建对象
Car* c = new Benchi();
std::cout << "Car name: " << c->get_name() << std::endl; // Car name: benchi
*/
// 工厂模式测试
// 使用工厂来创建一辆奔驰车
Car* c1 = (new BenchiFactory)->getCar();
std::cout << "Car1 name: " << c1->get_name() << std::endl; // Car1 name: benchi
// 同样,我们使用工厂来创建一辆宝马车(写法与奔驰车类似)
// Car* c2 = (new BaomaFactory)->getCar();
// std::cout << "Car2 name: " << c2->get_name() << std::endl; // Car2 name: baoma
return 0;
}
单例模式:
【文档 单例模式】
【文档 再谈单例模式】
【视频 C++单例模式:从设计到实现】
【视频 C++单例模式】
【视频 C++单例模式总结】
【视频 Java单例设计模式】
工厂模式:
【视频 C/C++项目实战-前置知识】
【仓库代码 SYaoJun/SystemProgramming】