C++设计模式创建型模式———生成器模式

文章目录

  • 一、引言
  • 二、生成器/建造者模式
  • 三、总结

一、引言

上一篇文章我们介绍了工厂模式,工厂模式的主要特点是生成对象。当对象较简单时,可以使用简单工厂模式或工厂模式;而当对象相对复杂时,则可以选择使用抽象工厂模式。

工厂用于生产各种对象,这些对象通常是兄弟类,继承自同一个基类。兄弟子类通过实现基类接口,展现不同的行为,并由工厂函数创建。然而,工厂模式在创建对象时并不关注构造细节,处理复杂对象生成时可能显得力不从心。抽象工厂模式专注于生成一系列相关对象,但在对象构造复杂时,其能力也有限。

相较于工厂模式,生成器模式同样用于对象的生成,但更侧重于构造细节,增加了额外的构建流程,以便处理复杂对象的构建需求。

生成器模式也是一种创建型设计模式, 使我们能够分步骤创建复杂对象。 该模式允许使用相同的创建代码生成不同类型和形式的对象。 也就是说,生成器模式就是为了生成一个复杂的对象。

化繁为简,逐个击破。分步骤创建复杂的对象,并且允许使用相同的代码生成不同类型和形式的对象。

在这里插入图片描述

假设有这样一个复杂对象, 在对其进行构造时需要对诸多成员变量和嵌套对象进行繁复的初始化工作。 这些初始化代码通常深藏于一个包含众多参数且让人基本看不懂的构造函数中; 甚至还有更糟糕的情况, 那就是这些代码散落在客户端代码的多个位置。

假设我们要建一个房子,房子由许多部分组成,如门和墙壁。生成器模式可以将对象构造的代码从产品类中抽取出来,并放在一个名为生成器的独立对象中。

该模式会将对象构造过程划分为一组步骤, 比如 build­Walls创建墙壁和 build­Door创建房门创建房门等。 每次创建对象时, 都需要通过生成器对象执行一系列步骤。 重点在于我们无需调用所有步骤, 而只需调用创建特定对象配置所需的那些步骤即可。

当你需要创建不同形式的产品时, 其中的一些构造步骤可能需要不同的实现。 例如, 木屋的房门可能需要使用木头制造, 而城堡的房门则必须使用石头制造。

在这种情况下, 可以创建多个不同的生成器, 用不同方式实现一组相同的创建步骤。 然后就可以在创建过程中使用这些生成器 (例如按顺序调用多个构造步骤) 来生成不同类型的对象。

我们通过写出不同生成器以不同方式执行相同的任务。

假设我们需要一个木门+石墙的房子。也需要一个石头门+钢铁的房子。在调用同一组步骤后, 第一个建造者会给你一栋普通房屋, 第二个会给你一座小城堡。但是, 只有在调用构造步骤的客户端代码可以通过通用接口与建造者进行交互时, 这样的调用才能返回需要的房屋。

然后我们可以进一步的把用于创建一系列生成器的步骤抽取出来,成为一个单独的主管类。主管类去定义创建步骤的执行顺序。而生成器去提供这些步骤的实现。

但是即使没有主管类,我们的用户也可以直接以特定的顺序去调用创建步骤。但是如果有主管类,主管类可以完全隐藏产品的构造细节。客户端只需要将一个生成器与主管类关联,然后使用主管类去构造产品,就能从生成器处获得构造结果了。


二、生成器/建造者模式

这里还是以游戏中的怪物类来讲解。怪物同样分为亡灵类怪物、元素类怪物、机械类怪物。

在创建怪物对象的过程中,有一个创建步骤非常烦琐,把怪物模型创建出来用于显示给玩家。策划规定,任何一种怪物都由头部、躯干(包括颈部、尾巴等)、肢体3个部位组成,在制作怪物模型时,头部、躯干、肢体模型分开制作。每个部位模型都会有一些位置和方向信息,用于挂接在其他部位模型上,比如将头部挂接到躯干部,再将肢体挂接到躯干部就可以构成一个完整的怪物模型。当然,一些在水中的怪物可能不包含四肢,那么将肢体挂接到躯干部这个步骤什么都不做即可。

之所以在制作怪物模型时将头部、躯干、肢体模型分开制作,是便于同类型怪物的3个
组成部位进行互换。试想一下,如果针对亡灵类怪物制作了3个头部、3个躯干以及3个肢体,则最多可以组合出27个外观不同的亡灵类怪物,这既节省了游戏制作成本,又节省了游戏运行时对内存的消耗。

