【设计模式精讲】4.单例模式(Singleton)

【设计模式精讲】4.单例模式(Singleton)

【摘要】:全局配置、日志器、线程池,这类对象在整个进程里只需要一份。多数人的第一反应是定义一个全局变量,随后便陷入初始化顺序失控、重复构造、多线程竞争的泥潭。本文从全局变量的三宗罪讲起,给出单例模式的意图与结构;重点拆解 C++ 中线程安全单例的三种写法------Meyers' Singleton、双检锁的坑与修正、std::call_once,并比较它们的行为差异。文章最后讨论单例的代价与替代方案,提醒读者:单例是受限的全局状态,能少用就少用。
【关键词】:单例模式、Meyers' Singleton、双检锁、std::call_once、C++

1. 一个全局配置对象引发的血案

新项目开工,你写了一个全局配置:

cpp 复制代码
// 说明性片段(省略 Config 的定义)
// config.h
Config g_config;  // ❌ 定义在头文件里

// a.cpp / b.cpp 都 include 了 config.h

链接器先报了重复定义;改成 extern Config g_config; 后,程序又在启动时随机崩溃------Logger 的构造函数在 g_config 初始化之前就读取了它。静态对象的初始化顺序在跨编译单元时是未定义的,这正是 C++ 著名的「静态初始化顺序惨案」(static initialization order fiasco)。等团队把配置挪进函数、日志改成懒加载,多线程模块上线后又出现了两份实例各写各的缓存。

三宗罪:初始化顺序不可控、构造时机不可控、多线程下不唯一。单例模式要解决的,就是「一个类恰好一个实例 + 全局可访问」这两件事,并且把「何时构造」的决定权收回到类自己手里。

2. 模式意图与定义

  • 一句话定义 (GoF 原文意图的译文):Ensure a class only has one instance, and provide a global point of access to it------保证一个类仅有一个实例,并提供一个访问它的全局访问点。Refactoring Guru 中文版亦称「单件模式」。
  • 解决的问题 :Refactoring Guru 指出单例同时解决两个问题------控制实例数量 (控制数据库连接、文件系统这类共享资源的访问,第二次「创建」拿到的是首次的那个对象)和 提供全局访问节点 (像全局变量一样好用,但实例被类保护、无法被外部代码覆盖)。代价是它一身兼两职,天然违反单一职责原则。GoF 的动机举例更直观:系统里可以有多台打印机,但打印后台(spooler)只能有一个;一个数字滤波器只能有一块 A/D 转换器。全局变量做不到「阻止你创建第二个」,所以更优解是 让类自己负责看管自己的唯一实例

GoF 的原始结构是把构造函数设为私有、静态成员 instance() 返回唯一实例。RG 把实现步骤归纳为四条:私有静态成员存实例、公有静态方法取实例、方法内做延迟初始化、构造函数设私有。经典写法是判断指针是否为空,本文第 4 节会看到它在 C++ 多线程下的问题与修正。

3. UML 图 + 结构说明

