代码改变世界

实用指南:【C++实战㊷】C++ 原型模式实战:从概念到高效应用

2025-09-26 19:21  tlnshuju  阅读(18)  评论(0)    收藏  举报


一、原型模式的概念

1.1 原型模式的定义

原型模式是一种创建型设计模式,其核心在于通过复制现有对象来创建新对象,而非传统的使用new关键字调用构造函数进行实例化。在传统的对象实例化方式中,每次创建对象都需要执行构造函数,完成对象的初始化过程,包括分配内存、初始化成员变量等操作 。而原型模式就像是使用复印机复印文件,我们有一份已有的文件(现有对象),当需要新的文件时,直接通过复印(复制现有对象)得到,大大节省了重新创建的成本和时间。这种方式在某些场景下能显著提高对象创建的效率,减少重复的初始化工作。

1.2 原型模式的适用场景

  • 对象创建成本高:当创建一个对象需要消耗大量的资源,比如进行复杂的数据库查询、网络通信,或者涉及大量的计算来初始化对象的状态时,使用原型模式就可以避免每次都进行这些高成本的操作。例如,一个对象需要从数据库中加载大量的配置信息,初始化过程可能涉及多次数据库连接和复杂的 SQL 查询,如果每次创建都重复这些操作,性能会受到很大影响。使用原型模式,我们可以先创建一个已经加载好配置的原型对象,后续需要新对象时直接复制该原型,从而提升性能。
  • 需要批量创建相似对象:在一些场景中,我们需要创建大量相似的对象,它们只有部分属性不同。比如在游戏开发中,需要创建大量相同类型的敌人,这些敌人具有相同的基础属性,如攻击力、防御力、生命值等,但可能有不同的位置、名称等。如果每次都通过构造函数逐个创建,代码会显得繁琐且重复。利用原型模式,我们只需创建一个敌人原型对象,然后通过复制该原型,再修改少量不同的属性,就可以快速创建出大量相似的敌人对象。

1.3 原型模式的核心

  • 克隆方法:这是原型模式的关键方法,用于实现对象的复制。在 C++ 中,通常通过重载operator=或者定义一个专门的克隆方法(如clone函数)来实现。克隆方法负责将原型对象的状态复制到新创建的对象中,使得新对象在创建时就具有与原型对象相同的初始状态。
  • 浅克隆与深克隆
    • 浅克隆:浅克隆在复制对象时,对于对象中的成员变量,如果是值类型(如int、double、char等),会直接复制其值;但如果是指针或引用类型,只是复制指针或引用本身,而不会复制指针或引用所指向的对象。这就意味着克隆后的对象和原对象在这些指针或引用所指向的内容上是共享的。例如,假设有一个包含指针成员变量的类A,通过浅克隆创建了新对象B,A和B的指针成员变量指向同一块内存区域,当A修改了这块内存区域的内容时,B也会受到影响。
    • 深克隆:深克隆则会递归地复制对象及其所有引用的对象。对于上述包含指针成员变量的类A,在进行深克隆时,不仅会复制指针成员变量本身,还会为指针所指向的对象在堆上重新分配内存,并将原对象中指针所指向对象的内容复制到新分配的内存中。这样,克隆后的对象B和原对象A在所有成员变量上都是完全独立的,修改A的内容不会影响到B,反之亦然。

二、原型模式的实现方式

2.1 浅克隆的实现

当类的成员变量均为值类型时,在 C++ 中实现浅克隆相对简单,通常可以通过拷贝构造函数来完成。下面通过一个简单的示例来展示浅克隆的实现过程。

假设我们有一个表示坐标点的类Point,它包含两个int类型的成员变量x和y,代码如下:

class Point
{
public:
int x;
int y;
// 构造函数
Point(int _x, int _y) : x(_x), y(_y) {
}
// 拷贝构造函数,实现浅克隆
Point(const Point& other) : x(other.x), y(other.y) {
}
};

在上述代码中,Point类的拷贝构造函数接收一个const Point&类型的参数other,它将other对象的x和y成员变量的值直接复制到新创建的对象中,这就是浅克隆的过程。由于x和y是值类型,这种直接复制就能够满足需求。

