【设计模式精讲】13.装饰器模式(Decorator)

【设计模式精讲】13.装饰器模式(Decorator)

【摘要】:给指标上报加校验、加缓存、加日志,用继承来做就是灾难:三个维度自由组合,子类数量按 2 的幂次膨胀,而且全是「真子类不该有的职责」。本文从指标管道的继承爆炸讲起,给出装饰器模式的 GoF 意图------同接口包同接口的洋葱结构,动态叠加职责、按需拆装;C++98 版用裸指针级联销毁,现代版换成 unique_ptr 链、std::function 中间件与 CRTP mixin 三档实现,从运行时灵活到编译期零开销。文末对照 Boost.Iostreams 的过滤流、POCO 的计数流与 C++20 ranges 的视图管道。读完你能判断哪些增强适合「叠」上去,哪些老老实实写进类里。

【关键词】:装饰器、继承爆炸、洋葱结构、中间件、CRTP mixin、组合优于继承

【代码基准】:C++17

1. 三个增强维度,八个子类起步

指标上报模块要升级:样本先校验(丢弃空名与 NaN)、高频样本攒批缓存、每次调用留日志。第一反应是继承:

cpp 复制代码
// 说明性片段(省略部分子类)
// ❌ 继承爆炸
class MetricSink {
public:
  virtual ~MetricSink() = default;
  virtual void report(const std::string& name,
                      double value) = 0;
};

class ValidatingSink : public MetricSink { /*...*/ };
class LoggingSink : public MetricSink { /*...*/ };
class LoggingValidatingSink
    : public MetricSink { /*...*/ };
class ValidatingLoggingSink
    : public MetricSink { /*...*/ };
// ......组合还在继续

三个增强维度,理论组合 2³ = 8 种(每种还要「要/不要」),写四种就已经在 LoggingValidatingSinkValidatingLoggingSink 这种命名地狱里打转。更糟的是语义:继承声明的是「是一个 」------「带日志的 Sink 是一种 Sink」勉强成立,「带校验的带日志的 Sink」是什么东西?这些增强的共同点其实是:不改变上报的本职,只在调用前后叠加行为,而且希望按环境拆装------联调时全开,线上只开校验,压测时全关。

用继承做增强还踩了第 2 篇的两条原则:继承应表达「is-a」而非「has-enhancement」;组合优先于继承。装饰器模式把这两句话落成了结构。

2. 模式意图与定义

  • 一句话定义 :动态地给对象附加额外职责;就扩展功能而言,装饰器比生成子类更灵活。
    解决的问题:多个独立增强维度的自由组合导致的继承爆炸,以及「增强想按需拆装」的运行期需求。
  • GoF 原文意图Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality. (动态地给一个对象附加职责。)它的别名又是 wrapper------与适配器(第 10 篇)共享别名而意图不同:适配器换形状,装饰器加职责。
  • Refactoring Guru 的表述:装饰器是一种结构型模式,允许将对象放入包含行为的特殊「包装」对象中,为原对象绑定新行为------每层包装都是同一个接口,因此可以层层嵌套。

核心结构定性只有一句话:装饰器与被装饰者实现同一接口,并持有该接口的一个实例。调用从最外层进入,逐层加工后抵达核心对象------洋葱结构。这一个约束同时解决了继承爆炸(n 个装饰器代替 2ⁿ 个子类)与拆装需求(换一层包装就换一种组合,运行时装配)。

与子类继承的对照是本篇的主轴:继承是 类级、静态、全量 的增强------新子类一刻不停地带着那份增强;装饰是 对象级、动态、增量 的增强------同一核心对象可以在不同装配里穿不同外衣。GoF 的选型判断也抄在这里:「逐步叠加功能,而不是一次倾倒」时用装饰器。

3. UML 图 + 结构说明

