C++ 中的设计模式是面试高频考点,面试官通常关注模式的核心思想、适用场景、C++ 实现细节(结合多态、内存管理等特性)以及模式间的区别。以下是高频设计模式的面试问题及详细解答:
1. 单例模式(Singleton)
问题:什么是单例模式?请用 C++ 实现线程安全的单例,并说明其优缺点及适用场景。
解答:
单例模式是一种创建型模式,确保一个类全局只有一个实例,并提供唯一的全局访问点。
核心思想:
- 私有构造函数(禁止外部创建实例)。
- 静态成员变量存储唯一实例。
- 静态成员函数提供全局访问接口。
线程安全的 C++ 实现(C++11 推荐方案):
cpp
class Singleton {
public:
// 禁用拷贝和移动(防止通过复制创建新实例)
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
Singleton(Singleton&&) = delete;
Singleton& operator=(Singleton&&) = delete;
// 全局访问点:返回局部静态变量的引用(C++11 保证初始化线程安全)
static Singleton& getInstance() {
static Singleton instance; // 仅初始化一次,线程安全
return instance;
}
// 示例功能:单例的业务方法
void doSomething() { /* ... */ }
private:
// 私有构造(仅内部可创建)
Singleton() = default;
// 私有析构(生命周期与程序一致)
~Singleton() = default;
};
优缺点:
-
优点
:
- 严格控制实例数量,节省资源(如全局配置、日志器)。
- 避免频繁创建销毁实例的开销。
-
缺点
:
- 引入全局状态,增加代码耦合度,不利于单元测试(难以模拟)。
- 若单例持有资源,可能导致资源释放时机不明确(依赖程序退出)。
适用场景:
- 全局配置管理器(如读取配置文件的实例,需唯一)。
- 日志器(避免多实例导致日志文件冲突)。
- 线程池、连接池(需统一管理资源)。
2. 工厂模式(Factory)
问题:工厂模式有哪几种?请用 C++ 实现工厂方法模式,并说明其与简单工厂、抽象工厂的区别。
解答:
工厂模式是创建型模式的总称,核心是将对象创建与使用分离 ,避免直接通过 new 硬编码对象类型。分为三类:
三种工厂模式的区别:
| 模式 | 核心思想 | 优缺点 |
|---|---|---|
| 简单工厂 | 用一个工厂类根据参数创建不同产品(非设计模式,是一种编程习惯)。 | 优点:简单直观;缺点:新增产品需修改工厂类,违反 "开闭原则"。 |
| 工厂方法 | 定义抽象工厂基类,每个具体产品对应一个具体工厂子类(通过继承实现扩展)。 | 优点:符合开闭原则(新增产品只需加新工厂);缺点:类数量爆炸(产品 = 工厂)。 |
| 抽象工厂 | 抽象工厂生产 "产品族"(一组相关产品),具体工厂生产具体产品族。 | 优点:支持多产品族扩展;缺点:新增单个产品需修改抽象工厂,灵活性较低。 |
工厂方法模式的 C++ 实现(以日志器为例):
场景:支持两种日志器(文件日志、控制台日志),需灵活扩展新日志类型。
cpp
// 1. 抽象产品:日志器基类
class Logger {
public:
virtual void log(const std::string& msg) = 0; // 纯虚函数,定义接口
virtual ~Logger() = default; // 基类析构需虚函数
};
// 2. 具体产品:文件日志器
class FileLogger : public Logger {
public:
void log(const std::string& msg) override {
std::cout << "File log: " << msg << std::endl;
}
};
// 2. 具体产品:控制台日志器
class ConsoleLogger : public Logger {
public:
void log(const std::string& msg) override {
std::cout << "Console log: " << msg << std::endl;
}
};
// 3. 抽象工厂:日志器工厂基类
class LoggerFactory {
public:
virtual Logger* createLogger() = 0; // 纯虚函数,创建产品
virtual ~LoggerFactory() = default;
};
// 4. 具体工厂:文件日志器工厂
class FileLoggerFactory : public LoggerFactory {
public:
Logger* createLogger() override {
return new FileLogger(); // 创建具体产品
}
};
// 4. 具体工厂:控制台日志器工厂
class ConsoleLoggerFactory : public LoggerFactory {
public:
Logger* createLogger() override {
return new ConsoleLogger(); // 创建具体产品
}
};
// 客户端使用:依赖抽象,不依赖具体
void clientCode(LoggerFactory& factory) {
Logger* logger = factory.createLogger();
logger->log("Hello Factory Method");
delete logger; // 释放资源(实际可结合智能指针)
}
int main() {
FileLoggerFactory fileFactory;
ConsoleLoggerFactory consoleFactory;
clientCode(fileFactory); // 输出:File log: ...
clientCode(consoleFactory); // 输出:Console log: ...
return 0;
}
适用场景:
- 简单工厂:产品类型少且稳定(如简单计算器的运算器)。
- 工厂方法:产品类型多变,需频繁扩展(如日志器、数据库驱动)。
- 抽象工厂:需创建一组相关产品(如不同操作系统的 UI 组件族:Windows 按钮 + Windows 文本框)。
3. 观察者模式(Observer)
问题:什么是观察者模式?请用 C++ 实现一个简单的观察者模式(如事件通知),并说明其优缺点。
解答:
观察者模式是行为型模式,定义对象间的一对多依赖:当一个对象(主题)状态变化时,所有依赖它的对象(观察者)会自动收到通知并更新。
核心角色:
- 主题(Subject):维护观察者列表,提供注册、注销和通知接口。
- 观察者(Observer):定义更新接口,接收主题通知后执行相应操作。
C++ 实现(以 "天气站通知" 为例):
场景:天气站(主题)检测到温度变化时,自动通知所有订阅的显示器(观察者)更新显示。
cpp
#include <vector>
#include <memory> // 智能指针管理内存
// 1. 观察者基类:定义更新接口
class Observer {
public:
virtual void update(float temperature) = 0; // 接收温度更新
virtual ~Observer() = default;
};
// 2. 主题基类:定义注册、注销、通知接口
class Subject {
public:
virtual void registerObserver(Observer* observer) = 0;
virtual void removeObserver(Observer* observer) = 0;
virtual void notifyObservers() = 0; // 通知所有观察者
virtual ~Subject() = default;
};
// 3. 具体主题:天气站
class WeatherStation : public Subject {
private:
float temperature_; // 状态:温度
std::vector<Observer*> observers_; // 观察者列表(弱引用,避免所有权问题)
public:
void setTemperature(float temp) {
temperature_ = temp;
notifyObservers(); // 温度变化,通知观察者
}
void registerObserver(Observer* observer) override {
observers_.push_back(observer);
}
void removeObserver(Observer* observer) override {
// 从列表中移除观察者
auto it = std::find(observers_.begin(), observers_.end(), observer);
if (it != observers_.end()) {
observers_.erase(it);
}
}
void notifyObservers() override {
// 遍历所有观察者,调用更新方法
for (Observer* observer : observers_) {
observer->update(temperature_);
}
}
};
// 4. 具体观察者:手机显示器
class PhoneDisplay : public Observer {
public:
void update(float temperature) override {
std::cout << "Phone Display: Temperature is " << temperature << "°C" << std::endl;
}
};
// 4. 具体观察者:电脑显示器
class ComputerDisplay : public Observer {
public:
void update(float temperature) override {
std::cout << "Computer Display: Temperature is " << temperature << "°C" << std::endl;
}
};
// 客户端使用
int main() {
WeatherStation station;
PhoneDisplay phone;
ComputerDisplay computer;
station.registerObserver(&phone); // 手机订阅
station.registerObserver(&computer); // 电脑订阅
station.setTemperature(25.5f); // 温度变化,触发通知
// 输出:
// Phone Display: Temperature is 25.5°C
// Computer Display: Temperature is 25.5°C
station.removeObserver(&phone); // 手机取消订阅
station.setTemperature(26.0f);
// 输出:
// Computer Display: Temperature is 26.0°C
return 0;
}
优缺点:
-
优点
:
- 主题与观察者解耦(主题无需知道观察者具体类型)。
- 支持动态添加 / 删除观察者,灵活性高。
-
缺点
:
- 若观察者过多,通知可能耗时(需考虑异步通知)。
- 可能导致循环依赖(如观察者同时是主题)。
适用场景:
- 事件驱动系统(如 GUI 按钮点击事件:按钮是主题,回调函数是观察者)。
- 发布 - 订阅模式(如消息队列:生产者是主题,消费者是观察者)。
- 状态监控(如股票价格更新、传感器数据通知)。
4. 装饰器模式(Decorator)
问题:装饰器模式的作用是什么?请用 C++ 实现一个装饰器模式(如给咖啡加配料),并说明其与继承的区别。
解答:
装饰器模式是结构型模式,动态地给对象添加额外功能,同时不改变其原始结构。通过 "组合" 而非 "继承" 实现功能扩展,避免类爆炸。
与继承的区别:
- 继承:编译时静态扩展,子类耦合父类,功能固定且类数量随功能组合呈指数增长(如 "咖啡 + 奶 + 糖" 需单独子类)。
- 装饰器:运行时动态扩展,通过组合叠加功能,类数量线性增长(每个功能一个装饰器)。
C++ 实现(咖啡加配料场景):
场景:基础咖啡(美式)可动态添加奶、糖、巧克力等配料,每种配料增加价格和描述。
cpp
#include <string>
#include <iostream>
// 1. 抽象组件:咖啡基类
class Coffee {
public:
virtual std::string getDescription() = 0; // 描述
virtual double getPrice() = 0; // 价格
virtual ~Coffee() = default;
};
// 2. 具体组件:基础咖啡(美式)
class Americano : public Coffee {
public:
std::string getDescription() override {
return "Americano";
}
double getPrice() override {
return 10.0;
}
};
// 3. 装饰器基类:继承咖啡接口,持有咖啡对象(组合)
class CoffeeDecorator : public Coffee {
protected:
Coffee* coffee_; // 被装饰的咖啡(指针避免对象切片)
public:
explicit CoffeeDecorator(Coffee* coffee) : coffee_(coffee) {}
~CoffeeDecorator() override { delete coffee_; } // 释放被装饰对象
};
// 4. 具体装饰器:加奶
class MilkDecorator : public CoffeeDecorator {
public:
explicit MilkDecorator(Coffee* coffee) : CoffeeDecorator(coffee) {}
std::string getDescription() override {
return coffee_->getDescription() + ", Milk";
}
double getPrice() override {
return coffee_->getPrice() + 2.0; // 奶加2元
}
};
// 4. 具体装饰器:加糖
class SugarDecorator : public CoffeeDecorator {
public:
explicit SugarDecorator(Coffee* coffee) : CoffeeDecorator(coffee) {}
std::string getDescription() override {
return coffee_->getDescription() + ", Sugar";
}
double getPrice() override {
return coffee_->getPrice() + 1.0; // 糖加1元
}
};
// 客户端使用:动态组合功能
int main() {
// 美式 + 奶 + 糖
Coffee* coffee = new SugarDecorator(
new MilkDecorator(
new Americano()
)
);
std::cout << "Description: " << coffee->getDescription() << std::endl; // Americano, Milk, Sugar
std::cout << "Price: " << coffee->getPrice() << std::endl; // 13.0
delete coffee; // 装饰器链会递归释放所有对象
return 0;
}
适用场景:
- 需动态扩展对象功能(如 IO 流:
std::ifstream可被std::buffered_stream装饰以增加缓冲功能)。 - 功能组合多样(如咖啡配料、GUI 组件的边框 / 滚动条组合)。
5. 适配器模式(Adapter)
问题:什么是适配器模式?请用 C++ 实现类适配器和对象适配器,并说明其区别。
解答:
适配器模式是结构型模式,将一个类的接口转换为客户端期望的另一个接口,使原本因接口不兼容而无法协作的类可以一起工作。
两种适配器的区别:
| 类型 | 实现方式 | 优缺点 |
|---|---|---|
| 类适配器 | 继承被适配类(Adaptee)和目标接口(Target),通过多继承实现。 | 优点:直接复用被适配类的行为;缺点:依赖多继承,灵活性低(C++ 支持,Java 不支持)。 |
| 对象适配器 | 持有被适配类的实例(组合),实现目标接口。 | 优点:更灵活(可适配任意被适配类的子类);缺点:需转发所有接口调用。 |
C++ 实现(适配旧接口到新接口):
场景 :已有一个旧的日志类 OldLogger(接口为 logMessage),需适配到新接口 NewLogger(接口为 log)。
cpp
#include <string>
#include <iostream>
// 1. 目标接口:客户端期望的新接口
class NewLogger {
public:
virtual void log(const std::string& msg) = 0; // 新接口:log
virtual ~NewLogger() = default;
};
// 2. 被适配类:旧接口
class OldLogger {
public:
// 旧接口:logMessage(参数和名称与新接口不同)
void logMessage(const char* msg, int level) {
std::cout << "OldLogger [level " << level << "]: " << msg << std::endl;
}
};
// 3. 类适配器:继承旧类和新接口(多继承)
class ClassAdapter : public NewLogger, private OldLogger {
public:
void log(const std::string& msg) override {
// 适配:将新接口调用转换为旧接口
logMessage(msg.c_str(), 1); // 固定level=1
}
};
// 4. 对象适配器:持有旧类实例(组合)
class ObjectAdapter : public NewLogger {
private:
OldLogger* oldLogger_; // 持有被适配对象
public:
explicit ObjectAdapter(OldLogger* logger) : oldLogger_(logger) {}
void log(const std::string& msg) override {
// 适配:转发到旧接口
oldLogger_->logMessage(msg.c_str(), 1);
}
};
// 客户端:只依赖新接口
void client(NewLogger& logger) {
logger.log("Hello Adapter");
}
int main() {
ClassAdapter classAdapter;
client(classAdapter); // 输出:OldLogger [level 1]: Hello Adapter
OldLogger oldLogger;
ObjectAdapter objectAdapter(&oldLogger);
client(objectAdapter); // 输出:OldLogger [level 1]: Hello Adapter
return 0;
}
适用场景:
- 集成 legacy 代码(旧系统接口适配新系统)。
- 复用第三方库(库接口与本地接口不兼容时)。
总结
C++ 设计模式面试重点关注:
- 创建型模式(单例、工厂):解决对象创建问题,结合 C++ 构造函数、静态成员、智能指针。
- 结构型模式(装饰器、适配器):解决类 / 对象组合问题,体现 "组合优于继承" 原则。
- 行为型模式(观察者):解决对象间交互问题,依赖 C++ 多态(虚函数)实现解耦。
回答时需结合具体场景说明模式的 "为什么用""怎么用",并对比相似模式的区别(如工厂方法 vs 抽象工厂,装饰器 vs 适配器)。