我们可以通过以下方式来测试这个浅克隆功能:

int main() {
Point p1(10, 20);
// 使用拷贝构造函数进行浅克隆
Point p2(p1);
std::cout <<
"p1: (" << p1.x <<
", " << p1.y <<
")" << std::endl;
std::cout <<
"p2: (" << p2.x <<
", " << p2.y <<
")" << std::endl;
return 0;
}

运行上述代码,会输出p1和p2的坐标值,它们是相同的,这表明浅克隆成功地复制了p1的状态到p2。 这种方式在成员变量为值类型时非常高效,因为不需要额外的内存分配和复杂的复制操作。

2.2 深克隆的实现

当类的成员变量包含指针或引用类型时,浅克隆就无法满足需求了,因为浅克隆只是复制指针或引用本身,而不会复制它们所指向的对象,这可能会导致多个对象共享同一块内存,引发内存管理问题。此时就需要使用深克隆,深克隆会为指针成员重新分配内存,并将原对象中指针所指向的内容复制到新分配的内存中。

假设我们有一个MyString类,它包含一个char*类型的指针data用于存储字符串,并且我们希望实现深克隆,代码如下:

class MyString
{
public:
char* data;
int length;
// 构造函数
MyString(const char* str) {
length = strlen(str);
data = new char[length + 1];
strcpy(data, str);
}
// 拷贝构造函数,实现深克隆
MyString(const MyString& other) {
length = other.length;
data = new char[length + 1];
strcpy(data, other.data);
}
// 析构函数,释放内存
~MyString() {
delete[] data;
}
};

在这个MyString类中,构造函数负责为data指针分配内存并复制字符串内容。拷贝构造函数则是实现深克隆的关键,它首先为新对象的data指针分配与原对象相同大小的内存空间,然后使用strcpy函数将原对象data所指向的字符串内容复制到新分配的内存中,这样就确保了克隆后的对象和原对象在data所指向的字符串内容上是完全独立的,不会相互影响。

我们可以通过以下方式测试这个深克隆功能:

int main() {
MyString s1("Hello, World!");
// 使用拷贝构造函数进行深克隆
MyString s2(s1);
// 修改s2的内容,验证深克隆的独立性
s2.data[0] = 'h';
std::cout <<
"s1: " << s1.data << std::endl;
std::cout <<
"s2: " << s2.data << std::endl;
return 0;
}

运行上述代码,会发现修改s2的字符串内容后,s1的内容并不会受到影响,这就验证了深克隆的正确性。如果这里使用浅克隆,s1和s2的data指针将指向同一块内存,修改s2的内容就会导致s1的内容也被改变。

2.3 原型管理器的设计与使用

原型管理器是一个用于管理多个原型对象的类,它提供了一种集中式的方式来存储和获取原型对象,方便在需要时进行克隆操作。原型管理器通常使用一个容器(如std::map)来存储原型对象,其中键是原型对象的标识符,值是原型对象的指针。

下面是一个原型管理器的设计示例,假设我们有一个抽象原型类Prototype,以及两个具体原型类ConcretePrototype1和ConcretePrototype2,代码如下:

#include <iostream>
  #include <unordered_map>
    #include <string>
      // 抽象原型类
      class Prototype
      {
      public:
      virtual Prototype* clone() const = 0;
      virtual ~Prototype() {
      }
      };
      // 具体原型类1
      class ConcretePrototype1
      : public Prototype {
      public:
      ConcretePrototype1(const std::string& value) : m_value(value) {
      }
      Prototype* clone() const override {
      return new ConcretePrototype1(*this);
      }
      void print() const {
      std::cout <<
      "ConcretePrototype1: " << m_value << std::endl;
      }
      private:
      std::string m_value;
      };
      // 具体原型类2
      class ConcretePrototype2
      : public Prototype {
      public:
      ConcretePrototype2(int value) : m_value(value) {
      }
      Prototype* clone() const override {
      return new ConcretePrototype2(*this);
      }
      void print() const {
      std::cout <<
      "ConcretePrototype2: " << m_value << std::endl;
      }
      private:
      int m_value;
      };
      // 原型管理器
      class PrototypeManager
      {
      public:
      // 添加原型对象到管理器
      void addPrototype(const std::string& key, Prototype* prototype) {
      m_prototypes[key] = prototype;
      }
      // 根据键获取克隆的原型对象
      Prototype* getPrototype(const std::string& key) const {
      auto it = m_prototypes.find(key);
      if (it != m_prototypes.end()) {
      return it->second->
      clone();
      }
      return nullptr;
      }
      // 释放所有原型对象的内存
      ~PrototypeManager() {
      for (auto& pair : m_prototypes) {
      delete pair.second;
      }
      }
      private:
      std::unordered_map<std::string, Prototype*> m_prototypes;
        };

