编程C++ 开发者

在 C++ 中,'Singleton' 设计模式是什么?如何正确实现它,并且主要的陷阱是什么?

用 Hintsage AI 助手通过面试

回答。

问题历史:

单例模式的提出是为了限制只能创建一个特定类的实例,这在实现全局管理器时非常有用(例如,日志记录器、资源池、配置器)。

问题:

实现单例看似简单,但在 C++ 中会遇到困难:线程安全性、销毁的正确性、初始化的顺序。

解决方案:

实现单例的最安全和现代的方法是在静态函数内部使用静态局部变量,这保证了在第一次调用时进行初始化,线程安全性(从 C++11 开始)和正确的销毁。

代码示例:

class Singleton { public: static Singleton& instance() { static Singleton s; return s; } void doSomething() {} private: Singleton() {} Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete; };

关键特性:

  • 确保只有一个类的实例存在。
  • 惰性创建对象和线程安全性(从 C++11 开始)。
  • 对实例的生命周期和销毁进行控制。

具有陷阱的问题。

是否可以通过序列化或克隆来创建第二个单例实例?

是的。如果实现序列化/反序列化方法或手动实现 clone(),而不限制复制构造函数,可能会出现第二个实例。为避免这个问题,需要禁止所有复制、克隆和通过序列化恢复的方式。

在 C++98/03 标准下通过局部静态变量实现的单例在多线程环境中是否正确?

否。在 C++11 之前,局部静态变量在初始化时不保证线程安全。这可能导致当两个线程同时进入 instance() 函数时创建多个实例。在 C++11 及更新版本中—这个问题在标准层面上得到了解决。

通过静态局部变量创建的单例实例何时被销毁?

对象在程序结束时(exit)按创建的相反顺序(LIFO)销毁。但如果在析构函数中访问已经销毁的对象,可能会导致问题。

常见错误和反模式

  • 使用 new 而不是静态变量,造成内存泄漏。
  • 没有禁止复制/赋值。
  • 没有考虑到线程(在旧标准中)。
  • 使用 shared_ptr 或 weak_ptr 存储单例实例(破坏了唯一性)。

现实生活中的例子

消极案例

在日志系统中,开发人员使用全局指针和 new 实现单例,忘记禁止复制。程序运行正常,但在多线程环境中,日志记录器的不同实例有时会出现,消息丢失。

优点:

  • 最简单的实现;在单线程模式下运行迅速。

缺点:

  • 潜在的内存泄漏。
  • 在线程中破坏实例的唯一性。
  • 销毁的问题。

积极案例

单例通过静态局部变量在静态函数中实现,禁止了复制。线程安全得到了保证,程序可以扩展,日志正确分隔。

优点:

  • 能够在任何条件下保证唯一性。
  • 正确的销毁。
  • 没有内存泄漏。

缺点:

  • 难以进行测试(对于单元测试,替换单例比较复杂)。