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中构建产品的代码由普通组员编写,这项工作自然比较轻松,但是,支撑代码编写所运用的设计模式及实现一般是由组长来完成,显然这项工作要复杂得多。

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

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

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

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

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

但是有如下缺点:

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

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Task :prepareKotlinBuildScriptModel UP-TO-DATE,编译卡在这里不动或报错

这里写自定义目录标题 原因方案其他思路 原因 一般来说&#xff0c;当编译到这个task之后&#xff0c;后续是要进行一些资源的下载的&#xff0c;如果你卡在这边不动的话&#xff0c;很有可能就是你的IDE目前没有办法进行下载。 方案 开关一下IDE内部的代理&#xff0c;或者…

Python日志系统详解:Logging模块最佳实践

Python日志系统详解&#xff1a;Logging模块最佳实践 在开发Python应用程序时&#xff0c;日志记录是排查问题、监控系统状态、优化性能的重要手段。Python标准库中提供了强大的logging模块&#xff0c;使开发者可以轻松实现灵活的日志系统。本文将详细介绍Python的logging模块…

「Mac畅玩鸿蒙与硬件14」鸿蒙UI组件篇4 - Toggle 和 Checkbox 组件

在鸿蒙开发中,Toggle 和 Checkbox 是常用的交互组件,分别用于实现开关切换和多项选择。Toggle 提供多种类型以适应不同场景,而 Checkbox 支持自定义样式及事件回调。本篇将详细介绍这两个组件的基本用法,并通过实战展示它们的组合应用。 关键词 Toggle 组件Checkbox 组件开…

探索现代软件开发中的持续集成与持续交付(CI/CD)实践

探索现代软件开发中的持续集成与持续交付&#xff08;CI/CD&#xff09;实践 随着软件开发的飞速进步&#xff0c;现代开发团队已经从传统的开发模式向更加自动化和灵活的开发流程转变。持续集成&#xff08;CI&#xff09; 与 持续交付&#xff08;CD&#xff09; 成为当下主…

Linux上配置NFS服务

架设一台NFS服务器&#xff0c;并按照以下要求配置&#xff1a; 1、开放/nfs/shared目录&#xff0c;供所有用户查询资料&#xff1a; 2、开放/nfs/upload目录&#xff0c;为192.168.xxx.0/24网段主机可以上传目录&#xff0c;并将所有用户及所属的组映射为nfs-upload,其UID和…

docker占用磁盘过多问题

我在windows系统上用docker&#xff0c;安装在C盘环境下&#xff0c;我发现C盘占用了大量的空间&#xff0c;查找后发现是docker的映像文件占用的&#xff0c;于是开始清理&#xff0c;中间还踩个坑&#xff0c;记录一下&#xff0c;下次需要的时候方便找。 踩坑 我本想移动映…

如何批量注册多个Outlook邮箱账号并避免关联

批量注册多个Outlook邮箱账号时&#xff0c;如何避免账号之间的关联性是一个重要的考量因素。会在此文一起探讨如何高效且安全地批量注册多个Outlook邮箱账号&#xff0c;并提供一些实用的建议来确保这些账号不会被关联。 一、Outlook邮箱批量注册机制 在深入注册流程之前&…

LCR 024. 反转链表 最细图片逐行解析过程

LCR 024. 反转链表 给定单链表的头节点 head &#xff0c;请反转链表&#xff0c;并返回反转后的链表的头节点。 示例 1&#xff1a; 输入&#xff1a;head [1,2,3,4,5] 输出&#xff1a;[5,4,3,2,1]示例 2&#xff1a; 输入&#xff1a;head [1,2] 输出&#xff1a;[2,1]示例…

MySQL 数据库备份与恢复全攻略

MySQL 数据库备份与恢复全攻略 引言 在现代应用中&#xff0c;数据库是核心组件之一。无论是个人项目还是企业级应用&#xff0c;数据的安全性和完整性都至关重要。为了防止数据丢失、损坏或意外删除&#xff0c;定期备份数据库是必不可少的。本文将详细介绍 MySQL 数据库的备…

国标GB28181摄像机接入EasyGBS国标GB28181公网平台,IPC和NVR的具体技术要求有哪些?