在上述代码中,PrototypeManager类包含一个addPrototype方法,用于将原型对象添加到管理器中,它接收一个键和一个原型对象指针作为参数,将原型对象存储到m_prototypes这个std::unordered_map中。getPrototype方法则用于根据键获取克隆的原型对象,它首先在m_prototypes中查找对应的原型对象,如果找到,则调用该原型对象的clone方法获取一个克隆对象并返回;如果未找到,则返回nullptr。~PrototypeManager析构函数负责在管理器销毁时释放所有存储的原型对象的内存,以避免内存泄漏。

我们可以通过以下方式使用这个原型管理器:

int main() {
PrototypeManager manager;
// 添加原型对象
manager.addPrototype("p1", new ConcretePrototype1("Prototype 1"));
manager.addPrototype("p2", new ConcretePrototype2(123));
// 获取并使用克隆的原型对象
Prototype* clonedP1 = manager.getPrototype("p1");
if (clonedP1) {
clonedP1->
print();
delete clonedP1;
}
Prototype* clonedP2 = manager.getPrototype("p2");
if (clonedP2) {
clonedP2->
print();
delete clonedP2;
}
return 0;
}

运行上述代码,会分别输出ConcretePrototype1和ConcretePrototype2克隆对象的信息,展示了原型管理器如何有效地管理和提供原型对象的克隆。

三、原型模式的实战技巧

3.1 深克隆的递归实现与循环引用处理

在实现深克隆时,递归是一种常用的方法。以一个包含嵌套结构的类为例,假设我们有一个表示文件夹的类Folder,它可以包含文件(用File类表示)和子文件夹(也是Folder类),结构如下:

class File
{
public:
std::string name;
File(const std::string& _name) : name(_name) {
}
};
class Folder
{
public:
std::string name;
std::vector<File*> files;
  std::vector<Folder*> subFolders;
    Folder(const std::string& _name) : name(_name) {
    }
    ~Folder() {
    for (File* file : files) {
    delete file;
    }
    for (Folder* subFolder : subFolders) {
    delete subFolder;
    }
    }
    // 深克隆方法,使用递归实现
    Folder* deepClone() const {
    Folder* newFolder = new Folder(name);
    for (File* file : files) {
    newFolder->files.push_back(new File(file->name));
    }
    for (Folder* subFolder : subFolders) {
    newFolder->subFolders.push_back(subFolder->
    deepClone());
    }
    return newFolder;
    }
    };

在上述Folder类的deepClone方法中,首先创建一个新的Folder对象,然后递归地复制files和subFolders。对于files,为每个文件创建一个新的File对象并添加到新文件夹的files列表中;对于subFolders,递归调用deepClone方法复制每个子文件夹并添加到新文件夹的subFolders列表中。

然而,当存在循环引用时,递归深克隆会导致无限递归,从而使程序崩溃。例如,如果两个Folder对象相互包含对方作为子文件夹,就会出现循环引用。为了解决这个问题,可以使用一个std::unordered_map来标记已经克隆过的对象。修改后的deepClone方法如下:

Folder* deepClone(const std::unordered_map<Folder*, Folder*>
  & clonedMap = {
  }) const {
  // 检查是否已经克隆过该对象
  auto it = clonedMap.find(this);
  if (it != clonedMap.end()) {
  return it->second;
  }
  Folder* newFolder = new Folder(name);
  std::unordered_map<Folder*, Folder*>
    newClonedMap(clonedMap);
    newClonedMap[this] = newFolder;
    for (File* file : files) {
    newFolder->files.push_back(new File(file->name));
    }
    for (Folder* subFolder : subFolders) {
    newFolder->subFolders.push_back(subFolder->
    deepClone(newClonedMap));
    }
    return newFolder;
    }