#mermaid-svg-MWoA4gTYVXEtQPpK{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-MWoA4gTYVXEtQPpK .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-MWoA4gTYVXEtQPpK .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-MWoA4gTYVXEtQPpK .error-icon{fill:#552222;}#mermaid-svg-MWoA4gTYVXEtQPpK .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-MWoA4gTYVXEtQPpK .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-MWoA4gTYVXEtQPpK .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-MWoA4gTYVXEtQPpK .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-MWoA4gTYVXEtQPpK .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-MWoA4gTYVXEtQPpK .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-MWoA4gTYVXEtQPpK .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-MWoA4gTYVXEtQPpK .marker{fill:#333333;stroke:#333333;}#mermaid-svg-MWoA4gTYVXEtQPpK .marker.cross{stroke:#333333;}#mermaid-svg-MWoA4gTYVXEtQPpK svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-MWoA4gTYVXEtQPpK p{margin:0;}#mermaid-svg-MWoA4gTYVXEtQPpK g.classGroup text{fill:#9370DB;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#mermaid-svg-MWoA4gTYVXEtQPpK g.classGroup text .title{font-weight:bolder;}#mermaid-svg-MWoA4gTYVXEtQPpK .cluster-label text{fill:#333;}#mermaid-svg-MWoA4gTYVXEtQPpK .cluster-label span{color:#333;}#mermaid-svg-MWoA4gTYVXEtQPpK .cluster-label span p{background-color:transparent;}#mermaid-svg-MWoA4gTYVXEtQPpK .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-MWoA4gTYVXEtQPpK .cluster text{fill:#333;}#mermaid-svg-MWoA4gTYVXEtQPpK .cluster span{color:#333;}#mermaid-svg-MWoA4gTYVXEtQPpK .nodeLabel,#mermaid-svg-MWoA4gTYVXEtQPpK .edgeLabel{color:#131300;}#mermaid-svg-MWoA4gTYVXEtQPpK .edgeLabel .label rect{fill:#ECECFF;}#mermaid-svg-MWoA4gTYVXEtQPpK .label text{fill:#131300;}#mermaid-svg-MWoA4gTYVXEtQPpK .labelBkg{background:#ECECFF;}#mermaid-svg-MWoA4gTYVXEtQPpK .edgeLabel .label span{background:#ECECFF;}#mermaid-svg-MWoA4gTYVXEtQPpK .classTitle{font-weight:bolder;}#mermaid-svg-MWoA4gTYVXEtQPpK .node rect,#mermaid-svg-MWoA4gTYVXEtQPpK .node circle,#mermaid-svg-MWoA4gTYVXEtQPpK .node ellipse,#mermaid-svg-MWoA4gTYVXEtQPpK .node polygon,#mermaid-svg-MWoA4gTYVXEtQPpK .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-MWoA4gTYVXEtQPpK .divider{stroke:#9370DB;stroke-width:1;}#mermaid-svg-MWoA4gTYVXEtQPpK g.clickable{cursor:pointer;}#mermaid-svg-MWoA4gTYVXEtQPpK g.classGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-MWoA4gTYVXEtQPpK g.classGroup line{stroke:#9370DB;stroke-width:1;}#mermaid-svg-MWoA4gTYVXEtQPpK .classLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-MWoA4gTYVXEtQPpK .classLabel .label{fill:#9370DB;font-size:10px;}#mermaid-svg-MWoA4gTYVXEtQPpK .relation{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-MWoA4gTYVXEtQPpK .dashed-line{stroke-dasharray:3;}#mermaid-svg-MWoA4gTYVXEtQPpK .dotted-line{stroke-dasharray:1 2;}#mermaid-svg-MWoA4gTYVXEtQPpK #compositionStart,#mermaid-svg-MWoA4gTYVXEtQPpK .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-MWoA4gTYVXEtQPpK #compositionEnd,#mermaid-svg-MWoA4gTYVXEtQPpK .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-MWoA4gTYVXEtQPpK #dependencyStart,#mermaid-svg-MWoA4gTYVXEtQPpK .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-MWoA4gTYVXEtQPpK #dependencyStart,#mermaid-svg-MWoA4gTYVXEtQPpK .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-MWoA4gTYVXEtQPpK #extensionStart,#mermaid-svg-MWoA4gTYVXEtQPpK .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-MWoA4gTYVXEtQPpK #extensionEnd,#mermaid-svg-MWoA4gTYVXEtQPpK .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-MWoA4gTYVXEtQPpK #aggregationStart,#mermaid-svg-MWoA4gTYVXEtQPpK .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-MWoA4gTYVXEtQPpK #aggregationEnd,#mermaid-svg-MWoA4gTYVXEtQPpK .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-MWoA4gTYVXEtQPpK #lollipopStart,#mermaid-svg-MWoA4gTYVXEtQPpK .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-MWoA4gTYVXEtQPpK #lollipopEnd,#mermaid-svg-MWoA4gTYVXEtQPpK .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-MWoA4gTYVXEtQPpK .edgeTerminals{font-size:11px;line-height:initial;}#mermaid-svg-MWoA4gTYVXEtQPpK .classTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-MWoA4gTYVXEtQPpK .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-MWoA4gTYVXEtQPpK .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-MWoA4gTYVXEtQPpK :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 核心实现
装饰器也是组件
持有下一层
<<interface>>
MetricSink
+report(name, value) : void
ConsoleSink
+report(name, value) : void
<<abstract>>
MetricDecorator
#inner_ MetricSink
ValidatingSink
+report(name, value) : void
LoggingSink
+report(name, value) : void

