C++设计模式之单例模式(Singleton)以及相关面试问题

单例模式(Singleton)

一、什么是单例模式(Singleton)

定义

单例模式保证一个类在程序运行期间 只有一个实例 ,并提供一个 全局访问点 来获取该实例。

核心目标

  • 控制对象的创建(只创建一次)
  • 全局可访问
  • 避免重复实例造成的资源浪费或状态冲突

重要理解单例模式的本质不是:

"全局只能有一个对象"

而是:

"在程序生命周期内,某类资源在逻辑上必须全局唯一,并由该类自己负责创建与访问"

换句话说:

  • 唯一性是业务语义,不是技术约束
  • Singleton 只是实现"唯一性"的一种方式

二、单例模式的基本结构

一个典型的单例类具备以下特征:

  1. 构造函数私有化
  2. 拷贝构造、赋值运算符禁用
  3. 通过静态方法返回唯一实例
cpp 复制代码
class Singleton {
public:
    static Singleton& instance();

private:
    Singleton();                          // 构造函数私有
    ~Singleton();

    Singleton(const Singleton&) = delete;            // 禁止拷贝
    Singleton& operator=(const Singleton&) = delete; // 禁止赋值
};

三、常见实现方式

1. 饿汉式(Eager Initialization)

程序启动时就创建实例

cpp 复制代码
class Singleton {
public:
    static Singleton& instance() {
        return instance_;
    }

private:
    Singleton() {}

    static Singleton instance_;
};

// 类外定义
Singleton Singleton::instance_;
优点
  • 实现简单
  • 天然线程安全
缺点
  • 即使不用也会创建
  • 可能增加启动时间
  • 静态初始化顺序问题(跨编译单元)

2. 懒汉式(非线程安全,不推荐)

cpp 复制代码
class Singleton {
public:
    static Singleton* instance() {
        if (!instance_) {
            instance_ = new Singleton();
        }
        return instance_;
    }

private:
    Singleton() {}
    static Singleton* instance_;
};

Singleton* Singleton::instance_ = nullptr;

问题严重

  • 多线程下可能创建多个实例
  • 内存泄漏风险
  • 不推荐使用

3. 双重检查锁(DCLP,C++11 前后差异)

cpp 复制代码
#include <mutex>

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

private:
    Singleton() {}
    static Singleton* instance_;
    static std::mutex mutex_;
};
问题
  • C++11 之前 存在指令重排问题
  • 实现复杂
  • 可读性差

现代 C++ 中不推荐


4. Meyers Singleton(推荐方式)

C++11 之后的标准解法

cpp 复制代码
class Singleton {
public:
    static Singleton& instance() {
        static Singleton instance; // 局部静态变量
        return instance;
    }

private:
    Singleton() {}
};
推荐原因
  • C++11 保证:

    • 局部静态变量初始化线程安全
  • 懒加载

  • 无需手动加锁

  • 自动析构(程序结束)

这是目前最优雅、最安全的实现


5. 智能指针 + call_once

cpp 复制代码
#include <memory>
#include <mutex>

class Singleton {
public:
    static Singleton& instance() {
        std::call_once(initFlag, &Singleton::init);
        return *instance_;
    }

private:
    Singleton() {}

    static void init() {
        instance_.reset(new Singleton());
    }

    static std::unique_ptr<Singleton> instance_;
    static std::once_flag initFlag;
};

std::unique_ptr<Singleton> Singleton::instance_;
std::once_flag Singleton::initFlag;
适合场景
  • 需要精细控制初始化流程
  • 单例生命周期复杂

6.工程级增强:接口 + Singleton(强烈推荐)

问题:单例导致"不可测试"
cpp 复制代码
foo() {
    Config::instance().maxIter(); //  隐式依赖
}
解法:接口隔离
cpp 复制代码
class IConfig {
public:
    virtual ~IConfig() = default;
    virtual int maxIter() const = 0;
};
cpp 复制代码
class Config final : public IConfig {
public:
    static Config& instance() {
        static Config inst;
        return inst;
    }

    int maxIter() const override { return max_iter_; }

private:
    int max_iter_ = 10;
};
cpp 复制代码
void run(const IConfig& cfg) {
    std::cout << cfg.maxIter() << std::endl;
}
测试时
cpp 复制代码
class MockConfig : public IConfig {
public:
    int maxIter() const override { return 1; }
};

四、单例模式的常见陷阱

1. 静态初始化顺序问题(Static Initialization Order Fiasco)

