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

单例模式(C++)详解(3)

<摘要>
单例模式是创建型设计模式中最经典且应用最广泛的设计模式之一,其核心目标是确保一个类只有一个实例并提供全局访问点。本文从单例模式的历史背景和发展脉络出发,系统阐述了其在软件架构演进过程中的重要地位。通过深入分析懒汉式、饿汉式、Meyers Singleton等不同实现方式的优缺点,详细剖析了线程安全、内存管理、初始化时机等关键设计考量因素。文章结合日志系统、配置管理、数据库连接池等实际应用场景,提供了完整的可编译代码示例和Makefile配置,并使用时序图直观展示了多线程环境下的竞态条件问题及其解决方案。最后,总结了单例模式的适用场景、最佳实践以及在现代C++中的改进,为开发者提供了全面而实用的指导。


<解析>

1. 背景与核心概念

1.1 产生背景与发展脉络

单例模式(Singleton Pattern)的诞生源于软件开发中对特定类型对象管理的实际需求。在早期的软件开发实践中,开发者逐渐意识到某些类的实例应该在整个应用程序生命周期中只存在一个,这种需求催生了单例模式的形成。

历史演进阶段

  1. 初期探索阶段(1980年代前):在面向对象编程范式普及之前,开发者通常使用全局变量来实现类似单例的功能。这种方式虽然简单,但带来了命名冲突、初始化顺序不确定和访问控制缺失等问题。

  2. 模式化阶段(1980-1990年代):随着"Gang of Four"(Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides)在1994年出版的《设计模式:可复用面向对象软件的基础》一书中正式提出单例模式,它被系统性地归纳为23种经典设计模式之一,属于创建型模式类别。

  3. 语言特性融合阶段(2000年代至今):随着编程语言的发展,单例模式的实现方式不断演进。C++11标准引入的线程局部存储(thread_local)、原子操作(atomic)和静态变量初始化线程安全等特性,为单例模式提供了更优雅的实现方案。

产生的根本原因

  • 资源共享需求:如数据库连接池、线程池等需要集中管理的资源
  • 状态一致性要求:如配置管理、计数器等需要全局一致状态的对象
  • 性能优化考虑:避免频繁创建销毁重量级对象带来的开销
  • 访问控制需要:集中管控对特定资源的访问,如日志系统

1.2 核心概念与关键术语

单例模式(Singleton Pattern):确保一个类只有一个实例,并提供一个全局访问点来获取该实例的设计模式。

关键特性

  • 唯一实例性(Instance Uniqueness):保证类只有一个实例存在
  • 全局可访问性(Global Accessibility):提供统一的访问入口
  • 延迟初始化(Lazy Initialization):多数实现支持在第一次使用时创建实例
  • 线程安全性(Thread Safety):在多线程环境下保证正确性

基本结构组件

class Singleton {
private:static Singleton* instance;    // 静态私有成员,保存唯一实例Singleton();                   // 私有构造函数,防止外部实例化Singleton(const Singleton&) = delete;            // 删除拷贝构造函数Singleton& operator=(const Singleton&) = delete; // 删除赋值运算符public:static Singleton* getInstance(); // 静态公共方法,提供全局访问点// 其他成员函数...
};

UML表示

Singleton
-static Singleton* instance
-Singleton()
+static getInstance()
+someOperation() : void

图1.1:单例模式基本UML类图

2. 设计意图与考量

2.1 核心设计目标

单例模式的设计旨在解决以下核心问题:

2.1.1 controlled Instance Creation(受控实例创建)
通过将构造函数设为私有,单例模式彻底消除了客户端随意创建类实例的可能性。这种强制性的创建控制确保了实例数量的严格管理,从语言机制层面而非仅仅约定层面保证了单一实例的约束。

2.1.2 Global Access Point(全局访问点)
提供静态方法getInstance()作为获取单例实例的统一入口,解决了全局变量方式的散乱访问问题。这种方法:

  • 明确了职责:清晰标识这是获取实例的正确方式
  • 封装了复杂性:隐藏了实例创建和管理的细节
  • 提供了灵活性:允许在不改变客户端代码的情况下修改实例化策略

2.1.3 Resource Coordination(资源协调)
对于需要协调共享资源的场景,单例模式提供了自然的设计方案:

  • 避免资源冲突:如多个日志写入器同时写文件可能导致的内容交错
  • 减少资源浪费:如数据库连接的重用而非重复创建
  • 统一管理策略:如缓存的一致性管理和过期策略