程序需要先把怪物模型载入内存并进行装配以保证正确地显示给玩家看。所以程序需
要进行如下编码步骤:

  • 将怪物的躯干模型信息读人内存并提取其中的位置和方向信息;

  • 将怪物的头部和四肢模型信息读人内存并提取其中的位置和方向信息;

  • 将头部和四肢模型以正确的位置和方向挂接(Mount)到躯干部位,从而装配出完整的怪物模型。

我们实现一个Monster父类。

class Monster
{
public:Monster(int life, int magic, int attack):m_life(life), m_magic(magic), m_attack(attack){}// 创建怪物的纯虚函数,具体实现将在子类中进行void Assemble(string strmodelno)//参数:模型编号,形如“1253679201245”等,每种位的组合都有一些特别的含义,这里无须深究{LoadTrunkModel(strmodelno.substr(4, 3));//载人躯干模型,截取某部分字符串以表示躯干模型的编号LoadHeadkModel(strmodelno.substr(7, 3));//载人头部模型并挂接到躯干模型上LoadLimbsModel(strmodelno.substr(10, 3)); //载人四肢模型并挂接到躯干模型上}virtual void LoadTrunkModel(const string& strno) = 0;virtual void LoadHeadkModel(const string& strno) = 0;virtual void LoadLimbsModel(const string& strno) = 0;virtual~Monster() {}
protected:						int m_life; int m_magic;int m_attack;
};