#mermaid-svg-m9DJ2niR9c9aOrkL{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-m9DJ2niR9c9aOrkL .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-m9DJ2niR9c9aOrkL .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-m9DJ2niR9c9aOrkL .error-icon{fill:#552222;}#mermaid-svg-m9DJ2niR9c9aOrkL .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-m9DJ2niR9c9aOrkL .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-m9DJ2niR9c9aOrkL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-m9DJ2niR9c9aOrkL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-m9DJ2niR9c9aOrkL .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-m9DJ2niR9c9aOrkL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-m9DJ2niR9c9aOrkL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-m9DJ2niR9c9aOrkL .marker{fill:#333333;stroke:#333333;}#mermaid-svg-m9DJ2niR9c9aOrkL .marker.cross{stroke:#333333;}#mermaid-svg-m9DJ2niR9c9aOrkL svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-m9DJ2niR9c9aOrkL p{margin:0;}#mermaid-svg-m9DJ2niR9c9aOrkL g.classGroup text{fill:#9370DB;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#mermaid-svg-m9DJ2niR9c9aOrkL g.classGroup text .title{font-weight:bolder;}#mermaid-svg-m9DJ2niR9c9aOrkL .cluster-label text{fill:#333;}#mermaid-svg-m9DJ2niR9c9aOrkL .cluster-label span{color:#333;}#mermaid-svg-m9DJ2niR9c9aOrkL .cluster-label span p{background-color:transparent;}#mermaid-svg-m9DJ2niR9c9aOrkL .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-m9DJ2niR9c9aOrkL .cluster text{fill:#333;}#mermaid-svg-m9DJ2niR9c9aOrkL .cluster span{color:#333;}#mermaid-svg-m9DJ2niR9c9aOrkL .nodeLabel,#mermaid-svg-m9DJ2niR9c9aOrkL .edgeLabel{color:#131300;}#mermaid-svg-m9DJ2niR9c9aOrkL .edgeLabel .label rect{fill:#ECECFF;}#mermaid-svg-m9DJ2niR9c9aOrkL .label text{fill:#131300;}#mermaid-svg-m9DJ2niR9c9aOrkL .labelBkg{background:#ECECFF;}#mermaid-svg-m9DJ2niR9c9aOrkL .edgeLabel .label span{background:#ECECFF;}#mermaid-svg-m9DJ2niR9c9aOrkL .classTitle{font-weight:bolder;}#mermaid-svg-m9DJ2niR9c9aOrkL .node rect,#mermaid-svg-m9DJ2niR9c9aOrkL .node circle,#mermaid-svg-m9DJ2niR9c9aOrkL .node ellipse,#mermaid-svg-m9DJ2niR9c9aOrkL .node polygon,#mermaid-svg-m9DJ2niR9c9aOrkL .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-m9DJ2niR9c9aOrkL .divider{stroke:#9370DB;stroke-width:1;}#mermaid-svg-m9DJ2niR9c9aOrkL g.clickable{cursor:pointer;}#mermaid-svg-m9DJ2niR9c9aOrkL g.classGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-m9DJ2niR9c9aOrkL g.classGroup line{stroke:#9370DB;stroke-width:1;}#mermaid-svg-m9DJ2niR9c9aOrkL .classLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-m9DJ2niR9c9aOrkL .classLabel .label{fill:#9370DB;font-size:10px;}#mermaid-svg-m9DJ2niR9c9aOrkL .relation{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-m9DJ2niR9c9aOrkL .dashed-line{stroke-dasharray:3;}#mermaid-svg-m9DJ2niR9c9aOrkL .dotted-line{stroke-dasharray:1 2;}#mermaid-svg-m9DJ2niR9c9aOrkL #compositionStart,#mermaid-svg-m9DJ2niR9c9aOrkL .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-m9DJ2niR9c9aOrkL #compositionEnd,#mermaid-svg-m9DJ2niR9c9aOrkL .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-m9DJ2niR9c9aOrkL #dependencyStart,#mermaid-svg-m9DJ2niR9c9aOrkL .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-m9DJ2niR9c9aOrkL #dependencyStart,#mermaid-svg-m9DJ2niR9c9aOrkL .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-m9DJ2niR9c9aOrkL #extensionStart,#mermaid-svg-m9DJ2niR9c9aOrkL .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-m9DJ2niR9c9aOrkL #extensionEnd,#mermaid-svg-m9DJ2niR9c9aOrkL .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-m9DJ2niR9c9aOrkL #aggregationStart,#mermaid-svg-m9DJ2niR9c9aOrkL .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-m9DJ2niR9c9aOrkL #aggregationEnd,#mermaid-svg-m9DJ2niR9c9aOrkL .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-m9DJ2niR9c9aOrkL #lollipopStart,#mermaid-svg-m9DJ2niR9c9aOrkL .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-m9DJ2niR9c9aOrkL #lollipopEnd,#mermaid-svg-m9DJ2niR9c9aOrkL .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-m9DJ2niR9c9aOrkL .edgeTerminals{font-size:11px;line-height:initial;}#mermaid-svg-m9DJ2niR9c9aOrkL .classTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-m9DJ2niR9c9aOrkL .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-m9DJ2niR9c9aOrkL .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-m9DJ2niR9c9aOrkL :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 调用 instance()
Singleton
-Singleton$ instance
-~configData : std::string
-Singleton()
-operator=(Singleton&)
+instance() : Singleton&
-doWork() : void
Client
构造/拷贝全部私有

instance() 是唯一入口

角色只有一个,但三道锁缺一不可:

  • 私有构造与析构 :外界无法 new、无法创建栈对象;
  • 禁用拷贝与赋值:否则「唯一」会被拷贝绕过;
  • 静态 instance():唯一访问点,同时负责「首次构造」。

4. 传统 C++ 写法(C++11 之前)

先按 GoF 原书时代的形态实现一遍。那时没有 = delete,禁拷贝的惯用法是「声明为 private 且不给出定义」:

cpp 复制代码
// 经典懒汉式(C++98/03 写法)
class Config {
public:
  static Config* instance() {
    if (s_inst == 0) {             // 两个线程可能
      s_inst = new Config();       // 同时通过判断
    }                              // 造成重复构造
    return s_inst;
  }

private:
  Config() {}                      // 私有构造
  Config(const Config&);           // ❌ 只声明不定义
  Config& operator=(const Config&);// 即可禁用拷贝