在这个改进版本中,deepClone方法接收一个std::unordered_map<Folder*, Folder*>类型的参数clonedMap,用于存储已经克隆过的Folder对象及其对应的克隆对象。在克隆之前,先检查当前对象是否已经在clonedMap中,如果存在,则直接返回对应的克隆对象,避免重复克隆和无限递归;如果不存在,则进行正常的克隆操作,并将当前对象和克隆对象添加到新的clonedMap中,以便在递归克隆子文件夹时使用。

3.2 原型模式与工厂模式的结合使用

工厂模式主要负责对象的创建,它根据不同的条件创建不同类型的对象;而原型模式则侧重于通过复制现有对象来创建新对象。将两者结合,可以充分发挥它们的优势。例如,在一个图形绘制系统中,有多种类型的图形,如圆形、矩形、三角形等,每种图形都有自己的属性和绘制方法。

首先定义一个抽象图形类Shape,以及具体的图形类Circle、Rectangle和Triangle,并为它们实现克隆方法:

// 抽象图形类
class Shape
{
public:
virtual Shape* clone() const = 0;
virtual void draw() const = 0;
virtual ~Shape() {
}
};
// 圆形类
class Circle
: public Shape {
public:
int radius;
Circle(int _radius) : radius(_radius) {
}
Shape* clone() const override {
return new Circle(radius);
}
void draw() const override {
std::cout <<
"Drawing a circle with radius " << radius << std::endl;
}
};
// 矩形类
class Rectangle
: public Shape {
public:
int width;
int height;
Rectangle(int _width, int _height) : width(_width), height(_height) {
}
Shape* clone() const override {
return new Rectangle(width, height);
}
void draw() const override {
std::cout <<
"Drawing a rectangle with width " << width <<
" and height " << height << std::endl;
}
};
// 三角形类
class Triangle
: public Shape {
public:
int side1;
int side2;
int side3;
Triangle(int _side1, int _side2, int _side3) : side1(_side1), side2(_side2), side3(_side3) {
}
Shape* clone() const override {
return new Triangle(side1, side2, side3);
}
void draw() const override {
std::cout <<
"Drawing a triangle with sides " << side1 <<
", " << side2 <<
", " << side3 << std::endl;
}
};

然后定义一个工厂类ShapeFactory,它负责创建原型对象,并提供一个方法根据类型获取克隆对象:

class ShapeFactory
{
public:
ShapeFactory() {
circlePrototype = new Circle(0);
rectanglePrototype = new Rectangle(0, 0);
trianglePrototype = new Triangle(0, 0, 0);
}
~ShapeFactory() {
delete circlePrototype;
delete rectanglePrototype;
delete trianglePrototype;
}
Shape* createShape(const std::string& type) const {
if (type == "circle") {
return circlePrototype->
clone();
} else if (type == "rectangle") {
return rectanglePrototype->
clone();
} else if (type == "triangle") {
return trianglePrototype->
clone();
}
return nullptr;
}
private:
Circle* circlePrototype;
Rectangle* rectanglePrototype;
Triangle* trianglePrototype;
};

在ShapeFactory类中,构造函数初始化了三种图形的原型对象,createShape方法根据传入的类型参数,返回对应原型对象的克隆。这样,客户端代码只需要与ShapeFactory交互,通过它获取所需的图形克隆,而不需要关心具体图形对象的创建细节。例如:

int main() {
ShapeFactory factory;
Shape* circle = factory.createShape("circle");
if (circle) {
circle->
draw();
delete circle;
}
Shape* rectangle = factory.createShape("rectangle");
if (rectangle) {
rectangle->
draw();
delete rectangle;
}
return 0;
}

通过这种结合方式,既利用了工厂模式的创建逻辑管理,又借助了原型模式的高效复制特性,提高了代码的可维护性和灵活性。

3.3 原型模式的性能优化

在使用原型模式时,减少重复初始化是优化性能的关键。例如,在一个游戏开发场景中,有大量的游戏道具对象,这些道具对象在创建时需要进行复杂的初始化操作,如加载纹理、设置属性等。

