引言
设计模式在软件开发中扮演着至关重要的角色,然而,没有一种设计模式是完美无缺的,单例设计模式便是其中之一。它一直以来都备受争议,有人认为它是解决特定问题的有效方案,也有人觉得它存在诸多弊端。在实际应用中,我们需要根据具体问题和项目需求来判断是否选择单例模式。
单例模式概述
单例模式是一种创建型设计模式,主要关注对象的创建方式。其核心思想是确保在一个程序中,某个类只能有一个实例存在,并且提供一个全局访问点来获取这个实例。
以 C++ 语言为例,通常情况下,我们创建一个类的对象时可以多次实例化。比如创建一个名为 Type
的类:
class Type {
// 类的成员和方法
};
我们可以在程序中多次实例化这个类的对象:
Type myObject1;
Type myObject2;
但在单例模式下,我们需要限制只能创建一个该类的实例。
实现单例模式的步骤
初始尝试
为了演示单例模式,我们创建一个简单的日志类 Logger
。首先,我们按照常规方式创建这个类,包含构造函数、析构函数、拷贝构造函数和赋值运算符:
#include <iostream>
class Logger {
public:
Logger() {
std::cout << "Logger was created" << std::endl;
}
~Logger() {
// 析构函数代码
}
Logger(const Logger&) = default;
Logger& operator=(const Logger&) = default;
};
在 main
函数中,我们可以多次创建 Logger
类的对象:
int main() {
Logger logger1;
Logger logger2;
Logger logger3;
Logger logger4;
return 0;
}
编译并运行这个程序,我们会看到多次输出 “Logger was created”,这表明创建了多个 Logger
实例。
限制对象创建
为了实现单例模式,我们需要限制对象的创建。一个简单的方法是将构造函数设为私有:
class Logger {
private:
Logger() {
std::cout << "Logger was created" << std::endl;
}
~Logger() {
// 析构函数代码
}
Logger(const Logger&) = default;
Logger& operator=(const Logger&) = default;
};
此时,如果我们尝试在 main
函数中创建 Logger
对象,编译器会报错,因为构造函数是私有的,外部无法直接调用。
提供全局访问点
为了能够获取 Logger
类的唯一实例,我们需要提供一个公共的成员函数 getInstance
:
class Logger {
private:
Logger() {
std::cout << "Logger was created" << std::endl;
}
~Logger() {
// 析构函数代码
}
Logger(const Logger&) = default;
Logger& operator=(const Logger&) = default;
static Logger* s_instance;
public:
static Logger* getInstance() {
if (s_instance == nullptr) {
s_instance = new Logger();
}
return s_instance;
}
};
Logger* Logger::s_instance = nullptr;
在 main
函数中,我们可以通过 getInstance
函数来获取 Logger
类的唯一实例:
int main() {
Logger* logger = Logger::getInstance();
return 0;
}
静态关键字的使用
在单例模式中,静态关键字 static
起着关键作用。静态变量的生命周期是整个程序的运行期间,并且只会被初始化一次。
例如,我们可以在 Logger
类中添加一个静态成员变量 numberOfLoggers
来记录创建的 Logger
实例数量:
class Logger {
private:
Logger() {
std::cout << "Logger was created, number of loggers: " << numberOfLoggers << std::endl;
numberOfLoggers++;
}
~Logger() {
// 析构函数代码
}
Logger(const Logger&) = default;
Logger& operator=(const Logger&) = default;
static int numberOfLoggers;
public:
static Logger* getInstance() {
if (s_instance == nullptr) {
s_instance = new Logger();
}
return s_instance;
}
};
int Logger::numberOfLoggers = 0;
需要注意的是,静态成员变量必须在类外部进行初始化。
完善日志类功能
为了让 Logger
类更具实用性,我们可以添加一些成员函数,如 addMessage
用于添加日志消息,printMessages
用于打印所有日志消息:
#include <iostream>
#include <vector>
#include <string>
class Logger {
private:
Logger() {
std::cout << "Logger was created" << std::endl;
}
~Logger() {
// 析构函数代码
}
Logger(const Logger&) = default;
Logger& operator=(const Logger&) = default;
static Logger* s_instance;
std::vector<std::string> messages;
public:
static Logger* getInstance() {
if (s_instance == nullptr) {
s_instance = new Logger();
}
return s_instance;
}
void addMessage(const std::string& message) {
messages.push_back(message);
}
void printMessages() {
for (const auto& message : messages) {
std::cout << message << std::endl;
}
}
};
Logger* Logger::s_instance = nullptr;
在 main
函数中,我们可以使用这些功能:
int main() {
Logger* logger = Logger::getInstance();
logger->addMessage("hello message1");
logger->addMessage("hello message2");
logger->addMessage("hello message3");
logger->printMessages();
return 0;
}
单例模式的问题与改进
指针返回的风险
在上述实现中,getInstance
函数返回的是一个指针。这可能会带来一些问题,例如用户可能会意外删除这个指针,导致单例对象被销毁。为了避免这种情况,我们可以返回一个引用而不是指针:
class Logger {
private:
Logger() {
std::cout << "Logger was created" << std::endl;
}
~Logger() {
// 析构函数代码
}
Logger(const Logger&) = default;
Logger& operator=(const Logger&) = default;
static Logger* s_instance;
std::vector<std::string> messages;
public:
static Logger& getInstance() {
static Logger instance;
return instance;
}
void addMessage(const std::string& message) {
messages.push_back(message);
}
void printMessages() {
for (const auto& message : messages) {
std::cout << message << std::endl;
}
}
};
在 main
函数中,我们可以这样使用:
int main() {
Logger& logger = Logger::getInstance();
logger.addMessage("hello message1");
logger.addMessage("hello message2");
logger.addMessage("hello message3");
logger.printMessages();
return 0;
}
多线程考虑
在多线程环境下,单例模式可能会出现问题。例如,多个线程同时调用 getInstance
函数时,可能会创建多个实例。为了保证线程安全,我们可以使用互斥锁(mutex)来保护 getInstance
函数:
#include <iostream>
#include <vector>
#include <string>
#include <mutex>
class Logger {
private:
Logger() {
std::cout << "Logger was created" << std::endl;
}
~Logger() {
// 析构函数代码
}
Logger(const Logger&) = default;
Logger& operator=(const Logger&) = default;
static Logger* s_instance;
static std::mutex mtx;
std::vector<std::string> messages;
public:
static Logger& getInstance() {
std::lock_guard<std::mutex> lock(mtx);
if (s_instance == nullptr) {
s_instance = new Logger();
}
return *s_instance;
}
void addMessage(const std::string& message) {
messages.push_back(message);
}
void printMessages() {
for (const auto& message : messages) {
std::cout << message << std::endl;
}
}
};
Logger* Logger::s_instance = nullptr;
std::mutex Logger::mtx;
总结
单例模式是一种强大的设计模式,它可以确保一个类只有一个实例,并提供全局访问点。然而,它也存在一些问题,如全局状态的引入、多线程安全问题以及可测试性降低等。在使用单例模式时,我们需要权衡其优缺点,并根据具体情况进行选择。同时,我们还需要注意代码的实现细节,避免出现潜在的问题。希望通过本文的介绍,你能对单例模式有更深入的理解,并在实际开发中合理运用。