一、设计模式到底是在解决什么问题
设计模式并不是某种:
固定语法
也不是:
必须照抄的代码模板
它更像是:
针对某类重复出现的软件设计问题,总结出来的一套常见解决思路。
例如:
程序中只希望存在一个配置管理对象
可以考虑:
单例模式
Singleton
如果:
程序需要根据不同条件创建不同对象
可以考虑:
工厂模式
Factory
例如:
创建不同日志对象
FileLogger
ConsoleLogger
NetworkLogger
如果业务代码直接:
FileLogger logger;
那么业务代码就和:
FileLogger
绑定得比较死。
如果以后换成:
NetworkLogger
很多地方可能都需要修改。
工厂模式的思路就是:
业务代码
↓
只告诉工厂
"我要什么类型"
↓
工厂负责创建对象
所以:
单例模式
↓
控制对象数量
工厂模式
↓
控制对象创建方式
这两种模式解决的是完全不同的问题。
二、单例模式:整个程序只创建一个对象
单例模式的核心要求很简单:
1. 构造函数不能让外部随便调用
2. 整个程序只能有一个实例
3. 提供统一接口获取这个实例
例如配置管理器:
ConfigManager config1;
ConfigManager config2;
ConfigManager config3;
如果每个地方都创建一个:
ConfigManager
可能造成:
配置状态不一致
重复加载文件
浪费资源
所以希望整个程序共享:
唯一一个ConfigManager
最基本的写法:
class Singleton {
private:
Singleton() {}
public:
Singleton(const Singleton &) = delete;
Singleton &operator=(const Singleton &) = delete;
static Singleton &getInstance() {
static Singleton instance;
return instance;
}
};
使用:
Singleton &s1 = Singleton::getInstance();
Singleton &s2 = Singleton::getInstance();
此时:
s1
↓
Singleton实例
↑
s2
两个引用实际上指向:
同一个对象
这里最关键的是:
private:
Singleton() {}
把构造函数设为:
private
这样外部不能:
Singleton s;
直接创建对象。
同时:
Singleton(const Singleton &) = delete;
Singleton &operator=(const Singleton &) = delete;
禁止:
拷贝
赋值
否则可能通过:
Singleton s2 = Singleton::getInstance();
再复制出新的对象。
真正负责创建实例的是:
static Singleton &getInstance() {
static Singleton instance;
return instance;
}
这里:
static Singleton instance;
是:
函数内部静态对象
第一次调用:
getInstance()
时创建。
以后再次调用:
不会重新创建
而是直接返回:
同一个instance
这种写法通常叫:
Meyers Singleton
也是现代 C++ 中比较推荐的单例实现方式。
从 C++11 开始,函数局部静态变量的初始化具有线程安全保证,因此多个线程第一次同时调用:
getInstance()
时,不需要自己再额外写双重检查锁来保证初始化只发生一次。
完整代码:
#include <iostream>
class Singleton {
private:
Singleton() {
std::cout << "Singleton构造" << std::endl;
}
~Singleton() {
std::cout << "Singleton析构" << std::endl;
}
public:
Singleton(const Singleton &) = delete;
Singleton &operator=(const Singleton &) = delete;
static Singleton &getInstance() {
static Singleton instance;
return instance;
}
void show() {
std::cout << "Singleton::show()" << std::endl;
}
};
int main() {
Singleton &s1 = Singleton::getInstance();
Singleton &s2 = Singleton::getInstance();
s1.show();
std::cout << &s1 << std::endl;
std::cout << &s2 << std::endl;
return 0;
}
运行时可以看到:
s1地址 == s2地址
说明两次获取的是:
同一个对象
整个过程可以理解成:
第一次getInstance()
↓
创建static instance
↓
返回instance
第二次getInstance()
↓
instance已经存在
↓
直接返回原对象
三、懒汉式、饿汉式和双重检查怎么理解
单例面试经常会继续问:
懒汉式
饿汉式
有什么区别。
1. 懒汉式
所谓懒汉式就是:
需要的时候才创建
例如:
static Singleton &getInstance() {
static Singleton instance;
return instance;
}
第一次调用:
创建对象
如果程序从来没有使用单例:
对象就不会创建
所以叫:
懒加载
2. 饿汉式
饿汉式的思路:
程序开始时就把对象创建好
例如:
class Singleton {
private:
Singleton() {}
static Singleton instance_;
public:
static Singleton &getInstance() {
return instance_;
}
};
Singleton Singleton::instance_;
程序进入主要业务逻辑之前:
instance_
通常已经完成初始化。
可以理解成:
饿汉式
↓
提前创建
懒汉式
↓
第一次使用时创建
3. 双重检查锁
以前还经常看到:
if (instance == nullptr) {
std::lock_guard<std::mutex> lock(mutex);
if (instance == nullptr) {
instance = new Singleton;
}
}
也就是:
Double-Checked Locking
双重检查锁
为什么检查两次?
第一次:
if (instance == nullptr)
是为了:
实例已经创建以后
避免每次都加锁
第二次:
加锁以后再判断一次
是为了防止:
多个线程同时通过第一次判断
例如:
线程A:instance == nullptr
线程B:instance == nullptr
线程 A 先拿锁创建对象。
线程 B 随后拿到锁时必须重新判断:
instance已经不为空
就不能再创建第二个实例。
不过现代 C++ 面试中,如果只是让写线程安全单例,通常优先写:
static Singleton &getInstance() {
static Singleton instance;
return instance;
}
更加简单,而且从 C++11 开始局部静态变量初始化有线程安全保证。
所以面试时可以这样回答:
现代 C++ 中我更倾向使用 Meyers Singleton,通过函数内部的局部静态对象实现懒加载。C++11 起局部静态变量初始化是线程安全的,同时还能利用对象自动析构管理生命周期,比手写双重检查锁更加简单。
四、工厂模式:把对象创建过程封装起来
再来看工厂模式。
假设现在有:
圆形
矩形
三角形
不同图形。
先定义统一接口:
class Shape {
public:
virtual ~Shape() = default;
virtual void draw() = 0;
};
然后分别实现:
class Circle : public Shape {
public:
void draw() override {
std::cout << "Draw Circle" << std::endl;
}
};
class Rectangle : public Shape {
public:
void draw() override {
std::cout << "Draw Rectangle" << std::endl;
}
};
如果没有工厂模式,业务代码可能:
Circle circle;
circle.draw();
或者:
Rectangle rectangle;
rectangle.draw();
这样业务代码必须知道:
到底应该new哪个具体类
如果使用工厂:
class ShapeFactory {
public:
static std::unique_ptr<Shape> create(const std::string &type) {
if (type == "circle") return std::make_unique<Circle>();
if (type == "rectangle") return std::make_unique<Rectangle>();
return nullptr;
}
};
业务代码变成:
auto shape = ShapeFactory::create("circle");
if (shape) {
shape->draw();
}
业务层只需要告诉工厂:
我要circle
至于:
Circle怎么构造
Rectangle怎么构造
由工厂负责。
整个流程:
业务代码
↓
ShapeFactory::create("circle")
↓
Factory判断类型
↓
创建Circle
↓
返回Shape指针
↓
业务代码调用draw()
完整代码:
#include <iostream>
#include <memory>
#include <string>
class Shape {
public:
virtual ~Shape() = default;
virtual void draw() = 0;
};
class Circle : public Shape {
public:
void draw() override {
std::cout << "Draw Circle" << std::endl;
}
};
class Rectangle : public Shape {
public:
void draw() override {
std::cout << "Draw Rectangle" << std::endl;
}
};
class ShapeFactory {
public:
static std::unique_ptr<Shape> create(const std::string &type) {
if (type == "circle") return std::make_unique<Circle>();
if (type == "rectangle") return std::make_unique<Rectangle>();
return nullptr;
}
};
int main() {
auto shape1 = ShapeFactory::create("circle");
auto shape2 = ShapeFactory::create("rectangle");
if (shape1) shape1->draw();
if (shape2) shape2->draw();
return 0;
}
这里工厂返回:
std::unique_ptr<Shape>
而不是:
Circle *
Rectangle *
因为我们希望业务代码依赖:
抽象接口Shape
而不是依赖具体实现。
所以:
Circle
Rectangle
都可以通过:
Shape*
进行多态调用。
这也正好和前面写过的:
虚函数
多态
智能指针
RAII
这些知识串起来了。
五、单例模式和工厂模式面试怎么回答
首先最常见的问题:
单例模式适合什么场景?
一般适合:
程序中逻辑上只需要一个实例
例如:
配置管理器
日志管理器
全局资源管理器
某些线程池管理器
但是不要看到:
全局使用
就全部使用单例。
因为单例本质上带有:
全局状态
使用过多可能导致:
模块耦合增加
测试困难
依赖关系隐藏
所以应该根据实际设计选择,而不是所有管理类都写成单例。
第二个问题:
为什么单例构造函数要private?
因为如果构造函数是:
public
外部可以:
Singleton a;
Singleton b;
这样就根本无法保证:
只有一个实例
所以构造函数必须限制外部直接调用。
同时一般还需要:
Singleton(const Singleton &) = delete;
Singleton &operator=(const Singleton &) = delete;
避免通过拷贝生成新的实例。
第三个问题:
怎么写线程安全单例?
现代 C++ 中可以直接写:
static Singleton &getInstance() {
static Singleton instance;
return instance;
}
面试中可以回答:
从 C++11 开始,函数局部静态变量的初始化具有线程安全保证,因此 Meyers Singleton 是一种非常简单的线程安全懒加载实现。
第四个问题:
工厂模式最大的作用是什么?
可以回答:
工厂模式把对象的创建过程封装起来,调用者只依赖抽象接口,不直接依赖具体类。这样增加或替换具体实现时,可以减少业务代码对具体类型的耦合。
例如:
没有工厂:
业务代码
↓
new Circle
变成:
有工厂:
业务代码
↓
Factory
↓
具体对象
也就是:
对象创建逻辑
从业务代码中被抽离出来。
第五个问题:
简单工厂和工厂方法有什么区别?
本文写的是:
简单工厂
一个工厂:
ShapeFactory
负责根据条件创建:
Circle
Rectangle
如果类型越来越多:
if (...)
else if (...)
else if (...)
工厂本身会越来越大。
工厂方法模式则进一步:
不同产品
由不同工厂负责创建
例如:
ShapeFactory
↑
│
┌────┴─────┐
↓ ↓
CircleFactory
RectangleFactory
其中:
CircleFactory
只负责:
Circle
而:
RectangleFactory
只负责:
Rectangle
不过面试中如果只是要求:
手写一个简单工厂
那么写本文这种:
ShapeFactory::create(type)
已经能够体现工厂模式最核心的思想。
最后把两种模式放在一起理解:
单例模式
↓
重点控制
"对象有几个"
↓
整个程序只有一个实例
工厂模式
↓
重点控制
"对象怎么创建"
↓
调用者不直接依赖具体类
如果面试官问:
单例模式怎么实现?
可以回答:
单例模式需要限制外部创建对象,因此构造函数设为私有,同时禁止拷贝构造和拷贝赋值,再提供静态接口获取唯一实例。现代 C++ 中常用函数局部静态变量实现 Meyers Singleton,从 C++11 开始其初始化具有线程安全保证。
如果问:
工厂模式解决什么问题?
可以回答:
工厂模式主要用于封装对象创建逻辑,让业务层面向抽象接口编程,而不是直接
new某个具体类,从而降低对象创建逻辑和业务逻辑之间的耦合。
这两种设计模式真正需要掌握的并不是把代码背下来,而是先知道:
为什么需要它
↓
它解决了什么问题
↓
什么时候值得使用
这样在实际项目或者面试设计题中,才能根据场景选择合适的模式。