2.2 设计考量因素

2.2.1 线程安全性考量
在多线程环境下,单例模式的实现必须考虑竞争条件(Race Condition)问题:

Thread1Thread2Singleton竞态条件发生场景getInstance()检查instance==nullptrgetInstance()检查instance==nullptrnew Singleton()创建实例new Singleton()创建另一个实例两个线程得到不同实例,违反单例原则Thread1Thread2Singleton

图2.1:多线程环境下的竞态条件时序图

解决方案包括:

  • 饿汉式初始化:在程序启动时即创建实例,避免运行时竞争
  • 互斥锁保护:在懒汉式初始化时使用锁机制
  • 双检锁模式:减少锁的使用频率,提高性能
  • 局部静态变量:利用C++11的静态变量线程安全特性

2.2.2 初始化时机权衡

初始化方式优点缺点适用场景
饿汉式实现简单,线程安全可能提前占用资源,启动慢实例小且必定使用
懒汉式资源按需分配,启动快实现复杂,需要线程安全措施实例大或不一定会使用

2.2.3 继承与扩展性
单例类的继承会带来设计上的挑战:

  • 构造函数隐私性:派生类需要访问基类构造函数
  • 实例唯一性:每个派生类是否都应该是单例?
  • 模板方法应用:通过模板元编程实现可复用的单例基类

2.2.4 测试困难性
单例模式对单元测试不友好,主要原因:

  • 全局状态共享:测试用例之间可能相互影响
  • 难以模拟:无法轻松替换为模拟对象进行测试
  • 重置困难:需要额外机制在测试间重置单例状态

2.2.5 生命周期管理
单例实例的生命周期管理需要考虑:

  • 创建时机:何时以及如何创建实例
  • 销毁时机:是否需要显式销毁,如何保证安全销毁
  • 依赖关系:单例之间的依赖关系及初始化顺序

3. 实例与应用场景

3.1 日志系统(Logger)

应用场景
在大多数应用程序中,日志系统需要满足以下要求:

  • 全局唯一:多个模块共享同一个日志实例
  • 线程安全:多线程环境下能安全写入日志
  • 集中配置:统一设置日志级别、输出目标等

完整实现代码

// logger.h
#ifndef LOGGER_H
#define LOGGER_H#include <iostream>
#include <string>
#include <mutex>
#include <fstream>
#include <chrono>
#include <iomanip>enum class LogLevel {DEBUG,INFO,WARNING,ERROR,CRITICAL
};class Logger {
public:static Logger& getInstance();void setLogLevel(LogLevel level);void setLogFile(const std::string& filename);void log(const std::string& message, LogLevel level = LogLevel::INFO);// 删除拷贝构造函数和赋值运算符Logger(const Logger&) = delete;Logger& operator=(const Logger&) = delete;private:Logger();~Logger();std::string getLevelString(LogLevel level) const;static std::mutex mutex_;LogLevel currentLevel_;std::ofstream logFile_;std::ostream* outputStream_;bool useFile_;
};#endif // LOGGER_H
// logger.cpp
#include "logger.h"
#include <iostream>std::mutex Logger::mutex_;Logger::Logger() : currentLevel_(LogLevel::INFO), useFile_(false), outputStream_(&std::cout) {std::cout << "Logger initialized" << std::endl;
}Logger::~Logger() {if (logFile_.is_open()) {logFile_.close();}std::cout << "Logger destroyed" << std::endl;
}Logger& Logger::getInstance() {static Logger instance;return instance;
}void Logger::setLogLevel(LogLevel level) {std::lock_guard<std::mutex> lock(mutex_);currentLevel_ = level;
}void Logger::setLogFile(const std::string& filename) {std::lock_guard<std::mutex> lock(mutex_);if (logFile_.is_open()) {logFile_.close();}logFile_.open(filename, std::ios::out | std::ios::app);if (logFile_.is_open()) {useFile_ = true;outputStream_ = &logFile_;} else {useFile_ = false;outputStream_ = &std::cout;std::cerr << "Failed to open log file: " << filename << std::endl;}
}void Logger::log(const std::string& message, LogLevel level) {if (level < currentLevel_) {return;}std::lock_guard<std::mutex> lock(mutex_);// 获取当前时间auto now = std::chrono::system_clock::now();auto time = std::chrono::system_clock::to_time_t(now);*outputStream_ << "[" << std::put_time(std::localtime(&time), "%Y-%m-%d %H:%M:%S") << "] "<< "[" << getLevelString(level) << "] "<< message << std::endl;
}std::string Logger::getLevelString(LogLevel level) const {switch (level) {case LogLevel::DEBUG: return "DEBUG";case LogLevel::INFO: return "INFO";case LogLevel::WARNING: return "WARNING";case LogLevel::ERROR: return "ERROR";case LogLevel::CRITICAL: return "CRITICAL";default: return "UNKNOWN";}
}
// main_logger.cpp
#include "logger.h"
#include <thread>
#include <vector>void logMessages(int threadId) {for (int i = 0; i < 5; ++i) {std::string message = "Thread " + std::to_string(threadId) + " - Message " + std::to_string(i);Logger::getInstance().log(message, LogLevel::INFO);std::this_thread::sleep_for(std::chrono::milliseconds(100));}
}int main() {// 设置日志级别和输出文件Logger::getInstance().setLogLevel(LogLevel::DEBUG);Logger::getInstance().setLogFile("application.log");Logger::getInstance().log("Application started", LogLevel::INFO);// 创建多个线程测试线程安全性std::vector<std::thread> threads;for (int i = 0; i < 3; ++i) {threads.emplace_back(logMessages, i);}for (auto& thread : threads) {thread.join();}Logger::getInstance().log("Application finished", LogLevel::INFO);return 0;
}