假设我们有一个Item类表示游戏道具:

class Item
{
public:
std::string name;
int value;
// 这里假设还有一些复杂的资源,如纹理等,初始化时加载
// 为了简单起见,这里省略具体的资源加载代码
Item(const std::string& _name, int _value) : name(_name), value(_value) {
// 模拟复杂的初始化操作,如加载纹理等
std::cout <<
"Initializing item " << name << std::endl;
}
Item(const Item& other) : name(other.name), value(other.value) {
// 克隆时不需要重复初始化复杂资源,直接复制状态
std::cout <<
"Cloning item " << name << std::endl;
}
Item&
operator=(const Item& other) {
if (this != &other) {
name = other.name;
value = other.value;
}
return *this;
}
};

在这个Item类中,构造函数进行复杂的初始化操作,而拷贝构造函数只是复制对象的状态,避免了重复的初始化。为了进一步优化性能,可以在克隆前缓存已初始化的数据。例如,创建一个ItemCache类来管理缓存:

class ItemCache
{
public:
static ItemCache&
getInstance() {
static ItemCache instance;
return instance;
}
Item* getCachedItem(const std::string& name, int value) {
auto it = cache.find(name);
if (it != cache.end()) {
return new Item(*it->second);
}
Item* newItem = new Item(name, value);
cache[name] = newItem;
return newItem;
}
~ItemCache() {
for (auto& pair : cache) {
delete pair.second;
}
}
private:
std::unordered_map<std::string, Item*> cache;
  ItemCache() {
  }
  ItemCache(const ItemCache&
  ) = delete;
  ItemCache&
  operator=(const ItemCache&
  ) = delete;
  };

ItemCache类使用单例模式确保全局只有一个实例,getCachedItem方法首先检查缓存中是否已经存在指定名称的道具对象,如果存在,则直接返回克隆对象;如果不存在,则创建新的道具对象,将其添加到缓存中,然后返回。这样,对于相同名称的道具对象,后续创建时就可以直接从缓存中获取克隆,避免了重复的复杂初始化操作,大大提高了性能。客户端代码可以这样使用:

int main() {
Item* item1 = ItemCache::getInstance().getCachedItem("Sword", 10);
Item* item2 = ItemCache::getInstance().getCachedItem("Sword", 10);
// 使用完后释放内存
delete item1;
delete item2;
return 0;
}

通过这种缓存机制,在频繁创建相似对象时,有效减少了重复初始化带来的性能开销,提升了系统的整体性能。

四、实战项目:报表模板克隆系统

4.1 项目需求

在许多企业的业务流程中,报表是数据展示和分析的重要工具。不同的业务场景可能需要大量相似结构的报表,只是在一些细节属性上有所差异。例如,一家电商企业可能需要每月生成销售报表,这些报表具有相同的基本结构,如包含订单信息、商品信息、客户信息等,但每个月的报表数据和一些特定的统计时间段、报表名称等属性不同。

基于这种情况,本项目的需求是构建一个报表模板克隆系统。该系统需要具备以下功能:

  • 基于现有报表模板克隆新模板:能够选择一个已有的报表模板作为原型,快速创建新的报表模板。这些新模板在结构和大部分属性上与原型模板相同,无需重新设计报表的整体架构。
  • 修改部分属性:对于克隆出的新报表模板,用户可以方便地修改一些特定属性,如报表的标题、统计时间段、数据筛选条件等,以满足不同业务场景的需求。例如,在销售报表中,用户可以修改统计时间段为特定的月份或季度,或者根据不同的商品类别筛选数据。

通过这样的系统,可以大大提高报表制作的效率,减少重复劳动,让企业的业务人员能够更快速地获取满足自身需求的报表,支持决策分析。

4.2 原型模式实现报表模板浅克隆与深克隆

假设我们有一个ReportTemplate类来表示报表模板,它包含一些基本属性,如报表名称reportName、报表标题reportTitle,以及一个包含报表数据的ReportData类对象的指针data。ReportData类包含一个std::vector<int>类型的成员变量values用于存储报表数据。

