96、STL与原型模式:对象克隆、浅拷贝、深拷贝

原型模式,说白了就是「复制对象」这件事。你想想看,在C++里我们天天跟拷贝构造、赋值操作打交道,但真正遇到「我要克隆一个对象,而且得按我的意愿控制复制深度」这种需求时,很多人就开始踩坑了。

我个人习惯把原型模式理解为「对象复印机」。你给复印机一个原件,它给你吐出一份副本。但问题来了——这份副本是只复印了表面(浅拷贝),还是连内部的所有细节都复印了(深拷贝)?

嗯,这里就是STL和原型模式结合最微妙的地方。

浅拷贝 vs 深拷贝:一张图说清楚

先看一个最典型的场景。假设我们有一个类,内部持有动态分配的内存:

class Image {
public:
    Image(int w, int h) : width(w), height(h) {
        data = new int[w * h]();  // 动态分配
    }
    // 默认拷贝构造——浅拷贝!
    ~Image() { delete[] data; }
private:
    int width, height;
    int* data;
};

Image img1(1920, 1080);
Image img2 = img1;  // 浅拷贝:两个对象指向同一块内存!

为什么会这样?因为编译器生成的默认拷贝构造函数,只是逐字节复制成员变量。data指针被复制了,但指针指向的那块内存并没有被复制。img1和img2的data指向同一块地址——谁先析构,另一个就悬空了。

我在项目中遇到过这种问题,当时是一个图像处理管线,多个线程同时操作「克隆」出来的Image对象。结果崩溃得莫名其妙,查了两天才定位到是浅拷贝导致的野指针访问。

⚠️ 避坑指南:我曾经在代码评审时见过一个同事,写了三年C++还在用memcpy复制带指针的对象。他说「反正都是二进制数据,复制过去不就行了?」——不行!绝对不行!memcpy只适合POD类型,对于管理资源的类,必须用拷贝构造或克隆接口。

STL容器与原型模式

STL容器本身已经实现了深拷贝语义。当你拷贝一个vector时,它会复制所有元素。但如果vector里存的是指针呢?

std::vector<Image*> vec1;
vec1.push_back(new Image(100, 100));

std::vector<Image*> vec2 = vec1;  // 复制了指针,没复制对象!

你看,STL容器只负责复制它存储的元素。如果元素是指针,它就复制指针值——这仍然是浅拷贝。要真正实现深拷贝,你需要自己写克隆逻辑。

我个人习惯的做法是:在类内部提供一个clone()虚函数,让每个子类都能正确复制自己。这就是原型模式的核心思想。

用STL实现原型模式

来看一个完整的例子。我们设计一个图形基类,支持克隆:

#include <iostream>
#include <vector>
#include <memory>
#include <string>

class Shape {
public:
    virtual ~Shape() = default;
    virtual std::unique_ptr<Shape> clone() const = 0;
    virtual void draw() const = 0;
};

class Circle : public Shape {
public:
    Circle(double r, const std::string& color) 
        : radius(r), color(color) {}
    
    std::unique_ptr<Shape> clone() const override {
        // 调用拷贝构造,返回深拷贝后的对象
        return std::make_unique<Circle>(*this);
    }
    
    void draw() const override {
        std::cout << "Circle: radius=" << radius 
                  << ", color=" << color << std::endl;
    }
private:
    double radius;
    std::string color;
};

class Rectangle : public Shape {
public:
    Rectangle(double w, double h, const std::string& color)
        : width(w), height(h), color(color) {}
    
    std::unique_ptr<Shape> clone() const override {
        return std::make_unique<Rectangle>(*this);
    }
    
    void draw() const override {
        std::cout << "Rectangle: " << width << "x" << height 
                  << ", color=" << color << std::endl;
    }
private:
    double width, height;
    std::string color;
};
💡 关键点:使用std::unique_ptr作为clone()的返回类型,明确表达「调用者获得对象所有权」的语义。这比裸指针安全得多。

现在我们可以用STL容器来管理这些可克隆的对象了:

int main() {
    std::vector<std::unique_ptr<Shape>> shapes;
    
    // 添加原型
    shapes.push_back(std::make_unique<Circle>(5.0, "red"));
    shapes.push_back(std::make_unique<Rectangle>(3.0, 4.0, "blue"));
    
    // 克隆所有对象
    std::vector<std::unique_ptr<Shape>> clones;
    for (const auto& s : shapes) {
        clones.push_back(s->clone());  // 每个对象自己负责深拷贝
    }
    
    // 修改原型,不影响克隆体
    shapes.clear();  // 原型销毁
    for (const auto& c : clones) {
        c->draw();   // 克隆体依然有效
    }
    return 0;
}

你想想看,如果没有clone()接口,你要怎么复制一个多态对象?用dynamic_cast一个个判断类型?那代码得写成什么样。原型模式把「如何复制自己」的责任交给了对象本身,调用者只需要说「给我一份你的副本」就行了。

深拷贝的陷阱:当成员变量也是指针时

刚才的例子中,Circle和Rectangle的成员都是值类型(double、string),默认拷贝构造就能正确深拷贝。但如果成员变量本身也是指针呢?

class Layer {
public:
    Layer(Image* img) : image(img) {}
    
    std::unique_ptr<Layer> clone() const {
        // 错误!只复制了指针
        return std::make_unique<Layer>(*this);
    }
private:
    Image* image;  // 外部传入的指针,不拥有所有权
};

这里有个微妙的区别:如果Layer不拥有Image的所有权(只是观察者),那么浅拷贝指针反而是正确的。如果Layer拥有所有权,那应该用unique_ptr管理,clone时复制Image对象。

📌 我的经验:判断一个类应该浅拷贝还是深拷贝,核心原则是「所有权」。谁拥有资源,谁负责深拷贝。观察者只拷贝指针,不拷贝资源。这个原则帮我避免了很多内存管理问题。

原型模式 + STL容器 = 灵活的对象管理

把原型模式和STL容器结合起来,能做出非常灵活的设计。比如一个对象池:

class PrototypeManager {
public:
    void registerPrototype(const std::string& key, 
                           std::unique_ptr<Shape> proto) {
        prototypes[key] = std::move(proto);
    }
    
    std::unique_ptr<Shape> create(const std::string& key) const {
        auto it = prototypes.find(key);
        if (it != prototypes.end()) {
            return it->second->clone();
        }
        return nullptr;
    }
private:
    std::unordered_map<std::string, std::unique_ptr<Shape>> prototypes;
};

// 使用
PrototypeManager mgr;
mgr.registerPrototype("big_red_circle", 
                      std::make_unique<Circle>(10.0, "red"));
mgr.registerPrototype("small_blue_rect",
                      std::make_unique<Rectangle>(2.0, 3.0, "blue"));

auto c1 = mgr.create("big_red_circle");  // 克隆
auto c2 = mgr.create("big_red_circle");  // 再克隆一个

你看,原型模式配合STL的unordered_map,实现了一个轻量级的「对象工厂」。不需要复杂的工厂类继承体系,只需要注册原型、调用clone()即可。

总结一下

原型模式在C++里,说白了就是「拷贝构造的面向对象版本」。它解决了两个核心问题:

  • 多态克隆:通过虚函数clone(),让基类指针也能正确复制派生类对象
  • 控制复制深度:每个类自己决定哪些成员浅拷贝、哪些深拷贝

STL容器和智能指针让这个模式更安全、更易用。我个人建议:只要你的类需要被复制,而且有继承关系,就考虑实现clone()接口。这比让调用者去猜「这个类能不能拷贝、怎么拷贝」要靠谱得多。

嗯,最后提醒一句:别忘了遵循三五法则。如果你手动实现了clone(),大概率也需要实现拷贝构造、拷贝赋值和析构函数。这三者要一起维护,缺一个就可能出问题。

原型模式与对象克隆核心逻辑 原型对象 (Prototype) 持有原始数据 clone() 克隆体 (Clone) 独立副本 浅拷贝 共享内部资源 指针指向同一内存 深拷贝 复制所有资源 各自独立内存 STL容器 + 智能指针管理 vector<unique_ptr<Shape>> / unordered_map<string, unique_ptr<Shape>> 图例 clone() 调用 浅拷贝路径 深拷贝路径

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