cpp 复制代码
// a.cpp
Singleton& s = Singleton::instance();

// b.cpp
static Singleton singleton;

解决方案

  • 使用 函数内局部静态对象(Meyers Singleton)

2. 析构顺序问题

  • 单例在程序结束时析构
  • 可能被其他静态对象使用

建议

  • 避免在析构阶段访问单例
  • 或使用"永不析构"的方式(new + 不 delete)

3. 隐式全局变量问题

单例本质上是:

"受控的全局变量"

可能导致:

  • 强耦合
  • 难以测试
  • 难以并行化

五、单例模式的适用场景

1. 必要条件(满足 ≥ 2 条才考虑)

条件 解释
资源唯一 OS / GPU / 文件 / 端口
全局一致性 配置、日志、统计
生命周期长 从 main 到 exit
跨模块共享 多子系统都要用

适合场景:

  • 日志系统(Logger)
  • 配置管理(Config)
  • 线程池
  • 资源管理器(GPU、文件句柄)
  • 全局状态只允许唯一实例

2. 不要用 Singleton 的典型场景

场景 为什么是灾难
算法对象 无法并行、多实例
Solver / Optimizer 测试和调参困难
业务对象 隐式依赖
状态机 无法回滚

经验法则:

如果你将来可能想同时创建两个对象 → 不要用 Singleton


六、Singleton 的生命周期与初始化陷阱

错误 1:构造函数做重活

cpp 复制代码
Config() {
    load("config.yaml"); // 
}

问题

  • 初始化顺序不可控
  • 异常难处理

正确做法

cpp 复制代码
void init(const std::string& path) {
    load(path);
}
cpp 复制代码
int main() {
    Config::instance().init("config.yaml");
}

错误 2:单例互相依赖(静态初始化地狱)

cpp 复制代码
Logger::instance().log(Config::instance().path());

解法

  • 延迟调用
  • 显式初始化顺序
  • 不在构造函数中访问其他 Singleton

七、线程安全 ≠ 单例安全

1. 常见误区

cpp 复制代码
Config::instance().set("x", 1); // 多线程

单例的"创建"是安全的,
但"状态访问"不一定安全

2. 正确方式

cpp 复制代码
class Config {
public:
    int get() const {
        std::shared_lock lock(mtx_);
        return value_;
    }

    void set(int v) {
        std::unique_lock lock(mtx_);
        value_ = v;
    }

private:
    mutable std::shared_mutex mtx_;
    int value_;
};

八、Singleton 的替代方案

方案 何时用
依赖注入(DI) 大型系统
Context 对象 算法工程
Namespace + static 无状态工具
Factory + Registry 插件系统

如果只是"方便访问",那你并不需要 Singleton


七、综合示例

1.简单示例

cpp 复制代码
class Singleton {
public:
    static Singleton& instance() {
        static Singleton instance;
        return instance;
    }

    void doSomething() {}

private:
    Singleton() = default;
    ~Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};

2.工程示例

场景:在SLAM / 算法工程中的配置 + 算法模块

Config(唯一)

cpp 复制代码
class Config {
public:
    static Config& instance();
    int maxIterations() const;

private:
    int max_iter_;
};

Optimizer(不依赖 Singleton)

cpp 复制代码
class Optimizer {
public:
    explicit Optimizer(const IConfig& cfg)
        : max_iter_(cfg.maxIterations()) {}

    void solve();

private:
    int max_iter_;
};
cpp 复制代码
int main() {
    auto& cfg = Config::instance();
    cfg.init("config.yaml");

    Optimizer opt(cfg);
    opt.solve();
}

Singleton 只出现在 main / infra 层


面试问题

1. 什么是单例模式?为什么要用它?

考察点

  • 是否理解"唯一性语义"
  • 是否知道它不是"全局变量"

优秀回答要点

  • 保证某类资源在程序中逻辑唯一
  • 控制实例创建与访问
  • 常用于日志、配置、硬件资源

错误回答

"就是全局只能有一个对象"


2. C++11 之后最推荐的单例实现方式是什么?为什么?

标准答案

Meyers Singleton(函数内 static)

理由

  • C++11 保证线程安全
  • 延迟初始化
  • 无锁
  • 实现简单
cpp 复制代码
static T instance;

3. 单例和全局变量的本质区别是什么?

关键区别

方面 全局变量 Singleton
创建控制 ❌ 无 ✅ 有
生命周期 编译期 运行期
封装性
可替换性 较好

4. 为什么要删除拷贝和移动构造?