#include <iostream>
  #include <vector>
    #include <string>
      // 报表数据类
      class ReportData
      {
      public:
      std::vector<
      int> values;
      ReportData(const std::vector<
      int>
      & _values) : values(_values) {
      }
      // 简单的深克隆方法,用于复制数据
      ReportData* deepClone() const {
      return new ReportData(values);
      }
      };
      // 报表模板类
      class ReportTemplate
      {
      public:
      std::string reportName;
      std::string reportTitle;
      ReportData* data;
      ReportTemplate(const std::string& _name, const std::string& _title, ReportData* _data)
      : reportName(_name), reportTitle(_title), data(_data) {
      }
      // 浅克隆方法
      ReportTemplate* shallowClone() const {
      return new ReportTemplate(reportName, reportTitle, data);
      }
      // 深克隆方法
      ReportTemplate* deepClone() const {
      ReportData* newData = data->
      deepClone();
      return new ReportTemplate(reportName, reportTitle, newData);
      }
      ~ReportTemplate() {
      delete data;
      }
      };

在上述代码中:

  • ReportData类的deepClone方法用于复制其内部的数据,为ReportTemplate类的深克隆提供支持。
  • ReportTemplate类的shallowClone方法实现浅克隆,它直接复制reportName、reportTitle和data指针,这意味着克隆后的模板和原模板共享data所指向的内存。
  • ReportTemplate类的deepClone方法实现深克隆,它首先调用ReportData类的deepClone方法复制数据,然后创建一个新的ReportTemplate对象,将复制的数据和其他属性赋值给新对象,确保克隆后的模板和原模板在数据上是完全独立的。

4.3 批量克隆报表模板的效率测试

为了测试批量克隆报表模板的效率,我们可以使用 C++ 的<chrono>库来记录克隆操作前后的时间戳,计算时间差来衡量克隆的效率。

#include <chrono>
  int main() {
  // 创建原型报表模板
  std::vector<
  int> sampleData = {
  1, 2, 3, 4, 5
  };
  ReportData* sampleReportData = new ReportData(sampleData);
  ReportTemplate* prototype = new ReportTemplate("SampleReport", "Sample Title", sampleReportData);
  // 测试浅克隆效率
  auto startShallow = std::chrono::high_resolution_clock::now();
  const int cloneCount = 10000;
  for (int i = 0; i < cloneCount;
  ++i) {
  ReportTemplate* cloned = prototype->
  shallowClone();
  // 这里可以对克隆对象进行一些操作,如修改属性等
  delete cloned;
  }
  auto endShallow = std::chrono::high_resolution_clock::now();
  auto durationShallow = std::chrono::duration_cast<std::chrono::milliseconds>
    (endShallow - startShallow).count();
    // 测试深克隆效率
    auto startDeep = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < cloneCount;
    ++i) {
    ReportTemplate* cloned = prototype->
    deepClone();
    // 这里可以对克隆对象进行一些操作,如修改属性等
    delete cloned;
    }
    auto endDeep = std::chrono::high_resolution_clock::now();
    auto durationDeep = std::chrono::duration_cast<std::chrono::milliseconds>
      (endDeep - startDeep).count();
      std::cout <<
      "Time taken for shallow cloning " << cloneCount <<
      " templates: " << durationShallow <<
      " ms" << std::endl;
      std::cout <<
      "Time taken for deep cloning " << cloneCount <<
      " templates: " << durationDeep <<
      " ms" << std::endl;
      delete prototype;
      return 0;
      }

在上述测试代码中:

  • 首先创建了一个原型报表模板prototype。
  • 然后分别进行浅克隆和深克隆的效率测试,每次测试克隆cloneCount个报表模板,并记录克隆过程所花费的时间。
  • 最后输出浅克隆和深克隆cloneCount个报表模板所花费的时间。

通过这样的测试,可以直观地看到浅克隆和深克隆在批量克隆场景下的效率差异。一般来说,浅克隆由于不需要复制指针所指向的对象,速度会比深克隆快很多,但需要注意共享数据带来的潜在问题;深克隆虽然速度较慢,但能保证克隆对象之间数据的独立性,在需要避免数据共享影响的场景下是必要的。