  static Config* s_inst;
};

Config* Config::s_inst = 0;

这一版的问题有两个。其一,多线程下不安全:线程 A 执行到 new Config() 分配了内存、还没构造完,线程 B 看到 s_inst 非空就拿去用------读到半成品对象。其二,即使构造受锁保护,普通指针赋值与构造完成之间没有顺序保证 ,直觉的修法是给整个函数加锁,但每次访问都加锁太贵,于是有了「双检锁」(DCLP)。以下以 POSIX pthread 接口为例(Linux 服务端的老代码里随处可见):

cpp 复制代码
// POSIX 平台片段:pthread 双检锁
#include <pthread.h>

class Config {
public:
  static Config* instance() {
    if (s_inst == 0) {             // 第一道检查:
      pthread_mutex_lock(&s_mtx);  // 无实例才抢锁
      if (s_inst == 0) {           // 第二道检查:
        s_inst = new Config();     // 抢到锁后再确认
      }
      pthread_mutex_unlock(&s_mtx);
    }
    return s_inst;
  }

private:
  Config() {}
  Config(const Config&);
  Config& operator=(const Config&);

  static pthread_mutex_t s_mtx;
  static Config* s_inst;
};

pthread_mutex_t Config::s_mtx =
    PTHREAD_MUTEX_INITIALIZER;
Config* Config::s_inst = 0;

严谨地说,这版仍不完美:new 可能被编译器重排成「先赋值指针、后执行构造」,第一道无锁检查就可能读到半成品。POSIX 给出的正解是 pthread_once------干脆不做双检:

cpp 复制代码
// POSIX 平台片段(Config 定义同上,略)
#include <pthread.h>

static pthread_once_t s_once =
    PTHREAD_ONCE_INIT;
static Config* s_cfg = 0;

static void initConfig() {
  s_cfg = new Config();
}

Config* getConfig() {
  pthread_once(&s_once, initConfig);
  return s_cfg;
}

可以看到,前 C++11 时代想写对一个懒汉单例,要么绑死平台接口,要么踩内存模型的坑------用 std::atomicacquire/release 语义堵住重排漏洞的标准 C++ 版双检锁也存在,但正确写法已经繁琐到需要逐行注释才能读懂。这正是下一节 Meyers' Singleton 胜出的原因:C++11 把这些补丁全部收进了语言本身

5. 现代 C++ 进阶写法

首选:Meyers' Singleton。C++11 起标准保证:局部静态变量的初始化是线程安全的(「如果控制流并发地进入声明,并发执行应当等待初始化完成」stmt.dcl)。于是双检锁的全部代码塌缩成三行:

cpp 复制代码
// ✅ C++11 起推荐的写法
#include <iostream>
#include <mutex>
#include <string>

class Logger {
public:
  static Logger& instance() {
    static Logger inst;  // 线程安全的首懒初始化
    return inst;
  }

  void log(const std::string& msg) {
    std::lock_guard<std::mutex> lk(m_mtx);
    std::cout << "[log] " << msg << "\n";
  }

private:
  Logger() = default;
  ~Logger() = default;
  Logger(const Logger&) = delete;
  Logger& operator=(const Logger&) = delete;

  std::mutex m_mtx;
};

使用方式:Logger::instance().log("hello");。要点有三:

  1. 返回引用而不是指针 ------指针可以被 delete 或被复制,引用语义上就是「那一个」;
  2. = delete 显式禁拷贝(C++11),比 GoF 时代私有不定义的 hack 清晰得多;
  3. 析构如需参与退出顺序,把析构也设为 private 并提供 destroy(),或干脆用 std::shared_ptr 管理静态实例。

备选:std::call_once。当构造逻辑无法塞进一个静态局部变量(比如要按参数选择子类)时:

cpp 复制代码
// 节选(Config 定义见上文,略)
#include <mutex>

static std::once_flag s_flag;
static Config* s_cfg = nullptr;

Config& getConfig() {
  std::call_once(s_flag, [] {
    s_cfg = new Config();  // 任意复杂的一次性构造
  });
  return *s_cfg;
}

三者怎么选:默认 Meyers' ;构造有分支/依赖注入需求用 call_once;双检锁仅在被迫维护旧代码或需要返回指针接口时出现,新代码不必手写。

