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

news/2025/2/25 8:06:41

在这里插入图片描述

引言

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

单例模式概述

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

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

总结

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


http://www.niftyadmin.cn/n/5865228.html

相关文章

第5章 软件工程(一)

5.1 软件工程定义 软件工程是指应用计算机科学、数学及管理科学等原理&#xff0c;以工程化的原则和方法来解决软件问题的工程&#xff0c;其目的是提高软件生产率、 提高软件质量、降低软件成本。 5.2 软件需求 软件需求是指用户对系统在功能、行为、性能、设计约束等方面的…

玩转Docker | 使用Docker搭建Vikunja任务管理应用

玩转Docker | 使用Docker搭建Vikunja任务管理应用 前言一、 Vikunja介绍Vikunja 简介主要特点二、系统要求环境要求环境检查Docker版本检查检查操作系统版本三、部署Vikunja服务下载镜像创建容器检查容器状态检查服务端口安全设置四、访问Vikunja应用注册账号访问Vikunja主页五…

LabVIEW C编译支持工具库CCompileSupp.llb

路径&#xff1a;C:\Program Files (x86)\National Instruments\LabVIEW 2019\vi.lib\Platform\CCompileSupp.llb ​ 1. 工具库概述 定位&#xff1a;LabVIEW内置的C语言编译支持工具库&#xff0c;用于处理LabVIEW与C/C代码的混合编程接口&#xff0c;涵盖编译器配置、代码生成…

Go 语言内存池 (`sync.Pool`) 深度解析

Go 语言内存池 (sync.Pool) 深度解析 在高并发和性能敏感的应用中&#xff0c;频繁的内存分配和释放会带来显著的性能开销&#xff0c;并增加垃圾回收&#xff08;GC&#xff09;的压力。Go 语言通过 sync.Pool 提供了一种高效的对象复用机制&#xff0c;能够显著减少内存分配…

利用开源小智AI制作桌宠机器狗

本文主要介绍如何利用开源小智AI制作桌宠机器狗 1 源码下载 首先下载小智源码,下载地址, 下载源码后,使用vsCode打开,需要在vscode上安装esp-idf,安装方式请自己解决 2 源码修改 2.1添加机器狗控制代码 在目录main/iot/things下添加dog.cc文件,内容如下; #include…

【NLP】注意力机制

目录 一、认识注意力机制 1.1 常见注意力计算规则 1.2 注意力机制的作用 1.3 注意力机制代码实现 二、注意力机制原理 2.1 attention计算过程 2.2 attention的计算逻辑 2.3 有无attention模型对比 2.3.1 无attention机制的模型 2.3.2 有attention机制的模型 三、Se…

C/C++ | 每日一练 (3)

&#x1f4a2;欢迎来到张胤尘的技术站 &#x1f4a5;技术如江河&#xff0c;汇聚众志成。代码似星辰&#xff0c;照亮行征程。开源精神长&#xff0c;传承永不忘。携手共前行&#xff0c;未来更辉煌&#x1f4a5; 文章目录 C/C | 每日一练 (3)题目参考答案静态变量静态局部变量…

unordered_set和unordered_map的使用

Hello&#xff0c;今天我来为大家介绍一下前几年才刚刚新出的两个容器——unordered_map和unordered_set&#xff0c;这两个容器属于是map系列和set系列中的一种&#xff0c;和map/set不同的是它们的底层&#xff0c;map/set的底层是红黑树&#xff0c;而unordered_map/unorder…