在上述代码中做了很多简化,只是大致的实现代码,在Assemble成员函数中实现了载人一个怪物模型的固定流程一分别载入了躯干、头部、四肢模型并将它们装配到一起,游戏中所有怪物的载入都遵循该流程(其中的代码是稳定的,不发生变化。

因为亡灵类怪物、元素类怪物、机械类怪物的外观差别巨大,所以虽然这3类怪物的载人流程相同,但不同种类怪物的细节载人差别很大,所以,将LoadTrunkModelLoadHeadModelLoadLimbsModel(构建模型的子步骤)成员函数写为虚函数以方便在Monster的子类中重新实现。

// 亡灵怪物类
class M_Undead : public Monster {
public:M_Undead(int life, int magic, int attack) : Monster(life, magic, attack) {cout << "一只亡灵类怪物来到了这个世界" << endl;}void LoadTrunkModel(const string& strno) override {cout << "载入亡灵躯干模型:" << strno << endl;}void LoadHeadkModel(const string& strno) override {cout << "载入亡灵头部模型:" << strno << endl;}void LoadLimbsModel(const string& strno) override {cout << "载入亡灵四肢模型:" << strno << endl;}
};
// 元素类怪物
class M_Element : public Monster {
public:M_Element(int life, int magic, int attack) : Monster(life, magic, attack) {cout << "一只元素类怪物来到了这个世界" << endl;}void LoadTrunkModel(const string& strno) override {cout << "载入元素躯干模型:" << strno << endl;}void LoadHeadkModel(const string& strno) override {cout << "载入元素头部模型:" << strno << endl;}void LoadLimbsModel(const string& strno) override {cout << "载入元素四肢模型:" << strno << endl;}
};// 机械类怪物
class M_Mechanic : public Monster {
public:M_Mechanic(int life, int magic, int attack) : Monster(life, magic, attack) {cout << "一只机械类怪物来到了这个世界" << endl;}void LoadTrunkModel(const string& strno) override {cout << "载入机械躯干模型:" << strno << endl;}void LoadHeadkModel(const string& strno) override {cout << "载入机械头部模型:" << strno << endl;}void LoadLimbsModel(const string& strno) override {cout << "载入机械四肢模型:" << strno << endl;}
};

这时在main函数中加入如下代码,创建一个怪物对象并对齐进行生成:

unique_ptr<Monster> undead = make_unique<M_Undead>(100, 50, 20);
undead->Assemble("1253679201245"); // 传入模型编号进行组装
/*
我们会看到如下输入结结果:
一只亡灵类怪物来到了这个世界
载入亡灵躯干模型:679
载入亡灵头部模型:201
载入亡灵四肢模型:245
*/

上述这些代码用于创建怪物对象以显示给玩家看,但怪物的创建比较复杂,严格地说,应该是怪物模型的载入过程比较复杂,需要按顺序分别载入躯干、头部、四肢模型并实现不同部位模型之间的挂接。但是,目前的代码并不能称为生成器模式。通过对过程进一步拆分还可以进一步提高灵活性。

这里将AssembleLoadTrunkModelLoadHeadModelLoadLimbsModel这些与模型载人与挂接步骤相关的成员函数称为构建过程相关函数。

考虑到Monster类中要实现的逻辑功能可能较多,如果把构建过程相关函数提取出来(分离)放到一个单独的类中,不但可以减少Monster类中的代码量,还可以增加构建过程相关代码的独立性,日后游戏中任何由头部、躯干、肢体3个部位组成并需要将头部挂接到躯干部,再将肢体挂接到躯干部的生物,都可以通过这个单独的类实现模型的构建。

// 怪物基类
class Monster {
public:virtual ~Monster() {}
};// 亡灵怪物类
class M_Undead : public Monster {
public:M_Undead() {cout << "一只亡灵类怪物来到了这个世界" << endl;}
};// 元素怪物类
class M_Element : public Monster {
public:M_Element() {cout << "一只元素类怪物来到了这个世界" << endl;}
};// 机械怪物类
class M_Mechanic : public Monster {
public:M_Mechanic() {cout << "一只机械类怪物来到了这个世界" << endl;}
};// 怪物构建器基类
class MonsterBuilder {
public:virtual ~MonsterBuilder() {}// 载入不同部分模型的纯虚函数virtual void LoadTrunkModel(const string& strno) = 0;virtual void LoadHeadkModel(const string& strno) = 0;virtual void LoadLimbsModel(const string& strno) = 0;// 组装模型void Assemble(const string& strmodelno) {LoadTrunkModel(strmodelno.substr(4, 3)); // 躯干模型LoadHeadkModel(strmodelno.substr(7, 3));  // 头部模型LoadLimbsModel(strmodelno.substr(10, 3)); // 四肢模型}// 获取构建结果unique_ptr<Monster> GetResult() { return move(m_pMonster); }protected:unique_ptr<Monster> m_pMonster; // 使用智能指针管理怪物对象
};// 亡灵怪物构建器
class UndeadBuilder : public MonsterBuilder {
public:UndeadBuilder() {m_pMonster = make_unique<M_Undead>(); // 创建亡灵怪物}void LoadTrunkModel(const string& strno) override {cout << "载入亡灵类怪物的躯干部位模型,需要m_pMonster指针调用M_Undead类或其父类中其他诸多成员函数,逻辑代码......" << strno << endl;具体要做的事情其实是委托给怪物子类来完成,委托指把本该自已实现的功能转给其他类实现}void LoadHeadkModel(const string& strno) override {cout << "载入亡灵类怪物的头部模型,需要m_pMonster指针调用M_Undead类或其父类中其他诸多成员函数,逻辑代码......" << strno << endl;}void LoadLimbsModel(const string& strno) override {cout <<"载入亡灵类怪物的四肢模型,需要m_pMonster指针调用M_Undead类或其父类中其他诸多成员函数,逻辑代码......"  << strno << endl;}
};// 元素怪物构建器
class ElementBuilder : public MonsterBuilder {
public:ElementBuilder() {m_pMonster = make_unique<M_Element>(); // 创建元素怪物}void LoadTrunkModel(const string& strno) override {cout << "载入元素类怪物的躯干部位模型,需要m_pMonster指针调用M_Undead类或其父类中其他诸多成员函数,逻辑代码......" << strno << endl;具体要做的事情其实是委托给怪物子类来完成,委托指把本该自已实现的功能转给其他类实现}void LoadHeadkModel(const string& strno) override {cout << "载入元素类怪物的头部模型,需要m_pMonster指针调用M_Undead类或其父类中其他诸多成员函数,逻辑代码......" << strno << endl;}void LoadLimbsModel(const string& strno) override {cout << "载入元素类怪物的四肢模型,需要m_pMonster指针调用M_Undead类或其父类中其他诸多成员函数,逻辑代码......" << strno << endl;}
};// 机械怪物构建器
class MechanicBuilder : public MonsterBuilder {
public:MechanicBuilder() {m_pMonster = make_unique<M_Mechanic>(); // 创建机械怪物}void LoadTrunkModel(const string& strno) override {cout << "载入机械类怪物的躯干部位模型,需要m_pMonster指针调用M_Undead类或其父类中其他诸多成员函数,逻辑代码......" << strno << endl;具体要做的事情其实是委托给怪物子类来完成,委托指把本该自已实现的功能转给其他类实现}void LoadHeadkModel(const string& strno) override {cout << "载入机械类怪物的头部模型,需要m_pMonster指针调用M_Undead类或其父类中其他诸多成员函数,逻辑代码......" << strno << endl;}void LoadLimbsModel(const string& strno) override {cout << "载入机械类怪物的四肢模型,需要m_pMonster指针调用M_Undead类或其父类中其他诸多成员函数,逻辑代码......" << strno << endl;}
};

在上述代码中,可以注意到,在MonsterBuilder类中放置了一个指向Monster类的成员变量智能指针m_pMonster,同时引人GetResult成员函数用于返回这个m_pMonster指针,也就是说,当一个复杂的对象通过构建器构建完成后,可以通过GetResult返回。

重点观察MonsterBuilder类中的Assemble成员函数,前面曾经提过,该成员函数中的代码是稳定的,不会发生变化。所以可以继续把Assemble成员函数的功能拆出到一个新类中(这步拆分也不是必需的)。创建新类MonsterDirector(扮演一个指挥者角色),将
MonsterBuilder类中的Assemble成员函数整个迁移到MonsterDirector类中并按照惯例重新命名为Construct,同时,在MonsterDirector类中放置一个指向MonsterBuilder类的成员变量指针m_pMonsterBuilder,同时对Construct成员函数的代码进行调整(注意也增加了返回值)。完整的MonsterDirector类代码如下:

//指挥者类
class MonsterDirector {
public:// 使用 unique_ptr 作为构造函数的参数MonsterDirector(std::unique_ptr<MonsterBuilder> ptmpBuilder): m_pMonsterBuilder(std::move(ptmpBuilder)) {}// 使用构建器创建怪物unique_ptr<Monster> ConstructMonster(const string& modelNumber) {m_pMonsterBuilder->LoadTrunkModel(modelNumber.substr(0, 3)); // 组装躯干m_pMonsterBuilder->LoadHeadkModel(modelNumber.substr(3, 3)); // 组装头部m_pMonsterBuilder->LoadLimbsModel(modelNumber.substr(6, 3)); // 组装四肢return m_pMonsterBuilder->GetResult();}void SetBuilder() {//指定新的生成器}
private:unique_ptr<MonsterBuilder> m_pMonsterBuilder; // 指向生成器类的父类
};

我们可以这样使用:

string modelNumber = "1253679201245"; // 模型编号// 创建指挥者,并传入智能指针
auto undeadBuilder = std::make_unique<UndeadBuilder>();
MonsterDirector director(std::move(undeadBuilder));// 构造怪物
director.ConstructMonster(modelNumber);

引入生成器模型的定义:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

在上述范例中,MonsterBuilder类是对象的构建,而Monster类是对象的表示,这两个类是相互分离的。构建过程是指MonsterDirector类中的Construct成员函数所代表的怪物模型的载人和装配(挂接)过程,该过程稳定不会发生变化(稳定的算法),所以只要传递给MonsterDirector不同的构建器子类(M_UndeadBuilderM_ElementBuilderM_MechanicBuilder),就会构建出不同的怪物,可以随时调用MonsterDirector类的SetBuilder成员函数为MonsterDirector(指挥者)指定一个新的构建器以创建不同种类的怪物对象。

在这里插入图片描述

我们可以发现生成器模式包含四种角色。

  • 抽象构建器Builder):为创建一个产品对象的各个部件指定抽象接口
    LoadTrunkModelLoadHeadModelLoadLimbsModel),同时,也会指定一个接口(GetResult)用于返回所创建的复杂对象。这里指MonsterBuilder类。
  • 具体构建器Concrete Builder):实现了Builder接口以创建(构造和装配)该产品的各个部件,定义并明确其所创建的复杂对象,有时也可以提供一个方法用于返回创建好的复杂对象。这里指M_UndeadBuilderM_ElementBuilderM_MechanicBuilder类。
  • 产品Product):指的是被构建的复杂对象,其包含多个部件,由具体构建器创建该产品的内部表示并定义它的装配过程。这里指M_UndeadM_ElementM_Mechanic类。
  • 指挥者Director):又称主管类,这里指MonsterDirector类。该类有一个指向抽象构建器的指针(m_pMonsterBuilder),利用该指针可以在Construct成员函数中调用构建器对象中“构建和装配产品部件”的方法来完成复杂对象的构建,只要指定不同的具体构建器,用相同的构建过程就会构建出不同的产品。同时,Construct成员函数还控制复杂对象的构建次序(例如,在Construct成员函数中对LoadTrunkModelLoadHeadModelLoadLimbsModel的调用是有先后次序的)。在使用这段内容时,只需要生成一个具体的构建器对象,并利用该构建器对象创建指挥者对象并调用指挥者类的Construct成员函数,就可以构建一个复杂的对象。