随着科技的飞速发展&#xff0c;视频监控技术已经成为各行各业不可或缺的安全保障手段。在众多视频监控解决方案中&#xff0c;网络摄像机&#xff08;IPC&#xff09;和网络视频录像机&#xff08;NVR&#xff09;作为构建现代化视频监控系统的两大核心组件&#xff0c;发挥着…

qt QHBoxLayout详解

QHBoxLayout 是 Qt 框架中用于创建水平布局的类。它将子控件以横向的方式排列&#xff0c;并自动调整大小&#xff0c;以适应父窗口的尺寸变化。 重要方法 QHBoxLayout(QWidget *parent nullptr)&#xff1a;创建一个 QHBoxLayout 对象&#xff0c;并指定其父窗口部件。addWi…

Linux更改符号链接

目录 1. 删除旧链接 2. 创建新的符号链接 例如我的电脑上有两个版本的cuda&#xff0c;11.8和12.4 1. 删除旧链接 rm cuda 2. 创建新的符号链接 ln -s /usr/local/cuda-11.8/ /usr/local/cuda

【Oracle实验】字段为空的,无法通过排除判断

Oracle相关文档&#xff0c;希望互相学习&#xff0c;共同进步 风123456789&#xff5e;-CSDN博客 1.场景描述 需求&#xff1a;查询不是某个机构的数据。 同事SQL&#xff1a;where substr(bank_code,1,9) not in(014009001)&#xff1b; 看SQL似乎没什么问题&#xff0c;分析…

总结围绕临床预测模型做的一些事情

&#x1f4dc;总结近期围绕临床预测模型做的一些事情&#x1f379; 思考预测模型如何进入到临床实践&#xff1f; 预测模型在临床上的角色定位是“辅助临床决策”&#xff0c; 即预测模型提供的信息要与人的选择进行结合做出决策&#xff0c; 从已有的研究来看&#xff0c;预测…

C++ 整型大数运算(大整数运算)项目

C 整型大数运算项目 一、项目介绍二、项目变量成员三、项目实现构造函数加法减法乘法先计算再进位边计算边进位 除法与取模判断输入输出 四、项目源代码展示在 Big_integer.h 中&#xff1a;在 Big_integer.cpp 中&#xff1a; 五、测试准确性六、优化方向 一、项目介绍 整型大…

Cesium基础-(Entity)-(Box)

** 里边包含Vue、React框架代码详细步骤、以及代码详细解释 ** 3、Box 盒子 以下是 BoxGeometry 类的属性、方法和静态方法,以表格形式展示: 属性 属性名类型默认值描述minimumCartesian3盒子的最小 x, y, 和 z 坐标。maximumCartesian3盒子的最大 x, y, 和 z 坐标。vertex…

第三十三篇:TCP协议如何避免/减少网络拥塞,TCP系列八

一、流量控制 一般来说&#xff0c;我们总是希望数据传输得更快一些&#xff0c;但是如果发送方把数据发送得太快&#xff0c;接收方可能来不及接收&#xff0c;造成数据的丢失&#xff0c;数据重发&#xff0c;造成网络资源的浪费甚至网络拥塞。所谓的流量控制&#xff08;fl…

Flutter Image和Text图文组件实战案例

In this section, we’ll go through the process of building a user interface that showcases a product using the Text and Image widgets. We’ll follow Flutter’s best practices to ensure a clean and effective UI structure. 在本节中&#xff0c;我们将使用“Te…

---synchronized 关键字---

在多线程编程中&#xff0c;由于代码的并发执行&#xff0c;导致了不同的线程在修改相同的变量会导致变量的值错误 比如 变量 c 2&#xff0c;这里有线程A 和 B一起使用 c变量并对他加1&#xff0c;这时就会有多中情况 这里要注意的是变量c是储存在内存中的&#xff0c;而线…

EDA --软件开发之路

之前一直在一家做数据处理的公司&#xff0c;从事c开发&#xff0c;公司业务稳定&#xff0c;项目有忙有闲&#xff0c;时而看下c&#xff0c;数据库&#xff0c;linux相关书籍&#xff0c;后面跳槽到了家eda公司&#xff0c;开始了一段eda开发之路。 eda 是 electric design …