还剩一个所有懒汉写法都绕不开的话题:销毁 。局部静态变量会在进程退出时、按与构造相反的顺序析构,听起来很美,但这个顺序只在「同一个线程先后触发初始化」时才成立。若单例 A 的析构函数里调用了单例 B,而 B 恰好在 A 之后析构,退出阶段就翻车了;更隐蔽的是 exit() 之后仍有后台线程在访问单例。工程上有两种成熟对策:一是 不死单例(leaky singleton) ,刻意 new 出来永不析构,把回收交给操作系统------进程级日志器、指标收集器常用这招,反正退出后资源都会被回收;二是 降级空实现,析构时把内部指针置空、后续调用变成安全空操作。大型框架几乎都选其一,第 7 节的三个真实库给出了各自的答案。

6. 优缺点与适用场景

  • ✅ 优点:严格保证唯一实例;懒加载,未用到不付出构造成本;访问点统一,接口集中。
  • ❌ 缺点:它就是全局状态------隐藏依赖、妨碍并行测试(单例无法在两个测试间替换)、跨编译单元的销毁顺序仍可能出问题;「唯一」约束在将来需求变化(突然要支持多租户/多实例)时是硬伤。
  • 🎯 适用场景:进程级天然唯一的资源------日志器、配置中心、线程池、硬件设备抽象。判断标准是「领域本身就是唯一的」,而不是「我图省事只想建一个」。

〔辨析〕单例 vs 全局变量:全局变量在 main 之前构造、顺序不可控;单例把构造推迟到首次访问,顺序由使用关系决定,且能挡住拷贝。单例 vs 静态类:静态成员函数全是过程的「伪单例」无法实现接口、无法虚函数分发,也拿不到实例语义。

关于测试再多说两句。单例妨害测试的根源是「类自己决定依赖谁」:TaxCalculator 内部直接调 Config::instance(),测试就无法替它换上一份假配置。缓解办法有层次的差别:最轻的是给单例留一个「重置/注入」接口(仅供测试编译期开启);更彻底的是 把唯一性上移 ------只在使用一根装配线的 main 里构造一份,往下用构造参数传递引用。后者其实就是依赖注入:类的代码看不出任何单例痕迹,「全局唯一」变成部署决策而不是类的设计,可测试性随之回归。这也解释了为什么 GoF 之后不少流派(如 Google 的代码可读性指南)都建议:单例能不进类就不进类。

7. 开源项目中的身影

三个真实 C++ 工程库对单例的处理,恰好对应本文讲过的三条技术路线,也各自暴露了不同的取舍。

AOSP(Android):锁保护的全局指针,以及一份「自我否定」 。Android 系统层的 native 库 libutils 提供了 android::Singleton<T> 模板,供 HAL、Binder 服务等系统组件复用(节选并简化自源码):

cpp 复制代码
// 节选自 AOSP system/core/libutils
// include/utils/Singleton.h(有简化)
namespace android {

template <typename TYPE>
class Singleton {
public:
  static TYPE& getInstance() {
    Mutex::Autolock _l(sLock);
    TYPE* instance = sInstance;
    if (instance == nullptr) {
      instance = new TYPE();
      sInstance = instance;
    }
    return *instance;
  }

  static bool hasInstance() {
    Mutex::Autolock _l(sLock);
    return sInstance != nullptr;
  }

protected:
  ~Singleton();

private:
  Singleton();
  static Mutex sLock;
  static TYPE* sInstance;
};

};  // namespace android

实现要点与本文第 4 节的 pthread 版几乎是同一套思路:Mutex::Autolock 是 RAII 风格的 lock_guard,构造放锁内、返回引用挡住外部 delete;配套宏 ANDROID_SINGLETON_STATIC_INSTANCE(TYPE) 负责在 cpp 文件里定义静态成员,绕开头文件重复定义问题。值得学习的反而是它的结局:这个头文件从 Android 9(P)起被标记废弃,官方注释直接建议改用 C++11 的局部静态变量写法------一个维护了十年的自研模板,最终被语言标准收编。这是「能上 Meyers' 就别手写锁」最有说服力的工程证据。

Boost:用 Meyers',但补上了「析构追踪」 。Boost.Serialization 里有一个被多个库借用的 boost::serialization::singleton<T>,核心骨架(节选并简化):

cpp 复制代码
// 节选自 boost/serialization/singleton.hpp
// (有简化)
namespace boost {
namespace serialization {

template <class T>
class singleton {
public:
  static T& get_instance() {
    // 局部静态:线程安全依赖 C++11
    // (旧编译器另有锁兜底分支)
    static detail::singleton_wrapper<T>
        t;
    return static_cast<T&>(t);
  }