四个参与者:

  • 组件(Component)接口MetricSink,核心对象与装饰器共有的接口------「装饰器也是组件」是全图的关键边;
  • 具体组件(Concrete Component)ConsoleSink,真正干本职的对象;
  • 装饰器基类 :持有 inner_,通常把接口操作原样转发(保持「透明」);
  • 具体装饰器:在转发前后插进自己的行为------校验丢弃、日志计时。

结构上有两点值得盯着图看:其一,MetricDecorator o-- MetricSink 的组合边指回接口本身,所以洋葱可以 任意深度递归 ------ValidatingSink 里包的可以是另一个 ValidatingSink(双重校验虽然荒谬但结构合法);其二,客户端只认 MetricSink,它拿到的引用既可能是核心对象也可能是任意一层装饰,分不出来正是设计目标(透明性),但也埋下了调试时类型不直观的伏笔(第 6 节缺点)。

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

C++98 形态:装饰器基类持裸指针、析构时级联销毁整个洋葱,禁拷贝照旧是私有不定义。GoF 原书的示例正是流------TextViewScrollDecoratorBorderDecorator 一层层包住:

cpp 复制代码
// C++98/03 写法
#include <stdio.h>
#include <string>

// ---- 组件接口 ----
class MetricSink {
public:
  virtual ~MetricSink() {}
  virtual void report(const std::string& name,
                      double value) = 0;
};

// ---- 具体组件 ----
class ConsoleSink : public MetricSink {
public:
  void report(const std::string& n, double v) {
    printf("[metric] %s = %.1f\n",
           n.c_str(), v);
  }
};

// ---- 装饰器基类:持有下一层 ----
class MetricDecorator : public MetricSink {
public:
  explicit MetricDecorator(MetricSink* inner)
      : inner_(inner) {}

  ~MetricDecorator() {
    delete inner_;   // 级联:剥一层删一层
  }

protected:
  MetricSink* inner_;

private:
  MetricDecorator(const MetricDecorator&);
  MetricDecorator& operator=(const MetricDecorator&);
};

// ---- 具体装饰器一:校验(丢弃非法样本)----
class ValidatingSink : public MetricDecorator {
public:
  ValidatingSink(MetricSink* inner)
      : MetricDecorator(inner) {}

  void report(const std::string& n, double v) {
    if (n.empty() || v != v)   // 空名或 NaN
      return;                  // 丢掉
    inner_->report(n, v);      // 放行
  }
};

