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对象。结果崩溃得莫名其妙,查了两天才定位到是浅拷贝导致的野指针访问。
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;
};
现在我们可以用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(),大概率也需要实现拷贝构造、拷贝赋值和析构函数。这三者要一起维护,缺一个就可能出问题。
公众号:蓝海资料掘金营,微信deep3321