考察点

  • 是否理解"唯一性破坏"

回答要点

  • 防止通过拷贝 / 移动产生第二个实例
  • 编译期约束优于运行期
cpp 复制代码
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;

5. Meyers Singleton 是如何保证线程安全的?

优秀回答

  • C++11 标准规定:
    函数内静态变量初始化是线程安全的
  • 编译器保证只初始化一次
  • 通常使用内部原子 / guard

6. 双重检查锁(DCLP)为什么不推荐?

答题要点

  • 代码复杂
  • 易出错
  • 早期内存重排序问题
  • C++11 后完全没必要

加分点:

提到 memory reordering / std::atomic / fence


7. Singleton 最大的工程问题是什么?

核心答案

隐藏依赖、强耦合、难测试

展开点:

  • 隐式依赖不可见
  • 无法注入 Mock
  • 多线程状态管理复杂

8.如何让 Singleton "可测试"?

优秀回答

  • 接口抽象(Interface)
  • 依赖注入
  • Singleton 只作为默认实现
cpp 复制代码
void foo(const IConfig& cfg);

9. 为什么不建议在算法模块中使用 Singleton?

答题要点

  • 算法通常需要多实例
  • 不利于并行 / 多数据集
  • 隐藏状态影响收敛和调试

SLAM / 优化器 面试加分题


10. Singleton 和依赖注入(DI)如何取舍?

对比思路

场景 更合适
小项目 / infra Singleton
大系统 / 可测试 DI
算法核心 DI
工具模块 Singleton

11. Singleton 的构造和析构顺序有什么问题?

考察点

  • 静态初始化顺序问题

回答要点

  • 不同翻译单元顺序未定义
  • 避免在构造 / 析构中访问其他单例
  • 显式 init / shutdown

12. 单例一定是线程安全的吗?

正确答案

不一定

说明:

  • 创建是线程安全的
  • 成员访问未必安全
  • 需自行加锁

13. 如何实现"可重置"的单例?为什么不推荐?

答题要点

  • 可通过指针 + reset
  • 破坏单例语义
  • 多线程风险极高
  • 测试专用,生产禁用

14. 如何在插件系统中避免 Singleton?

优秀回答

  • Factory + Registry
  • Context 传递
  • 生命周期由上层控制

15. 单例与 RAII 的关系?

加分点

  • Singleton 是对象管理
  • RAII 是资源管理
  • 单例常配合 RAII 使用(如 Logger / File)

16. 请说一个你用 Singleton 踩过的坑

面试官想听

  • 初始化顺序问题
  • 单元测试困难
  • 并发 bug

真实经历 > 完美答案


17 手写线程安全单例(5 分钟)

cpp 复制代码
class Singleton {
public:
    static Singleton& instance() {
        static Singleton inst;
        return inst;
    }
private:
    Singleton() = default;
};

18. 改造代码:去掉 Singleton 滥用

考察点

  • 架构能力
  • 依赖反转

总结回答

"Singleton 不是为了'方便访问',
而是为了'表达唯一性语义',
如果只是方便,我会用依赖注入。"


面试官视角总结

等级 表现
初级 会写单例
中级 知道什么时候不该用
高级 会替代 Singleton
架构 能限制 Singleton 的影响范围

相关推荐
草莓熊Lotso2 小时前
Qt 信号与槽深度解析:从基础用法到高级实战(含 Lambda 表达式)
java·运维·开发语言·c++·人工智能·qt·数据挖掘
脏脏a3 小时前
C++ STL list 模拟实现:从底层链表到容器封装
开发语言·c++·stl·双链表
熏鱼的小迷弟Liu4 小时前
【消息队列】RabbitMQ的基本架构?
面试·架构·rabbitmq
NAGNIP10 小时前
一文搞懂机器学习中的特征降维!
算法·面试
NAGNIP10 小时前
一文搞懂机器学习中的特征构造!
算法·面试
你怎么知道我是队长11 小时前
C语言---typedef
c语言·c++·算法
带土111 小时前
5. enum(枚举)关键字在C/C++中的作用
c语言·c++
驴友花雕12 小时前
【花雕学编程】Arduino BLDC 之群体机器人协同探索
c++·单片机·嵌入式硬件·arduino bldc·群体机器人协同探索
驴友花雕12 小时前
【花雕学编程】Arduino BLDC 之仿人机器人膝关节稳定系统
c++·单片机·嵌入式硬件·arduino bldc·仿人机器人膝关节稳定系统