// ---- 具体装饰器二:日志(记录再转发)----
class LoggingSink : public MetricDecorator {
public:
  LoggingSink(MetricSink* inner)
      : MetricDecorator(inner) {}

  void report(const std::string& n, double v) {
    printf("[log] report %s\n", n.c_str());
    inner_->report(n, v);
  }
};

// ---- 装配:洋葱从里往外长 ----
MetricSink* pipeline = new LoggingSink(
    new ValidatingSink(new ConsoleSink()));

pipeline->report("cpu.load", 0.42);
// 输出:[log] report cpu.load
//       [metric] cpu.load = 0.4

delete pipeline;   // 删一次,三层析构级联

三条传统写法的铁律:裸指针洋葱只能删最外层一次 ------每层析构删自己的 inner_,级联到核心,中途任何一层被单独 delete 就是双重释放;装饰器基类只转发、不加料 ------「保持透明」是纪律,谁在基类里顺手加了行为,下游装饰器的语义就开始分叉;装配发生在使用前 ------pipeline 的三层结构在构造期定型,运行期换装(给热路径临时剥掉日志层)需要重新装配整条链,裸指针时代没人敢轻易做。

5. 现代 C++ 进阶写法

升级零:unique_ptr 链,洋葱所有权自动化 。级联 delete 与禁拷贝样板全部消失,所有权语义由类型自证:

cpp 复制代码
// 节选(MetricSink/ConsoleSink 同第 4 节)
#include <memory>
#include <string>

class ValidatingSink : public MetricSink {
public:
  explicit ValidatingSink(
      std::unique_ptr<MetricSink> inner)
      : inner_(std::move(inner)) {}

  void report(const std::string& n, double v)
      override {
    if (n.empty() || v != v)
      return;
    inner_->report(n, v);
  }

private:
  std::unique_ptr<MetricSink> inner_;
};

// 装配:从里向外,层层 make_unique
auto pipeline = std::make_unique<LoggingSink>(
    std::make_unique<ValidatingSink>(
        std::make_unique<ConsoleSink>()));

拷贝自动禁、移动自动对、析构自动级联。装配顺序即执行顺序的倒影:make_unique 最先构造最里层,执行时调用最后才到达------读这段代码的技巧是「先里后外构造,先外后里执行」。

改进一:中间件式------用 std::function 装饰函数。接口只有一个操作时(或先把窄接口收敛成一个函数对象),洋葱可以退化成闭包链,免掉整套类层次:

cpp 复制代码
#include <cstdio>
#include <functional>
#include <string>

// 目标形态:一个可调用的上报函数
using Report = std::function<void(
    const std::string&, double)>;

Report withValidation(Report next) {
  return [next = std::move(next)](
             const std::string& n, double v) {
    if (n.empty() || v != v)
      return;
    next(n, v);               // 放行给下一层
  };
}

Report base = [](const std::string& n, double v) {
  std::printf("[metric] %s = %.1f\n",
              n.c_str(), v);
};

Report guarded = withValidation(base);
guarded("cpu.load", 0.42);

每层闭包按值捕获下一层,结构与类版洋葱完全同构,但表达力压缩了一个数量级。代价是 std::function 的类型擦除:可能的一次堆分配、内联不可达、调用多一层间接------热路径上要掂量(普通频率的日志、校验完全无感)。

改进二:CRTP mixin------编译期洋葱。增强维度在编译期就能定死的场景,模板继承把洋葱搬进类型系统,虚函数与间接层全部消失:

cpp 复制代码
#include <cstdio>
#include <string>

// 每层 mixin 继承「下一层」,转发即静态调用
template <typename Next>
struct WithValidation : Next {
  void report(const std::string& n, double v) {
    if (n.empty() || v != v)
      return;
    Next::report(n, v);
  }
};

struct ConsoleCore {
  void report(const std::string& n, double v) {
    std::printf("[metric] %s = %.1f\n",
                n.c_str(), v);
  }
};