  // 供序列化框架在退出阶段查询:
  // 单例是否已被析构
  static bool is_destroyed() {
    return detail::singleton_wrapper<
        T>::is_destroyed();
  }
  // ...(细节略)
};

}  // namespace serialization
}  // namespace boost

它用的正是本文推荐的 Meyers' 路线,但多包了一层 singleton_wrapper<T>:包装类的析构函数会把一个静态标志位置真,is_destroyed() 据此回答「现在还能不能用」。为什么需要这个?序列化框架的全局类型注册表活到进程尾声,而退出阶段各静态对象的析构顺序不可控------某个延迟的序列化操作可能撞上已析构的注册表。Boost 用「可查询的死亡状态」把未定义行为变成可检测的错误,这提醒我们:单例的麻烦一半在构造,另一半在析构,而后者 Meyers' 也只解决一半。

POCO:不设防的 SingletonHolder,把责任交还给使用者 。POCO 基础库的风格截然不同,它的 Poco::SingletonHolder<S> 模板短得可以整段贴进来(节选自源码,略去版权注释):

cpp 复制代码
// 节选自 POCO Foundation include/Poco/SingletonHolder.h
namespace Poco {

template <class S>
class SingletonHolder {
public:
  SingletonHolder() : _pS(0) {}
  ~SingletonHolder() { delete _pS; }

  S* get() {
    if (_pS == 0) _pS = new S();
    return _pS;
  }

private:
  SingletonHolder(const SingletonHolder&);
  SingletonHolder& operator=(
      const SingletonHolder&);

  S* _pS;
};

}  // namespace Poco

注意 get()没有任何锁 ------它不是线程安全的。这不是疏忽,而是设计取舍:POCO 把这个模板定位为「单例语义的脚手架」,线程安全由具体使用方按需包一层(例如 Poco::Util::Applicationinstance() 在其上层另行保证)。它同时用析构函数 delete 持有的指针,把生命周期收在 holder 手里,与 Boost「故意 leak 或追问死亡状态」是两种相反的哲学。三份代码放在一起看,同一个模式在不同工程里分别长成了「加锁」「包 wrapper」「裸模板」三种形态------模式给出意图,实现细节永远由具体工程的风险点决定

反模式提醒收尾:如果发现单例只是为了让「到处都能调到」,先用依赖注入(把 Logger& 作为参数传进去)重写一遍试试------很多「必须单例」的类,其实只是「懒得接线」。AOSP 的废弃通知、Boost 的死亡追踪、POCO 的不设防,说的都是同一件事:单例写对只是及格线,生命周期与并发边界才是真正的考题。

本篇小结

单例模式的全部价值浓缩在 instance() 一个函数里:把唯一性、构造时机与线程安全收进类内部 。C++11 后首选 Meyers' Singleton(局部静态 + 引用返回 + =delete 拷贝),需要复杂初始化时用 std::call_once,双检锁留给历史代码。而比「怎么写对」更重要的是「要不要写」:单例是受限的全局状态,用在天然唯一的资源上,而不是绕过设计的捷径。下一篇工厂方法,我们把注意力从「唯一实例」转向「实例从哪里来」。

本文模式定义与实现步骤参考了 Refactoring Guru《设计模式》中文版「单例」一章,意图译文与动机示例参考了 GoF《Design Patterns》第 3 章 Singleton 一节。

相关推荐
wind1003225 分钟前
VC++ 运行库 vcredist 安装失败|CLR 80131522 报错完整修复
开发语言·c++·游戏
雾削木30 分钟前
2026 编程领域热点知识速览:AI 辅助开发、设计模式实战与嵌入式高效迭代
人工智能·设计模式
OPEN-F7 小时前
C++进阶教程:继承与多态
开发语言·c++
luj_17689 小时前
大律师考核应重能力与科技素养
c语言·开发语言·c++·经验分享·算法
刃神太酷啦9 小时前
Linux 系统 MySQL 完整安装配置教程:从卸载 MariaDB 到优化 my.cnf----《Hello MySQL!》(1)
android·linux·c语言·c++·mysql·leetcode·mariadb
汉克老师12 小时前
CSP-J 初赛(以满分为目标):第十五课《结构体、排序与自定义比较——从“给数字排队”到“给一群人排队”》
c++·csp-j·小学生·学c++编程
HugoStudio_SWAN13 小时前
C++ CMD 互动动画:按键触发爆炸效果
开发语言·c++·学习·程序人生
水饺编程13 小时前
第5章,[Win32 章节] :Polygon 函数和多边形填充模式
c语言·c++·windows·visual studio
wuminyu13 小时前
Java FFM处理网络读写事件源码剖析
java·linux·c语言·jvm·c++