Makefile配置

# Makefile for Logger example
CXX = g++
CXXFLAGS = -std=c++11 -pthread -Wall -WextraTARGET = logger_example
SOURCES = main_logger.cpp logger.cpp
HEADERS = logger.h$(TARGET): $(SOURCES) $(HEADERS)$(CXX) $(CXXFLAGS) -o $(TARGET) $(SOURCES)clean:rm -f $(TARGET) application.logrun: $(TARGET)./$(TARGET).PHONY: clean run

编译运行

make        # 编译程序
make run    # 运行程序

3.2 配置管理器(Configuration Manager)

应用场景
应用程序通常需要读取和管理配置文件,配置管理器应该:

  • 全局唯一:确保所有模块使用相同的配置
  • 懒加载:只在第一次使用时加载配置
  • 线程安全:支持多线程并发读取配置

完整实现代码

// config_manager.h
#ifndef CONFIG_MANAGER_H
#define CONFIG_MANAGER_H#include <string>
#include <unordered_map>
#include <mutex>
#include <memory>class ConfigManager {
public:static ConfigManager& getInstance();bool loadConfig(const std::string& filename);std::string getString(const std::string& key, const std::string& defaultValue = "") const;int getInt(const std::string& key, int defaultValue = 0) const;double getDouble(const std::string& key, double defaultValue = 0.0) const;bool getBool(const std::string& key, bool defaultValue = false) const;void setString(const std::string& key, const std::string& value);void setInt(const std::string& key, int value);void setDouble(const std::string& key, double value);void setBool(const std::string& key, bool value);bool saveConfig(const std::string& filename) const;// 删除拷贝构造函数和赋值运算符ConfigManager(const ConfigManager&) = delete;ConfigManager& operator=(const ConfigManager&) = delete;private:ConfigManager();~ConfigManager() = default;void parseLine(const std::string& line);void setDefaultValues();mutable std::mutex configMutex_;std::unordered_map<std::string, std::string> configMap_;std::string configFileName_;
};#endif // CONFIG_MANAGER_H
// config_manager.cpp
#include "config_manager.h"
#include <fstream>
#include <sstream>
#include <iostream>
#include <algorithm>
#include <cctype>ConfigManager& ConfigManager::getInstance() {static ConfigManager instance;return instance;
}ConfigManager::ConfigManager() {setDefaultValues();
}void ConfigManager::setDefaultValues() {std::lock_guard<std::mutex> lock(configMutex_);configMap_["server.host"] = "localhost";configMap_["server.port"] = "8080";configMap_["database.enabled"] = "true";configMap_["log.level"] = "info";configMap_["cache.size"] = "100";
}bool ConfigManager::loadConfig(const std::string& filename) {std::ifstream file(filename);if (!file.is_open()) {std::cerr << "Failed to open config file: " << filename << std::endl;return false;}std::lock_guard<std::mutex> lock(configMutex_);configFileName_ = filename;configMap_.clear();setDefaultValues(); // 重新设置默认值std::string line;while (std::getline(file, line)) {// 移除行首尾的空白字符line.erase(0, line.find_first_not_of(" \t"));line.erase(line.find_last_not_of(" \t") + 1);// 跳过空行和注释if (line.empty() || line[0] == '#') {continue;}parseLine(line);}file.close();return true;
}void ConfigManager::parseLine(const std::string& line) {size_t equalsPos = line.find('=');if (equalsPos == std::string::npos) {return; // 无效行}std::string key = line.substr(0, equalsPos);std::string value = line.substr(equalsPos + 1);// 移除key和value首尾的空白字符key.erase(0, key.find_first_not_of(" \t"));key.erase(key.find_last_not_of(" \t") + 1);value.erase(0, value.find_first_not_of(" \t"));value.erase(value.find_last_not_of(" \t") + 1);// 移除value可能的引号if (!value.empty()) {if ((value.front() == '"' && value.back() == '"') ||(value.front() == '\'' && value.back() == '\'')) {value = value.substr(1, value.size() - 2);}}if (!key.empty()) {configMap_[key] = value;}
}std::string ConfigManager::getString(const std::string& key, const std::string& defaultValue) const {std::lock_guard<std::mutex> lock(configMutex_);auto it = configMap_.find(key);return it != configMap_.end() ? it->second : defaultValue;
}int ConfigManager::getInt(const std::string& key, int defaultValue) const {std::string value = getString(key, "");if (value.empty()) {return defaultValue;}try {return std::stoi(value);} catch (...) {return defaultValue;}
}double ConfigManager::getDouble(const std::string& key, double defaultValue) const {std::string value = getString(key, "");if (value.empty()) {return defaultValue;}try {return std::stod(value);} catch (...) {return defaultValue;}
}bool ConfigManager::getBool(const std::string& key, bool defaultValue) const {std::string value = getString(key, "");if (value.empty()) {return defaultValue;}// 转换为小写进行比较std::string lowerValue;std::transform(value.begin(), value.end(), std::back_inserter(lowerValue),[](unsigned char c) { return std::tolower(c); });if (lowerValue == "true" || lowerValue == "yes" || lowerValue == "1") {return true;} else if (lowerValue == "false" || lowerValue == "no" || lowerValue == "0") {return false;}return defaultValue;
}void ConfigManager::setString(const std::string& key, const std::string& value) {std::lock_guard<std::mutex> lock(configMutex_);configMap_[key] = value;
}void ConfigManager::setInt(const std::string& key, int value) {setString(key, std::to_string(value));
}void ConfigManager::setDouble(const std::string& key, double value) {setString(key, std::to_string(value));
}void ConfigManager::setBool(const std::string& key, bool value) {setString(key, value ? "true" : "false");
}bool ConfigManager::saveConfig(const std::string& filename) const {std::ofstream file(filename);if (!file.is_open()) {return false;}std::lock_guard<std::mutex> lock(configMutex_);for (const auto& pair : configMap_) {file << pair.first << " = " << pair.second << std::endl;}file.close();return true;
}
// main_config.cpp
#include "config_manager.h"
#include <iostream>
#include <thread>void printConfig(const std::string& threadName) {auto& config = ConfigManager::getInstance();std::cout << threadName << " - Server: " << config.getString("server.host") << ":" << config.getInt("server.port") << std::endl;std::cout << threadName << " - Database enabled: " << std::boolalpha << config.getBool("database.enabled") << std::endl;std::cout << threadName << " - Log level: " << config.getString("log.level") << std::endl;std::cout << threadName << " - Cache size: " << config.getInt("cache.size") << " MB" << std::endl;
}void configReader(int threadId) {std::string threadName = "Thread_" + std::to_string(threadId);printConfig(threadName);
}int main() {// 创建示例配置文件std::ofstream configFile("app.conf");configFile << "# Application configuration\n";configFile << "server.host = 192.168.1.100\n";configFile << "server.port = 9090\n";configFile << "database.enabled = true\n";configFile << "log.level = debug\n";configFile << "cache.size = 256\n";configFile.close();// 加载配置文件if (!ConfigManager::getInstance().loadConfig("app.conf")) {std::cerr << "Failed to load configuration" << std::endl;return 1;}std::cout << "Main thread - Configuration loaded successfully\n";// 创建多个线程读取配置std::thread threads[3];for (int i = 0; i < 3; ++i) {threads[i] = std::thread(configReader, i);}// 修改一些配置值ConfigManager::getInstance().setString("server.host", "10.0.0.1");ConfigManager::getInstance().setInt("server.port", 8080);for (int i = 0; i < 3; ++i) {threads[i].join();}// 保存当前配置if (ConfigManager::getInstance().saveConfig("app_modified.conf")) {std::cout << "Configuration saved to app_modified.conf" << std::endl;}return 0;
}