using Sink = WithValidation<ConsoleCore>;
Sink s;               // 类型即洋葱:s 的
s.report("cpu.load", 0.42);   // report 可被内联

编译期洋葱的三个红利:零开销(调用链可完全内联)、类型可辨识(Sink 就是确定的组合,sizeof、traits 都可查)、装配错误提前到编译期。代价对称:不能运行时按配置拆装。这与第 6 篇工厂、第 10 篇适配器的「模板换运行期多态」是同一条路线------组合的自由度,向编译期要性能

展望 :C++23 的显式对象形参(deducing this)让 mixin 不再需要重复写 CRTP 样板;C++20 ranges 的视图管道 views::filter | views::transform 则是洋葱思想的标准库化------每个视图包住前一个范围对象,本文第 7 节回头看它。

6. 优缺点与适用场景

  • ✅ 优点(GoF 后果清单):比静态继承 更灵活 ------增强按对象、按时刻施加与撤销;避免「功能蔓延」的复杂类层次------一个简单类配可丢弃的小装饰器,而不是塞满选项的巨类;职责可拆可并------校验、日志各自独立成装饰器,能任意组合、按序叠放;与组合模式(第 12 篇)天然衔接,产生「递归的装饰结构」。
  • ❌ 缺点(GoF 原话警示):装饰器与组件 「同型不同类」 ------客户端无法依赖类型识别拿到的是核心还是装饰(dynamic_cast 查到的是最外层),需要「剥洋葱取核心」的场合要专门开口子;接口一多,每个具体装饰器都得把全部操作转发一遍,样板随接口面积膨胀;多层洋葱 调试阅读成本高 ------栈里三个同名 report,得数参数才能定位在哪层;间接调用与缓存不友好的调用链在极端热路径有可测开销。
  • 🎯 适用场景:可自由叠加的 横切增强 ------校验、日志、计时、重试、压缩、加密、限流;增强要 按环境拆装(开发/生产不同装配);增强维度 ≥ 2 且会组合出现(单维增强直接写进子类或类里更清楚);库想让用户「自己叠出想要的行为」(中间件、过滤器链)。

〔辨析〕装饰器 vs 子类继承:继承是类级静态全量增强,装饰是对象级动态增量增强;维度多且可组合时,n 个装饰器对 2ⁿ 个子类。装饰器 vs 代理(第 16 篇,重点辨析):结构上都是「同接口包一个同接口」,装饰器意在增强------客户端知道且要的就是这层行为;代理意在控制------客户端以为在跟本体打交道。装饰器 vs 适配器(第 10 篇):一个同接口加职责,一个异接口换形状。完整对照见第 17 篇。

7. 开源项目中的身影

Boost.Iostreams:过滤流是装饰器的全家桶filtering_ostream 把若干 filter(装饰器)与一个 device(核心组件)串成洋葱,push 的顺序就是数据穿层的顺序:

cpp 复制代码
// 说明性片段(需链接 Boost.Iostreams)
#include <boost/iostreams/device/file.hpp>
#include <boost/iostreams/filter/gzip.hpp>
#include <boost/iostreams/filtering_stream.hpp>

namespace io = boost::iostreams;

io::filtering_ostream out;
out.push(io::gzip_compressor());   // 外层:压缩
out.push(io::file_sink("log.gz")); // 内核:写文件
out << "每一层 filter 包住下一层\n";

点评:想加密就在 push 一层 crypt filter,想统计字节数再 push 一层计数------这正是 GoF 流示例的现代工业化版本。对照第 6 节缺点也一目了然:filtering_ostream 把「装配」本身做成了流类型,洋葱结构对使用者显式可见,缓解了调试不直观的问题。

POCO:一个装饰器一个类,家族化生产 。POCO Foundation 在流上提供了成打的「包装流」:CountingInputStream 包住任意 istream 顺手统计读了多少字节,同族的还有 Base64Encoder/HexifyEncoder(包住 ostream 做编码写出)等:

