当前位置: 首页 > news >正文

【C++设计模式】 单例设计模式:重要常用却并非完美之选

在这里插入图片描述

引言

设计模式在软件开发中扮演着至关重要的角色,然而,没有一种设计模式是完美无缺的,单例设计模式便是其中之一。它一直以来都备受争议,有人认为它是解决特定问题的有效方案,也有人觉得它存在诸多弊端。在实际应用中,我们需要根据具体问题和项目需求来判断是否选择单例模式。

单例模式概述

单例模式是一种创建型设计模式,主要关注对象的创建方式。其核心思想是确保在一个程序中,某个类只能有一个实例存在,并且提供一个全局访问点来获取这个实例。

以 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;

总结

单例模式是一种强大的设计模式,它可以确保一个类只有一个实例,并提供全局访问点。然而,它也存在一些问题,如全局状态的引入、多线程安全问题以及可测试性降低等。在使用单例模式时,我们需要权衡其优缺点,并根据具体情况进行选择。同时,我们还需要注意代码的实现细节,避免出现潜在的问题。希望通过本文的介绍,你能对单例模式有更深入的理解,并在实际开发中合理运用。

相关文章:

  • Infrared拼接融合
  • your HTTP request connection start duration too long
  • 后端之JPA(EntityGraph+JsonView)
  • AI驱动的自动化留给人类的时间不多了
  • 【K8s】专题十六(2):Kubernetes 包管理工具之 Helm 使用
  • Springboot 文件下载
  • 【JavaEE进阶】Spring Boot配置文件
  • 本地Oracle数据库复制数据到Apache Hive的Linux服务器集群的分步流程
  • angular登录页
  • ARCGIS国土超级工具集1.4更新说明
  • Java Map实现类面试题
  • 位于陕西省的高校查收查引单位
  • 04.Python函数和模块
  • DeepSeek+Kimi 一键生成100种PPT
  • 商业化运作的“日记”
  • 突破多模态与跨领域瓶颈!清华华为联合推出革命性推荐系统专利,重塑AI时代用户体验
  • AF3 创新点总结
  • 【FAQ】HarmonyOS SDK 闭源开放能力 —Ads Kit(2)
  • 【算法】位运算
  • OpenCV计算摄影学Computational Photography
  • 哈马斯与以色列在多哈举行新一轮加沙停火谈判
  • 广西壮族自治区党委常委会:坚决拥护党中央对蓝天立进行审查调查的决定
  • 习近平就乌拉圭前总统穆希卡逝世向乌拉圭总统奥尔西致唁电
  • 新版城市规划体检评估解读:把城市安全韧性摆在更加突出位置
  • 上海虹桥国际咖啡文化节开幕,推出茶咖文化特色街区、宝妈咖啡师培训
  • 特朗普再提“接管”加沙,要将其变为“自由区”