Makefile配置

# Makefile for Config Manager example
CXX = g++
CXXFLAGS = -std=c++11 -pthread -Wall -WextraTARGET = config_example
SOURCES = main_config.cpp config_manager.cpp
HEADERS = config_manager.h$(TARGET): $(SOURCES) $(HEADERS)$(CXX) $(CXXFLAGS) -o $(TARGET) $(SOURCES)clean:rm -f $(TARGET) app.conf app_modified.confrun: $(TARGET)./$(TARGET).PHONY: clean run

编译运行

make        # 编译程序
make run    # 运行程序

4. 交互性内容解析

4.1 多线程环境下的交互分析

单例模式在多线程环境下的行为复杂性主要体现在实例化过程中。以下通过时序图详细分析不同实现方式的线程交互:

4.1.1 不安全懒汉式的竞态条件

ThreadAThreadBSingletonClass不安全懒汉式初始化getInstance()检查instance==nullptr确认instance为nullptrgetInstance()检查instance==nullptr确认instance为nullptr(此时还未创建)new Singleton()创建实例赋值给instancenew Singleton()创建另一个实例赋值给instance(覆盖前一个)产生两个实例,内存泄漏ThreadAThreadBSingletonClass

4.1.2 双检锁模式的正确交互

ThreadAThreadBMutexSingletonClassgetInstance()检查instance==nullptr确认instance为nullptrlock()获取互斥锁getInstance()检查instance==nullptr等待(因为instance仍为nullptr)再次检查instance==nullptr(双检)new Singleton()创建实例原子操作赋值给instanceunlock()释放互斥锁lock()获取互斥锁再次检查instance≠nullptr(双检)unlock()释放互斥锁返回已创建的实例ThreadAThreadBMutexSingletonClass