cpp 复制代码
// 说明性片段(需链接 PocoFoundation)
#include <Poco/CountingInputStream.h>
#include <fstream>

std::ifstream in("data.bin");
Poco::CountingInputStream counter(in);
char buf[256];
counter.read(buf, sizeof buf);
// 从 counter 读 == 从 in 读,
// 且 counter.charsRead() 已自动累加

点评:POCO 没有做一个万能「带一切增强的流」,而是把每种增强做成可独立叠加的小装饰器------「简单类 + 可丢弃装饰器」的忠实执行。对比某些框架把 gzip、base64、计数全焊死在一个 SuperStream 里,维护性的差距就是模式的差距。

标准库展望:ranges 视图管道(C++20)std::views 的组合语法把洋葱写进了语言层:

cpp 复制代码
// C++20 片段:每层视图包住前一层
#include <ranges>
#include <vector>

std::vector<int> xs{1, 2, 3, 4, 5};
auto pipe = xs
    | std::views::filter(
          [](int x) { return x % 2; })
    | std::views::transform(
          [](int x) { return x * 10; });
// pipe 是洋葱:filter 层包住 xs,
// transform 层又包住 filter 层

点评:管道符 | 只是语法糖,结构上 transform_view(filter_view(vector))LoggingSink(ValidatingSink(ConsoleSink)) 一模一样------装饰器的运行期自由换成了视图的惰性求值与零拷贝。它同时印证了本篇的取舍曲线:ranges 把组合定在编译期、换来内联与惰性;这正是「CRTP mixin」路线的标准库化。

三份代码合看:Boost 把洋葱显式化成可装配的流,POCO 把增强家族化成小装饰器,标准库(C++20)把包装组合升格为语法------结构三十年未变,变化的是 C++ 让「叠」这件事越来越便宜。

本篇小结

多维度、可组合、想拆装的增强,继承给不出答案------2ⁿ 个子类是把「组合」错当成「分类」。装饰器用一条结构约束解局:装饰器与被装饰者同接口、持有同接口的下一层,洋葱从里向外构造、从外向里执行;增强发生在转发的前后,核心对象的职责一个字节不多。实现按需取三档:unique_ptr 链是现代默认,所有权与级联析构自动化;单操作窄接口用 std::function 中间件,一个闭包一层洋葱;编译期能定死的组合交给 CRTP mixin,换零开销与类型可辨识。判断何时不该用同样重要:需要按类型识别组件、要剥出核心对象、或者增强只有一种不会组合------老老实实写进类里,比洋葱便宜。

本文模式定义与角色划分参考了 Refactoring Guru《设计模式》中文版「装饰器」一章,意图译文、流示例与后果清单参考了 GoF《Design Patterns》第 4 章 Decorator 一节。

相关推荐
OPEN-F1 小时前
C++综合实战:面向对象图书管理系统
开发语言·c++
NeoGressAI外贸数字化1 小时前
外贸独立站零询盘排查:从 Google Search Console 到 PageSpeed 的技术实操
开发语言·c++
神仙别闹2 小时前
基于 QT(C++)实现操作系统
数据库·c++·qt
无小道2 小时前
C/C++——异步编程小记
开发语言·c++·c++11
奇树谦2 小时前
Pimpl 模式(d-pointer)详解:如何解决 C++ 头文件过大、编译依赖和 ABI 兼容问题
开发语言·c++
shylyly_2 小时前
stack/queue中的deque
数据结构·c++·deque·双端队列·queue·stack·容器适配器
raindayinrain2 小时前
c++泛型编程
c++·函数模板·类模板·泛型编程
OPEN-F2 小时前
C++综合实战:网络编程入门与HTTP客户端
网络·c++·http
十五年专注C++开发2 小时前
CMake基础:BUILD_INTERFACE 与 INSTALL_INTERFACE 完全解析
c++·cmake·build_interface