一、设计模式是什么?先聊聊我的理解

设计模式,说白了就是前辈们踩过无数坑之后总结出来的「套路」。不是 C++ 特有的,但 C++ 里用起来特别顺手。

我个人习惯把设计模式分成三类:创建型、结构型、行为型。今天咱们聊创建型,就是管「对象怎么生出来」的。你想想看,一个对象 new 出来很简单,但什么时候 new、谁来 new、new 几个,这里头学问大了。

创建型模式的核心思想:把对象的创建和使用解耦。调用方不需要知道对象是怎么造出来的,只需要拿到就能用。

好,咱们一个一个来看。

二、单例模式(Singleton)

2.1 什么是单例?

全局只有一个实例。比如日志系统、配置管理器、线程池——这些东西你不需要两个副本,一个就够了。

我在项目中遇到过一个问题:日志类没做单例,结果两个模块各 new 了一个,写日志时互相覆盖。嗯,从那以后,日志类我必用单例。

2.2 C++ 实现要点

C++ 里实现单例,有几个关键点:

  • 构造函数私有化——不让外面 new
  • 拷贝构造和赋值操作要 delete 掉
  • 提供一个静态方法获取实例

最简单的写法(饿汉式):

class Singleton {
public:
    static Singleton& getInstance() {
        static Singleton instance;
        return instance;
    }
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
private:
    Singleton() {}
    ~Singleton() {}
};

小提示:C++11 之后,局部 static 变量是线程安全的。所以上面这个写法在多线程环境下没问题。我以前用老标准时还得加锁,现在省心了。

2.3 注意事项

  • 不要滥用单例。我见过有人把每个类都做成单例,那还不如写全局变量呢。
  • 单例的析构时机要小心。如果单例 A 依赖单例 B,析构顺序不可控,可能 crash。
  • 测试时单例比较麻烦,因为状态是全局的。可以考虑用依赖注入来替代。

我曾经踩过的坑:在单例的析构函数里调用了另一个单例的方法,结果那个单例已经被销毁了。程序退出时崩溃,查了两天才找到原因。建议:单例之间不要互相依赖,或者用智能指针管理生命周期。

三、工厂模式(Factory)

3.1 简单工厂

一个工厂类,根据参数返回不同的产品对象。说白了就是一个 switch-case 或者 if-else。

enum class ProductType { A, B };

class Product {
public:
    virtual void use() = 0;
    virtual ~Product() = default;
};

class ProductA : public Product {
public:
    void use() override { std::cout << "Product A\n"; }
};

class ProductB : public Product {
public:
    void use() override { std::cout << "Product B\n"; }
};

class SimpleFactory {
public:
    static std::unique_ptr<Product> create(ProductType type) {
        switch (type) {
            case ProductType::A: return std::make_unique<ProductA>();
            case ProductType::B: return std::make_unique<ProductB>();
            default: return nullptr;
        }
    }
};

简单工厂的问题在于:每加一种新产品,就得改工厂代码。违反了开闭原则。不过小项目用用还行,别太较真。

3.2 工厂方法

把工厂抽象成接口,每个产品对应一个具体工厂。这样加新产品时不用改已有代码,只要加一个新工厂类就行。

class Factory {
public:
    virtual std::unique_ptr<Product> create() = 0;
    virtual ~Factory() = default;
};

class FactoryA : public Factory {
public:
    std::unique_ptr<Product> create() override {
        return std::make_unique<ProductA>();
    }
};

class FactoryB : public Factory {
public:
    std::unique_ptr<Product> create() override {
        return std::make_unique<ProductB>();
    }
};

我的经验:工厂方法适合产品种类多、经常扩展的场景。但别过度设计——如果就两三个产品,简单工厂完全够用。

3.3 抽象工厂

工厂方法生产一种产品,抽象工厂生产一族产品。比如 GUI 库:Windows 风格有一整套按钮、菜单、文本框;Mac 风格也有一整套。抽象工厂就是管这个的。