4.2 单例与依赖组件的交互

在实际应用中,单例对象往往需要与其他系统组件进行交互。以下以日志单例为例展示其与文件系统、网络服务的交互:

ClientLoggerSingletonFileSystemNetworkServicelog("Error occurred", ERROR)获取当前时间戳格式化日志消息write(log_file, message)写入成功sendLog(message)发送确认alt[网络日志启用]日志记录完成ClientLoggerSingletonFileSystemNetworkService

5. 总结与最佳实践

5.1 单例模式适用场景总结

单例模式在以下场景中特别有用:

  1. 资源共享场景:如数据库连接池、线程池等需要集中管理的资源
  2. 配置管理:应用程序的全局配置信息管理
  3. 日志记录:统一的日志记录系统
  4. 缓存系统:全局缓存管理
  5. 设备访问:如打印机后台处理服务

5.2 实现方式选择指南

实现方式优点缺点适用场景
饿汉式线程安全,实现简单可能浪费资源,启动慢实例小且必定使用
懒汉式(带锁)资源按需分配每次访问都需要加锁,性能差不频繁访问的单例
双检锁线程安全,性能较好实现复杂,需要C++11支持高性能要求的场景
Meyer’s Singleton简单,线程安全,自动销毁C++11以上支持现代C++项目首选

5.3 最佳实践建议

  1. 优先使用Meyer’s Singleton:在C++11及以上环境中,这是最简单安全的实现方式
  2. 考虑依赖注入:对于可测试性要求高的项目,考虑使用依赖注入替代单例
  3. 谨慎使用单例:单例本质上是全局状态,过度使用会导致代码耦合度高
  4. 注意销毁顺序:单例的销毁顺序可能影响其他静态对象的析构
  5. 提供重置机制:在测试环境中,提供重置单例状态的方法

5.4 现代C++改进