前面已经说过,从MonsterBuilder分拆出MonsterDirector这步不是必需的,不做分拆可以看作生成器模式的一种退化情形,当然,此时客户端就需要直接针对构建器进行编码了。一般的建议是:如果MonsterBuilder类本身非常庞大、非常复杂,则进行分拆,否则可以不进行分拆,总之,复杂的东西就考虑做拆解,简单的东西就考虑做合并。

生成器模式结构

在这里插入图片描述

生成器模式的核心组成部分如下:

  1. 生成器接口(Builder:声明通用的产品构造步骤。
  2. 具体生成器(Concrete Builders:实现生成器接口,提供不同的构造过程,能够生成不遵循同一接口的产品。
  3. 产品(Products:最终生成的对象,不同生成器构造的产品可以不属于同一类层次结构。
  4. 主管(Director:定义构造步骤的调用顺序,用于创建和复用特定的产品配置。
  5. 客户端(Client:将生成器对象与主管类关联,通过主管类调用生成器来构建产品。可以在不同的构建过程中使用不同的生成器。

三、总结

通过上述案例,我们不难看出生成器模式主要用于分布建立一个复杂的对象,其中的构建步骤是一个稳定的算法,而复杂对象各个部分的创建会有不同的变化。

生成器模式的核心要点在于将构建算法和具体的构建算法分离,这样构建算法就可以被重用,通过编写不同的代码又可以很方便地对构建实现进行功能扩展。引入指挥者类后,只要使用不同的生成器,利用相同的构建过程就可以构建出不同的产品。

构建器接口定义的是如何构建各个部件,也就是说,当需要创建具体部件的时候,交给构建器来做。而指挥者有两个作用:

  • 负责通过部件以指定的顺序来构建整个产品(控制了构建的过程)。
  • 指挥者通过提供Construct接口隔离了客户端(指main主函数中的代码)与具体构建过程必须要调用的类的成员函数之间的关联

对于客户端,只需要知道各种具体的构建器以及指挥者的Construct接口即可,并不需要知道如何构建具体的产品。想象一个项目开发小组,如果main中构建产品的代码由普通组员编写,这项工作自然比较轻松,但是,支撑代码编写所运用的设计模式及实现一般是由组长来完成,显然这项工作要复杂得多。

工厂方法模式与生成器模式也有类似之处,但生成器模式侧重于一步步构建一个复杂的产品对象,构建完成后返回所构建的产品,工厂方法模式侧重于多个产品对象(且对象所属的类继承自同一个父类)的构建而无论产品本身是否复杂。

  • 生成器模式可以分步创建对象, 暂缓创建步骤或递归运行创建步骤。

  • 生成不同形式的产品时, 可以复用相同的制造代码。将一个复杂对象的创建过程封装起来。用同一个构建算法可以构建出表现上完全不同的产品,实现产品构建和产品表现(表示)上的分离。建造者模式也正是通过把产品构建过程独立出来,从而才使构建算法可以被复用。这样的程序结构更容易扩展和复用。

  • 单一职责原则。 可以将复杂构造代码从产品的业务逻辑中分离出来。

  • 产品的实现可以随时被替换(将不同的构建器提供给指挥者)。而且向客户端隐藏了产品内部的表现。

但是有如下缺点:

  • 要求所创建的产品有比较多的共同点,创建步骤(组成部分)要大致相同,如果产品很不相同,创建步骤差异极大,则不适合使用建造者模式,这是该模式使用范围受限的地方。
  • 生成器模式涉及很多的类,例如需要组合指挥者和构建器对象,然后才能开始对象的构建工作。

生成器重点关注如何分步生成复杂对象。 抽象工厂专门用于生产一列相关对象。 抽象工厂会马上返回产品, 生成器则允许你在获取产品前执行一些额外构造步骤。你可以在创建复杂组合模式树时使用生成器, 因为这可使其构造步骤以递归的方式运行。可以结合使用生成器和桥接模式: 主管类负责抽象工作, 各种不同的生成器负责实现工作。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/59272.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Python 如何在 Web 环境中使用 Matplotlib 进行数据可视化

Python Matplotlib 在 Web 环境中的可视化 数据可视化是数据科学和分析中一个至关重要的部分&#xff0c;它能帮助我们更好地理解和解释数据。在现代应用中&#xff0c;越来越多的开发者希望能够将数据可视化结果展示在网页上。Matplotlib 是 Python 中最常用的数据可视化库之…

模型部署流程

神经网络部署流程 工业界应用神经网络时&#xff0c;往往要对学术界产出的模型进行优化&#xff0c;才能在推理设备/服务器上实现更高的效率&#xff0c;从而降低成本&#xff0c;这整个过程也一般称之为模型部署&#xff08;Deployment&#xff09;。 部署的目的 模型部署目…

vue2中使用vue-awesome-swiper实现轮播

swiper官方文档&#xff1a;Swiper中文网-轮播图幻灯片js插件,H5页面前端开发 1.安装 注意&#xff1a;swiper和vue-awesome-swiper的版本一定一定一定要相对应&#xff0c;版本对应如下&#xff1a; Swiper 5-6 vue-awesome-swiper4.1.1(vue2) Swiper 4.x vue-awesome-swi…

less解决function中return写法在浏览器被识别成Object导致样式失败的问题

问题描述&#xff1a; 一开始写的是: baseFontSize: 37.5px;//基于屏幕尺寸/10得出的基准font-size// return失败,浏览器显示为[object Object],[object Object] .pxToRem(px){value: px / baseFontSize * 1rem;return value; } 使用height: .pxToRem(40px);之后浏览器却是这…

【04】【Maven项目热部署】将Maven项目热部署到远程tomcat服务器上

1.虽然现在Maven中央仓库中支持的tomcat插件只支持到tomcat7这个版本&#xff0c;但是可以利用这个插件对Web项目进行热部署&#xff0c;热部署到远程服务器的tomcat服务器上&#xff0c;远程服务器上的tomcat版本可以是更高的版本&#xff0c;比如说tomcat8、9、10或更高的版本…

开源一款前后端分离的企业级网站内容管理系统,支持站群管理、多平台静态化,多语言、全文检索的源码

大家好&#xff0c;我是一颗甜苞谷&#xff0c;今天分享一款前后端分离的企业级网站内容管理系统&#xff0c;支持站群管理、多平台静态化&#xff0c;多语言、全文检索的源码。 前言 在当今的数字化时代&#xff0c;企业网站和个人博客已成为信息传播和品牌建设的重要渠道。…

mfc | mfc集成opencv,实现摄像头监控、拍照、视频图像处理(亮度、对比度、色调、饱和度)功能

这里是引用 文章目录 一、开发环境二、MFC项目创建三、集成opencv3.1 opencv安装3.2 添加项目属性3.3 测试OpenCV&#xff08;打开摄像头&#xff09;3.4 OPENCV视频嵌入到弹框中 四、关闭摄像头、拍照功能实现4.1 添加按钮4.2 添加全局静态变量4.3 关闭摄像头功能实现4.4 拍照…

Rust 力扣 - 289. 生命游戏

文章目录 题目描述题解思路题解代码题目链接 题目描述 题解思路 我们记录上一行和当前行转换之后的状态&#xff0c;当前行转换之后的状态计算完毕后调整上一行状态&#xff0c;直至最后一行状态计算完毕后调整最后一行状态 题解代码 pub fn game_of_life(board: &mut V…

【eNSP】华为ensp快速入门实验

一、安装准备 1. 检查和卸载已安装的软件 检查是否已经安装 eNSP 和依赖软件&#xff1a; 打开控制面板&#xff0c;点击“程序和功能”。 搜索列表中是否存在 eNSP 或依赖软件&#xff08;如 WinPcap、Wireshark&#xff09;。 卸载已安装的软件&#xff1a; 如果找到 e…

一:Linux学习笔记(第一阶段)-- 安装软件 vmware workstation 虚拟机软件 centos系统

目录 学习计划&#xff1a; 资源准备 虚拟机软件&#xff1a;就别自己找了 现在换网站了 下载比较费劲 Centos8&#xff1a; 阿里云镜像地址下载&#xff08;下载比较版 但是有不同版本&#xff09;&#xff1a;centos安装包下载_开源镜像站-阿里云 百度网盘地址&#xff…

Java项目:165 springboot人事管理系统

作者主页&#xff1a;舒克日记 简介&#xff1a;Java领域优质创作者、Java项目、学习资料、技术互助 文中获取源码 项目介绍 人事管理系统分为管理员和用户两部分操作角色 本次开发的人事管理系统实现了财务报销管理、字典管理、试卷表管理、试题表管理、考试记录表管理、答题…

单臂路由实现不同VLAN之间设备通信

转载请注明出处 本实验为单臂路由配置&#xff0c;目的为让不同VLAN之间的设备能够互相通信。 1.首先&#xff0c;按照要求配置两个pc的ip地址&#xff0c;以pc0为例子&#xff1a; 2在交换机创建vlan10和vlan20 3.划分vlan&#xff0c;pc0为vlan10的设备&#xff0c;pc1为vla…

【FL0013】基于SpringBoot和微信小程序的机电公司管理信息系统

&#x1f9d1;‍&#x1f4bb;博主介绍&#x1f9d1;‍&#x1f4bb; 全网粉丝10W,CSDN全栈领域优质创作者&#xff0c;博客之星、掘金/知乎/b站/华为云/阿里云等平台优质作者、专注于Java、小程序/APP、python、大数据等技术领域和毕业项目实战&#xff0c;以及程序定制化开发…

2024保姆级微信 AI 机器人教程:如何打造私人和群聊助手

欢迎点击领取 -《前端开发面试题进阶秘籍》&#xff1a;前端登顶之巅-最全面的前端知识点梳理总结 *分享一个使用比较久的&#x1fa9c; 大家好&#xff0c;我是SunnyRun 微信 AI 机器人-人工智能技术&#xff0c;为用户提供服务的自动化系统&#xff1a;具备自然语言处理能…

HyperWorks进阶教程:Altair及其软件介绍

1.1 Altair 简介 Allair是一家全球技术公司&#xff0c;在产品开发、高性能计算和数据智能领域提供软件和云解决方案自1985年成立以来一直致力于为企业的决策者和技术的执行者开发用于仿真分析、优化、信息可视化、流程自动化和云计算的高端技术。Altair 公司的总部位于美国密…

Halcon3D image_points_to_world_plane详解

分三个部分来聊聊这个算子 一,算子的参数介绍 二,算法的计算过程 三,举例实现 第一部分,算子的介绍 image_points_to_world_plane( : : CameraParam, WorldPose, Rows, Cols, Scale : X, Y) 参数介绍: CameraParam,:相机内参 WorldPose 世界坐标系,也叫物体坐标系(成…

使用GetX实现GetPage中间件

前言 GetX 中间件&#xff08;Middleware&#xff09;是 GetX 框架中的一种机制&#xff0c;用于在页面导航时对用户进行权限控制、数据预加载、页面访问条件设置等。通过使用中间件&#xff0c;可以有效地控制用户的访问流程&#xff0c;并在适当条件下引导用户到所需页面。 这…

[java][基础]HTTPTomcatServlet

1&#xff0c;Web概述 1.1 Web和JavaWeb的概念 Web是全球广域网&#xff0c;也称为万维网(www)&#xff0c;能够通过浏览器访问的网站。 在我们日常的生活中&#xff0c;经常会使用浏览器去访问百度、京东、传智官网等这些网站&#xff0c;这些网站统称为Web网站。如下就是通…

CUDA环境安装终极指南——Linux(其它系统也一样)

文章目录 前言检查驱动配置nvcc安装cudnn完活 前言 不用看其它文章了&#xff0c;这篇文章保你不踩任何坑&#xff0c;安装方法简单快速 检查驱动 检查驱动是否安装&#xff0c;输入以下命令 nvidia-smi如果驱动已经安装&#xff0c;则可跳过此步&#xff0c;否则&#xff…

基于MATLAB的身份证号码识别系统

课题介绍 本课题为基于连通域分割和模板匹配的二代居民身份证号码识别系统&#xff0c;带有一个GUI人机交互界面。可以识别数十张身份证图片。 首先从身份证图像上获取0&#xff5e;9和X共十一个号码字符的样本图像作为后续识别的字符库样本&#xff0c;其次将待测身份证图像…