抽象工厂的复杂度比较高,我一般只在跨平台库或者插件系统里用。平时写业务代码,工厂方法就够用了。

四、建造者模式(Builder)

4.1 什么时候用?

当一个对象的构造参数特别多,或者构造过程分多个步骤时,建造者模式就派上用场了。

我记得有个项目要构造一个复杂的网络请求对象:URL、headers、body、超时时间、重试策略……如果用构造函数传参,那签名得写十几行。用建造者模式就清爽多了。

4.2 C++ 实现示例

class HttpRequest {
public:
    class Builder {
    public:
        Builder& setUrl(const std::string& url) {
            url_ = url;
            return *this;
        }
        Builder& setTimeout(int ms) {
            timeout_ = ms;
            return *this;
        }
        Builder& addHeader(const std::string& key, const std::string& value) {
            headers_[key] = value;
            return *this;
        }
        HttpRequest build() {
            return HttpRequest(url_, timeout_, headers_);
        }
    private:
        std::string url_;
        int timeout_ = 3000;
        std::map<std::string, std::string> headers_;
    };

    // 构造函数私有,只能通过 Builder 构造
private:
    HttpRequest(const std::string& url, int timeout,
                const std::map<std::string, std::string>& headers)
        : url_(url), timeout_(timeout), headers_(headers) {}
    // ...
};

用法:

auto req = HttpRequest::Builder()
                .setUrl("https://api.example.com")
                .setTimeout(5000)
                .addHeader("Authorization", "Bearer xxx")
                .build();

小技巧:Builder 的方法返回引用,实现链式调用。这是 C++ 里很常见的写法,看着舒服,用着也顺手。

五、原型模式(Prototype)

5.1 核心思想

通过拷贝已有对象来创建新对象,而不是通过 new。适合对象创建成本高、或者需要保留对象状态的情况。

我曾在游戏开发中用过原型模式:每个怪物类型只有一个原型对象,要生成怪物时就 clone 一份。这样不用每次都从配置文件读取数据,性能好很多。

5.2 C++ 实现

class Monster {
public:
    virtual std::unique_ptr<Monster> clone() = 0;
    virtual void attack() = 0;
    virtual ~Monster() = default;
};

class Dragon : public Monster {
public:
    std::unique_ptr<Monster> clone() override {
        return std::make_unique<Dragon>(*this);
    }
    void attack() override {
        std::cout << "Dragon breathes fire!\n";
    }
};

注意:clone 函数里用的是拷贝构造函数。如果你的类里有指针成员,记得实现深拷贝,不然两个对象共享同一块内存,析构时 double free 就麻烦了。

六、知识体系总览

下面这张图把四种创建型模式的核心逻辑串起来了,方便你对照理解:

创建型设计模式知识体系 创建型模式 单例模式 全局唯一实例 日志、配置、线程池 工厂模式 简单/方法/抽象工厂 解耦创建与使用 建造者模式 分步构建复杂对象 链式调用 原型模式 通过克隆创建对象 深拷贝注意 核心原则 将对象的创建与使用分离 根据场景选择合适模式,不要过度设计 注意线程安全、生命周期、拷贝语义

七、C++ 实现要点总结

模式 核心要点 常见坑
单例 构造函数私有、static 局部变量、delete 拷贝 线程安全(C++11 后局部 static 已安全)、析构顺序
工厂 虚函数、智能指针返回、开闭原则 过度抽象、工厂类爆炸
建造者 链式调用、Builder 内部类、build() 返回最终对象 Builder 状态管理、参数校验时机
原型 clone() 虚函数、拷贝构造函数 深拷贝 vs 浅拷贝、指针成员处理

最后说一句:设计模式是工具,不是目的。我见过有人为了用模式而用模式,把简单问题搞复杂了。我的建议是:先写出能跑的代码,再考虑要不要用模式重构。别一开始就套模式,那样反而束手束脚。

公众号:蓝海资料掘金营,微信deep3321