C++11及以上版本提供了更好的单例实现工具:

  • std::call_once:保证一次性初始化
  • thread_local:实现线程局部单例
  • 原子操作:实现无锁或低锁同步
  • 静态局部变量:线程安全的延迟初始化

单例模式是强大的工具,但需要谨慎使用。正确应用时,它可以提供优雅的解决方案;滥用时,它会导致代码难以维护和测试。始终考虑是否真的需要单例,或者是否有更好的替代设计方案。


文章转载自:

http://za3H3xpD.qwxhr.cn
http://wP3mWbs8.qwxhr.cn
http://VjRuiHEv.qwxhr.cn
http://pWwxLyMX.qwxhr.cn
http://3q3s7p9p.qwxhr.cn
http://T4MtQFtg.qwxhr.cn
http://OmFIcQO9.qwxhr.cn
http://vWRymNUx.qwxhr.cn
http://Fi7lpAOC.qwxhr.cn
http://uG2rLOfA.qwxhr.cn
http://f47kDiRu.qwxhr.cn
http://89Sge4g1.qwxhr.cn
http://zVohtySM.qwxhr.cn
http://66G520gl.qwxhr.cn
http://HnjggxgK.qwxhr.cn
http://yOb2CLm9.qwxhr.cn
http://KmzdgmjI.qwxhr.cn
http://VkR8dE45.qwxhr.cn
http://IyMrtQaL.qwxhr.cn
http://wTRr49aF.qwxhr.cn
http://CvnU6jFF.qwxhr.cn
http://sCqVlMiI.qwxhr.cn
http://x5DKNhsN.qwxhr.cn
http://5TO3ZJ40.qwxhr.cn
http://1tXquy6L.qwxhr.cn
http://nH3eQWKo.qwxhr.cn
http://42671wIH.qwxhr.cn
http://DzLbWxZe.qwxhr.cn
http://zKfwxdP1.qwxhr.cn
http://CeovrMkH.qwxhr.cn
http://www.dtcms.com/a/376518.html

相关文章:

  • Eyeshot 2025.3 3D 图形工具包
  • 【Linux手册】信号量与建造者模式:以 PV 操作保证并发安全,分步组装构建复杂对象
  • 【展厅多媒体】VR虚拟现实,构建展厅沉浸体验的重要技术
  • 京东京造K2 蓝牙/有线双模键盘键盘快捷键
  • Figma Make 输入指令浏览器无响应
  • 【设计模式】【观察者模式】实例
  • 【Linux手册】消息队列从原理到模式:底层逻辑、接口实战与责任链模式的设计艺术
  • 学习React-10-useTransition
  • Hive中的3种虚拟列以及Hive如何进行条件判断
  • 基于 C++ 的 IEC60870-5-104 规约的主从站模拟数据通信
  • css flex布局,设置flex-wrap:wrap换行后,如何保证子节点被内容撑高后,每一行的子节点高度一致。
  • 一款免费开源轻量的漏洞情报系统 | 漏洞情报包含:组件漏洞 + 软件漏洞 + 系统漏洞
  • 容器问答题上
  • uniapp发布成 微信小程序 主包内 main.wxss 体积太大
  • Uniapp中使用renderjs实现OpenLayers+天地图的展示与操作
  • 鸿蒙HAP包解包、打包、签名及加固全流程解析
  • [Leetcode 算法题单] 1456. 定长子串中元音的最大数目
  • 基于Springboot + vue实现的高校大学生竞赛项目管理系统
  • 为什么 socket.io 客户端在浏览器能连上,但在 Node.js 中报错 transport close?
  • Windows 命令行:切换盘符
  • 论文阅读记录之《VelocityGPT 》
  • 微服务通信实战篇:基于 Feign 的远程调用与性能优化
  • “双轮”驱动见成效 中和农信深耕乡村“最后一百米”
  • 高防IP怎样抵御CC攻击的频繁侵扰?
  • LeetCode 面试经典 150_矩阵_生命游戏(38_289_C++_中等)(额外状态)
  • Kotlin 2.2.20 现已发布!下个版本的特性抢先看!
  • Shell编程:计算鸡兔同笼问题
  • 如何解决pip安装报错ModuleNotFoundError: No module named ‘python-dateutil’问题
  • WenetSpeech-Yue数据集及其诞生之路
  • 用粒子群算法PSO优化BP神经网络改善预测精度