【C++杂货铺】模板(文末有彩蛋哟)

在这里插入图片描述

文章目录

  • 一、泛型编程
  • 二、函数模板
    • 2.1 函数模板的原理
    • 2.2 函数模板的实例化
    • 2.3 模板参数的匹配原则
  • 三、类模板
  • 四、非类型模板参数
  • 五、模板的特化
    • 5.1 函数模板特化
    • 5.2 类模板特化
  • 六、模板分离编译
  • 七、模板总结
  • 好书推荐
  • 🎁彩蛋

一、泛型编程

📖实现一个通用的交换函数

void Swap(int& left, int& right)
{int temp = left;left = right;right = temp;
}
void Swap(double& left, double& right)
{double temp = left;left = right;right = temp;
}
void Swap(char& left, char& right)
{char temp = left;left = right;right = temp;
}

想要实现一个通用的交换函数不难,借助函数重载就可以。函数重载小伙伴们还记得嘛👀,忘了的小伙伴可以走传送门回去复习一下。如上面代码所示,我们借助函数重载实现了三份Swap函数,分别用来交换两个整型变量、两个双精度浮点型变量、两个字符型变量。

小Tips:函数重载的通用性体现在函数调用的时候。当我们想交换两个变量的时候,不管变量是什么类型,都是直接使用Swap函数,编译器会根据用户传递的实参数据类型,自动去匹配调用对应的交换函数,在用户看来仿佛就只有一份Swap函数,实现了所有类型数据的交换。

📖函数重载的缺陷
通过上面的分析我们可以看出,函数重载的最大缺陷就是,当有一个新类型出现时,需要自己增加对应的重载交换函数,代码的复用率比较低。其次,因为所有的重载函数都是我们自己写的,过程繁琐并且难免会出现差错。

📖模板的引入
函数重载的主要问题就出在需要我们自己去写,那能否告诉编译器一个模子,让编译器代替我们,根据不同的类型利用该模子来生成代码呢?

在这里插入图片描述

如果在C++中,也能够存在这样一个模板,通过给这个模板涂上不同的颜色(类型),来获得不同颜色的五角星(生成具体的代码),那将会方便不少,我们的头发也能少掉一点。巧的是我们的先辈已经将树栽好,我们只需在此乘凉。

📖什么是泛型编程?
编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础,其中模板分为函数模板类模板

在这里插入图片描述

二、函数模板

📖概念
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。

📖格式

template <typename T1, typename T2,.......,typename Tn>
返回值类型 函数名(参数列表){函数体}

小Tipstypename是用来定义模板参数的关键字,也可以用class替换。T1T2…是模板参数,表示类型

//一个交换函数的函数模板
template<typename T>
void Swap( T& left, T& right)
{T temp = left;left = right;right = temp;
}

2.1 函数模板的原理

在这里插入图片描述

函数模板是一个蓝图,它本身并不是函数,是编译器用来产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的事情交给了编译器。

//一个交换函数模板
template<typename T>
void Swap(T& left, T& right)
{T temp = left;left = right;right = temp;
}int main()
{int i1 = 10;int i2 = 20;Swap(i1, i2);double d1 = 1.1;double d2 = 2.2;Swap(d1, d2);char c1 = 'a';char c2 = 'b';Swap(c1, c2);
}

在这里插入图片描述
在编译器编译阶段,对于函数模板的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。例如:当用double类型的数据去调用Swap函数,编译器通过对实参类型的推演,将T确定为double类型,然后生成一份专门处理double类型的代码,对于int类型和char类型的数据也是这样处理的。所以在使用者看来,无论什么类型都是调用Swap函数,但本质上不同的类型会去调用不同的Swap函数。

在这里插入图片描述
小Tips:这里为了给大家展示函数模板的原理,专门写了一个交换函数Swap的模板,实际中当大家需要交换两个数据的时候不需要自己写,因为库中已经帮我们实现好了,可以用库中的swap(s小写)实现对两个同类型数据的交换。

2.2 函数模板的实例化

用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化。

📖隐式实例化
隐式实例化就是让编译器根据实参,自动推演模板参数的实际类型。

template<class T>
T Add(const T& left, const T& right)
{return left + right;
}
int main()
{int a1 = 10, a2 = 20;double d1 = 10.0, d2 = 20.0;Add(a1, a2);Add(d1, d2);//Add(a1, d1);//该语句编译不通过Add(a, (int)d);//强制类型转换,将两个参数设置成同类型return 0;
}

注意Add(a1, d1)会导致编译失败,因为在编译期间,当编译器看到该函数调用的时候,会去自动推演模板参数的类型,首先通过实参a1T推演为int,通过实参d1T推演为double,但是模板参数列表中只有一个T,编译器无法确定此处到底应该将T确定为int或者double类型而报错。此时这里有三种处理方法,第一种方法:在函数模板的参数列表中再增加一个模板参数;第二种方法:用户自己来强制类型转换,对一个参数进行强制类型转换,使得两个参数的类型相同;第三种方法:使用接下来介绍的显式实例化。

小Tips:模板参数也可以作为函数模板的返回值类型。

📖显式实例化
显式实例化是在函数调用阶段,在函数名后跟一个<>,在里面指定模板参数的实际类型。

template<class T>
T Add(const T& left, const T& right)
{return left + right;
}int main(void)
{int a = 10;double b = 20.0;// 显式实例化Add<int>(a, b);return 0;
}

注意:如果传递的实参和实例化出的函数形参类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器会报错。

📖显式实例化的实际使用场景

template<typename T>//模板参数T
T* Alloc(int n)//函数模板的形参没有使用模板参数
{return new T[n];
}int main()
{double* p = Alloc<double>(10);return 0;
}

如上面的代码所示,函数模板Alloc并没有模板参数T类型的形参,因此编译器就无法去根据用户传递的实参类型推导出模板参数T的具体类型。因此,当用户想要调用Alloc函数时,必须进行显式实例化。从这里也可以看出,编译器支持隐式实例化的前提是:模板函数使用了模板参数类型的形参。

小Tips:编译器不会根据函数的返回值去推导模板参数T的类型,就像上面的Alloc函数模板,虽然返回值的类型是模板参数T,但是不管用,编译器不会根据这里去推演T的实际类型,也推演不出来。

2.3 模板参数的匹配原则

  • 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数。
// 专门处理int的加法函数
int Add(int left, int right)
{return left + right;
}
// 通用加法函数
template<class T>
T Add(T left, T right)
{return left + right;
}
void Test()
{Add(1, 2); // 与非模板函数匹配,编译器不需要特化Add<int>(1, 2); // 调用编译器特化的Add版本
}
  • 对于非模板函数和同名的函数模板,如果其他条件都相同,在调用时会优先调用非模板函数,而不会从函数模板产生一个实例。如果函数模板可以产生一个具有更好匹配的函数,那么将选择函数模板。
// 专门处理int的加法函数
int Add(int left, int right)
{return left + right;
}
// 通用加法函数
template<class T1, class T2>
T1 Add(T1 left, T2 right)
{return left + right;
}
void Test()
{Add(1, 2); // 与非函数模板类型完全匹配,不需要函数模板实例化Add(1, 2.0); // 模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的Add函数
}

三、类模板

📖类模板的定义格式

template<class T1, class T2, ..., class Tn>//模板参数列表
class 类模板名
{// 类内成员定义
}; 
// 动态顺序表
// 注意:Vector不是具体的类,是编译器根据被实例化的类型生成具体类的模具
template<class T>
class Vector
{ 
public :Vector(size_t capacity = 10): _pData(new T[capacity]), _size(0), _capacity(capacity){}// 使用析构函数演示:在类中声明,在类外定义。
~Vector();void PushBack(const T& data);
void PopBack();
// ...size_t Size() {return _size;}private:T* _pData;size_t _size;size_t _capacity;
};
// 注意:类模板中函数放在类外进行定义时,需要加模板参数列表
template <class T>
Vector<T>::~Vector()
{if(_pData)delete[] _pData;_size = _capacity = 0;
}

注意Vector不是具体的类,是编译器根据被实例化的类型生成具体类的模具。其次,类模板中的成员函数放在类外面进行定义时需要加模板参数列表,因为此时单独的Vector已经不再表示类型了,编译器可能会根据Vector这个模板,同时实例化出多个类,此时Vector<T>表示一个具体的类型。建议类模板中的成员函数,声明和定义不要分离到两个文件中。

📖类模板的实例化
类模板的实例化与函数模板的实例化不同,类模板的实例化需要在类模板的名字后面跟<>,然后将实例化的类型放在<>中即可,类模板的名字不是真正的类,而实例化的结果才是真正的类

// Vector类名,Vector<int>才是类型
Vector<int> s1;
Vector<double> s2;

📖类模板的优势
类模板的优势和函数模板一样,把本来需要我们自己干的事情,交给了编译器去干。以上面的动态顺序表为例,假如没有类模板,我需要在一个程序中同时定义一个存储int型数据的顺序表,和存储double型数据的顺序表,因为没有类模板的话,动态顺序表的成员变量_pData的类型就只能时固定的intdouble,此时我们就只能写两个动态顺序表的类,将其中一个类的成员变量_pData设置成int类型,用来存储int型数据,将另一个类的成员变量_pData设置成double类型,用来存储double型数据。当程序中还需要一个存储其他类型数据的顺序表时,我们还得自己再增加类,这工作量可见一斑,长时间下去,不管你能不能忍受,你的头发必定受不了。而类模板的出现,就极大的缓解了我们头发的压力,我们只需要写一份动态顺序表的模板代码出来,当要用动态顺序表存储某类型的数据时,我们只需要把该类型告诉编译器,让编译器根据我们写的模板去实例化一个对应的动态顺序表类出来,存储该类型的数据。

四、非类型模板参数

前文提到的模板参数,准确的说应该叫做模板的类型形参,出现在模板参数列表中,跟在classtypename之后,用来表示一种类型;除此之外,模板还有另一种参数:非类型形参,就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。

📖非类型模板参数的使用场景

//静态栈
//#define M 100
template<class T, size_t N = 10>
class Stack
{
public://一些成员函数
private:T _arr[N];//T _arr[M];int _top;
};
int main()
{Stack<int, 10> s1;//实例化一个栈可以存储10个整型数据Stack<double, 100> s2;//实例化出一个栈,可以存储100个double型数据return 0;
}

假设我们这里要实现一个静态的栈,即存储的数据量一经确认是无法扩容的,所以成员变量我们声明了一个数组_arr,在没有非类型模板参数的时候,设计静态的栈一般是通过#define来定义一个标识符常量,我们给这个常量一个初始值,就像上面代码中的M,最终设计出来的栈就能存储M个数据,但是一个程序中M只能被定义一次,当我们需要存储的多组数据,并且每组数据的数据量大不相同,就像上面的,s1需要存储10个int型数据,s2需要存储100个double型的数据,为了满足两者的需求,此时的M就只能大于100,这样对s1来说,就会造成极大的空间浪费。

非类型模板参数的引入就很好的解决了这个问题,我们可以根据实际的需求去传递参数,实例化出最符合自己需求的栈。

📖注意事项:

  • 非类型模板参数一定是一个常量,在类中不能对其进行修改。
  • 非类型模板参数的类型必须是整型,即intsize_tchar等。浮点型、类类型等是不允许作为非类型模板参数的。
  • 非类型模板参数必须在在编译阶段就能确认结果。

五、模板的特化

📖概念
在原模板的基础上,针对特殊类型惊醒特殊化的实现方式。模板特化分为函数模板特化类模板特化

📖为什么要有模板特化
通常情况下,使用模板可以实现一些与类型无关的代码,但是对于一些特殊的类型可能会得到一些错误的结果,此时就需要进行特殊处理。比如下面这个专门用来进行小于比较的函数模板:

template<class T>
bool Less(T left, T right)
{return left < right;
}
int main()
{int a = 10;int b = 20;cout << Less(a, b) << endl; // 可以比较,结果正确int* pa = &a;int* pb = &b;//希望通过传递a,b的地址去比较a,b的大小cout << Less(pa, pb) << endl; // 可以比较,结果错误return 0;
}

在这里插入图片描述
第一次,我们希望直接去比较ab的大小,于是就直接传递了ab,此时的模板参数T被隐式实例化为intLess函数中就进行的是两个整数的比较,比较的结果符合我们的预期。第二次,我们希望通过ab的地址去比较它们两个的大小,于是传递了ab的地址,此时的模板参数T被实例化为int*,因此Less函数就是进行两个地址的比较,并没有按照我们期望的那样,去比较两个地址中存储的数据大小,因此得到的结果也和我们预期的有所不同,此时我们就要对Less函数模板进行特化,让它能够满足我们的要求。

5.1 函数模板特化

📖注意事项:

  • 必须要先有一个基础的函数模板。
  • 关键字template后面接一对空的尖括号<>
  • 函数名后跟一对尖括号<>,尖括号中指定需要特化的类型。
  • 函数形参列表必须要和函数模板的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
//函数模板
template<class T>
bool Less(T left, T right)
{return left < right;
}
//对Less函数模板进行特化
template<>
bool Less<int*>(int* left, int* right)
{return *left < *right;
}int main()
{int a = 10;int b = 20;cout << Less(a, b) << endl; // 可以比较,结果正确int* pa = &a;int* pb = &b;//希望通过传递a,b的地址去比较a,b的大小cout << Less(pa, pb) << endl; // 调用特化之后的版本,不走函数模板return 0;
}

在这里插入图片描述
bool Less<int*>(int* left, int* right)就是对Less函数模板的一个特化,当用户调用Less传递的是int*类型的参数时,会去调用特化后的版本,而不走函数模板。

小Tips:根据2.3小节的内容,一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给出,像下面这样。

bool Less(int* left, int* right)
{return *left < *right;
}

这种实现简单明了,代码的可读性高,容易书写。

5.2 类模板特化

📖全特化
全特化就是将模板参数列表中所有的参数都确定化。

//类模板
template<class T1, class T2>
class Data
{
public:Data() { cout << "Data<T1, T2>" << endl; }
private:T1 _d1;T2 _d2;
};//对Data类模板进行特化
template<>
class Data<int, char>
{
public:Data() { cout << "Data<int, char>" << endl; }
private:int _d1;char _d2;
};void TestVector()
{Data<int, int> d1;//使用类模板Data<int, char> d2;//使用全特化
}

在这里插入图片描述

📖偏特化
任何针对模板参数进行进一步条件限制设计出来的特化版本,叫做偏特化。偏特化有以下两种表现方式:

  • 部分特化

将类模板参数列表中的一部分参数进行特化。

//原类模板
template<class T1, class T2>
class Data
{
public:Data() { cout << "Data<T1, T2>" << endl; }
private:T1 _d1;T2 _d2;
};// 将第二个参数特化为int
template <class T1>
class Data<T1, int>
{
public:Data() {cout<<"Data<T1, int>" <<endl;}
private:T1 _d1;int _d2;
};void TestVector()
{Data<int, int> d1;//使用部分特化Data<double, int> d2;//使用部分特化Data<char, int> d3;//使用部分特化
}

在这里插入图片描述
以上面为例,将原类模板的第一个参数仍旧使用模板T1,第二个模板参数特化成int型,此后只要第二个参数传的是int,就会走特化后的类。

  • 参数更进一步限制
//两个参数偏特化为指针类型
template <typename T1, typename T2>
class Data <T1*, T2*>
{
public:Data() { cout << "Data<T1*, T2*>" << endl; }private:T1 _d1;T2 _d2;
};//两个参数偏特化为引用类型
template <typename T1, typename T2>
class Data <T1&, T2&>
{
public:Data(const T1& d1, const T2& d2): _d1(d1), _d2(d2){cout << "Data<T1&, T2&>" << endl;}private:const T1& _d1;const T2& _d2;
};void test2()
{Data<double, int> d1; // 调用特化的int版本Data<int, double> d2; // 调用基础的模板 Data<int*, int*> d3; // 调用特化的指针版本Data<double*, double*> d4; // 调用特化的指针版本Data<int&, int&> d5(1, 2); //调用特化的引用版本Data<char&, char&> d6('a', 'b'); //调用特化的引用版本
}

在这里插入图片描述

六、模板分离编译

📖什么是分离编译?
一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有的目标文件链接起来形成一个可执行文件的过程就叫做分离编译模式。

📖模板的分离编译
将模板的声明与定义分离,在头文件中进行声明,源文件中完成定义。

// a.h
template<class T>
T Add(const T& left, const T& right);//a.cpp
#include "a.h"template<class T>
T Add(const T& left, const T& right)
{return left + right;
}//text.cpp
#include "a.h"int main()
{Add(1, 2);Add(1.1, 2.2);return 0;
}

在这里插入图片描述
通过结果可以看出,上面的代码产生了error LNK2019链接错误。为什么会这样呢?下面我们来分析一下原因。首先我们需要明确头文件是不会参与编译的,只有以.cpp结尾的文件才能被编译,在预处理阶段,会把头文件的内容拷贝到包含了该头文件的.cpp文件中,其次编译器对工程中的多个源文件是分离开单独编译,编译阶段干的主要工作是按照语言的特性进行词法、语法、语义分析,检查无误后生成汇编代码,最终的到一个.obj结尾的目标文件。

📖先看没有使用模板的分离编译

// a.hint Add(int left, int right);//a.cpp
#include "a.h"int Add(int left, int right)
{return left  + right
}//text.cpp
#include "a.h"int main()
{Add(1, 2);return 0;
}

以上面的代码为例,在编译text.cpp时,编译器不知道Add函数的实现,因为包含的a.h头文件中只有关于Add函数的一个声明,所以当编译器碰到对Add函数的调用时只是给出一个指示,指示链接器应该为它寻找Add函数的实现体。这也就是说test.obj中没有关于Add函数的任何一行二进制代码。

在编译a.cpp的时候,编译器找到了Add函数的实现。于是Add的实现,也就是对应的二进制代码出现在a.obj里。

链接时,链接器在a.obj中找到Add函数的实现代码(二进制)地址(通过符号表导出)。然后将test.obj中悬而未定的call XXX地址改成Add函数的实际地址。

📖模板需要实例化
然而,对于模板来说,模板函数的代码其实并不能直接编译生成二进制代码,其中要有一个“实例化”的过程,以下面的代码为例:

//test.cpp
template<typename T>
void Swap(T& left, T& right)
{T temp = left;left = right;right = temp;
}int main()
{int i1 = 10;int i2 = 20;Swap(i1, i2);double d1 = 1.1;double d2 = 2.2;Swap(d1, d2);
}

如果在test.cpp文件中没有调用Swap函数,Swap就得不到实例化,从而test.obj中也就没有关于Swap的任何一行二进制代码。如果像上面代码中那样,调用了Swap(i1, i2);Swap(d1, d2);,此时test.obj中就有了Swap<int>Swap<double>两个函数的二进制代码段。

📖再来分析模板分离编译

//a.h
template<class T>
class A
{
public:void f();//这里只是声明
};//a.cpp
template<class T>
void A<T>::f()//模板的实现
{//...实现
}//test.cpp
int main()
{A<int> a;a.f();return 0;
}

编译器在执行到a.f()的时候并不知道A<int>::f的定义,因为它不在a.h里面,于是编译器只好寄希望于链接器,希望它能够在其他的.obj文件里面找到A<int>::f的实例,在本例中就是a.obj中,然而a.obj中是没有A<int>::f的二进制代码。因为C++标准明确规定,当一个模板不被用到的时候,他就不该被实例化出来,a.cpp中并有用到A<int>::f,所以实际上a.cpp编译出来的a.obj文件中关于A::f一行二进制代码也没有,因为没有进行任何的实例化,于是链接器就傻眼了,只好给出一个链接错误。

//a.cpp
//显式实例化
template
class A<int>;

如果在a.cpp中加上上面这段代码,a.f();就可以成功执行啦,上面这段代码会将模板专用化,于是,a.obj的符号导出表中就有了A<int>::f这个符号的地址,于是链接器就能够完成任务。但是如果在test.cpp文件中再定义一个A<double> b;,我们就需要在a.cpp中再加入相应的显式实例化,模板的分离编译显然是麻烦的。

📖建议
将声明和定义放到一个文件xxx.hpp里面或者xxx.h其实也是可以的。在同一个文件中对模板的声明和定义进行分离是不会出现链接错误的。

:模板分离编译这块参考了刘未鹏(pongba)大佬的文章,下面附上原文链接:传送门,感兴趣的小伙伴可以点进去看看。

七、模板总结

📖优点

  • 模板复用了代码,将本来需要人去完成的工作交给编译器去做,使人们可以更快的迭代开发,C++的标准模板库(STL)因此而产生。
  • 增强了代码的灵活性。

📖缺点

  • 模板会导致代码膨胀问题,也会导致编译时间变长。
  • 出现模板编译错误时,错误信息非常凌乱,不易定位错误。

结语:本篇文章只是基于语法层面对模板做了简单介绍,关于模板我们需要通过大量的实践,才能体会到模板的魅力以及它的语法细节。因此,在后面的文章中,我会通过模拟实现string和STL中的经典容器,来帮助大家更好的理解模板,感兴趣的小伙伴可以点下关注,更新时会第一时间告知。


好书推荐

在这里推荐两本我个人最近正在读的书,供暑假有意提升自己能力的小伙伴参考:
在这里插入图片描述
近期有购书需求的小伙伴可以直接点击下方书名前往选购!

NO.1《我看见了风暴:人工智能基建革命》
本书深入讲解了阿里、微软等业界巨头在人工智能技术领域的迭代历程,从框架设计、平台开发以及云基础设施等三个关键领域,对AI的发展历史进行详尽而深入的剖析,揭示对未来更远视野的洞察。

NO.2《趣话计算机底层技术》
本书的内容设计独特,通过富有吸引力的故事,深入浅出地解读了计算机中的CPU、存储、I/O、操作系统、系统编程以及安全六大主题。每一章都深入剖析了计算机的核心概念和关键技术,让读者在轻松的阅读时能够迅速提升自身计算机认知水平。

🎁彩蛋

在这里插入图片描述

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

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

相关文章

Spring Boot单元测试入门指南

Spring Boot单元测试入门指南 JUnit是一个成熟和广泛应用的Java单元测试框架&#xff0c;它提供了丰富的功能和灵活的扩展机制&#xff0c;可以帮助开发人员编写高质量的单元测试。通过JUnit&#xff0c;开发人员可以更加自信地进行重构、维护和改进代码&#xff0c;同时提高代…

volley 学习笔记1--发送请求

一、概览 Volley 具有以下优势&#xff1a; 自动网络请求调度。 多个并发网络连接。 透明磁盘和具有标准 HTTP 缓存一致性的内存响应缓存。 支持请求优先级。 取消请求 API。您可以取消单个请求&#xff0c;也可以设置要取消的请求的时间段或范围。 可轻松自定义&#xff…

linux下i2c调试神器i2c-tools安装及使用

i2c-tools介绍 在嵌入式linux开发中&#xff0c;有时候需要确认i2c硬件是否正常连接&#xff0c;设备是否正常工作&#xff0c;设备的地址是多少等等&#xff0c;这里我们就需要使用一个用于测试I2C总线的工具——i2c-tools。 i2c-tools是一个专门调试i2c的开源工具&#xff…

Kafka 入门到起飞系列 - 消费者组管理、位移管理

消费者组 - Consumer Group 上文我们已经讲过消费者组了&#xff0c;我们知道消费组的存在可以保证一个主题下一个分区的消息只会被组内一个消费者消费&#xff0c;从而避免了消息的重复消费 什么是消费组 - Consumer Group&#xff1f; 消费者组是Kafka 提供的可扩展且具有容…

事后多重比较方法

一、案例介绍 由单因素方差分析案例中&#xff0c;为研究郁金对低张性缺氧小鼠存活时间的影响&#xff0c;将36只小鼠随机生成A、B以及 C 三组&#xff0c;每组12个&#xff0c;雌雄各半&#xff0c;分别以10g/kg、20g/kg、40g/kg三种不同剂量的郁金灌胃&#xff0c;各组小鼠均…

从原理到实践,分析 Redisson 分布式锁的实现方案(二)

上篇讲解了如何用 Redis 实现分布式锁的方案&#xff0c;它提供了简单的原语来实现基于Redis的分布式锁。然而&#xff0c;Redis作为分布式锁的实现方式也存在一些缺点。本文将引入Redisson来实现分布式锁。 一、Redisson是什么 Redisson是一个基于Redis的分布式Java框架。它提…

信息安全:网络安全体系 与 网络安全模型.

信息安全&#xff1a;网络安全体系 与 网络安全模型. 网络安全保障是一项复杂的系统工程&#xff0c;是安全策略、多种技术、管理方法和人员安全素质的综合。一般而言&#xff0c;网络安全体系是网络安全保障系统的最高层概念抽象&#xff0c;是由各种网络安全单元按照一定的规…

抖音seo短视频矩阵系统源代码开发技术分享

抖音SEO短视频矩阵系统是一种通过优化技术&#xff0c;提高在抖音平台上视频的排名和曝光率的系统。以下是开发该系统的技术分享&#xff1a; 熟悉抖音平台的算法 抖音平台的算法是通过分析用户的兴趣爱好和行为习惯&#xff0c;对视频进行排序和推荐。因此&#xff0c;开发人员…

Visitor设计模式访问元素方法的问题

Visitor设计模式访问元素方法的问题 GPT给出的答案寻找灵感前置声明Element层次的实例Visitor interface的声明Element interface的声明Element实际类的声明及实现实现一个Visitor客户端代码 实战测试结果 针对C来说&#xff0c;若要实现Visitor设计模式&#xff0c;则会面临循…

SAP安装笔记

1、准备安装介质&#xff0c;SWPM10SP25&#xff0c;51050829_NW750_JavaExport、SAP_HANA_CLIENT、kernel放到/sapcd/NetWeaver目录下 ​​​​​​​ 进入SWPM10SP25执行./sapinst安装 2、待出现 “Open your browser and paste the following URL address to access the G…

上门家政系统开发|上门预约家政小程序定制系统

随着人们生活水平的提高&#xff0c;对于家政服务的需求也越来越高。上门家政小程序的开发为家政服务商家提供了一个全新的经营和服务渠道。本文将介绍上门家政小程序适合的商家以及其优势。   1. 家政公司   家政公司是最直接受益于上门家政小程序开发的商家。通过开发家政…

企业博客资讯如何高效运营起来?

运营一个高效的企业博客资讯需要综合考虑多个因素&#xff0c;包括内容策划、发布频率、优化推广、互动反馈等。下面将从这些方面介绍如何高效运营企业博客资讯。 如何高效运营企业博客资讯 内容策划 首先&#xff0c;需要制定一个明确的内容策略。确定博客的定位和目标受众…

【C语言】指针进阶(二)

&#x1f490; &#x1f338; &#x1f337; &#x1f340; &#x1f339; &#x1f33b; &#x1f33a; &#x1f341; &#x1f343; &#x1f342; &#x1f33f; &#x1f344;&#x1f35d; &#x1f35b; &#x1f364; &#x1f4c3;个人主页 &#xff1a;阿然成长日记 …

【UE5 多人联机教程】04-加入游戏

效果 步骤 1. 新建一个控件蓝图&#xff0c;父类为“USC_Button_Standard” 控件蓝图命名为“UMG_Item_Room”&#xff0c;用于表示每一个搜索到的房间的界面 打开“UMG_Item_Room”&#xff0c;在图表中新建一个变量&#xff0c;命名为“Session” 变量类型为“蓝图会话结果…

MB5B在HDB上的性能调优

背景 MB5B是用于查询物料的收发以及现有库存。日常业务查询,通常会按照月份查看某片地区物料的库存以及收发状态。 调优思路 按照客户日常操作的习惯,得到日常操作的数据范围,选出数据量最为突出最有代表性的地区和物料;利用SE30分别运行不同数量级的数据,比如20个门店、…

利用sklearn 实现线性回归、非线性回归

代码&#xff1a; import pandas as pd import numpy as np import matplotlib import random from matplotlib import pyplot as plt from sklearn.preprocessing import PolynomialFeatures from sklearn.linear_model import LinearRegression# 创建虚拟数据 x np.array(r…

DAY3,Qt(完成闹钟的实现,定时器事件处理函数的使用)

1.完成闹钟的实现&#xff0c;到点播报文本框的内容&#xff1b; ---alarm.h---头文件 #ifndef ALARM_H #define ALARM_H#include <QWidget> #include <QTimerEvent> //定时器处理函数类 #include <QTime> //时间类 #include <QPushButton> //按钮…

蓝牙技术|智能照明市场蓬勃发展,蓝牙技术助力市场发展

照明控制系统在商业和工业领域的应用广泛。例如&#xff0c;智能办公楼、商场、工厂等场所&#xff0c;可以通过照明控制系统实现节能和舒适性的提升。预计将从2023年的74亿美元增长到2032年的108亿美元&#xff0c;复合年增长率(CAGR)为4.3%。 随着LED照明技术在市场上的逐渐普…

QT DAY3

1.思维导图 2.完成闹钟的实现 头文件 #include <QTextToSpeech> #include <QTextEdit> QT_BEGIN_NAMESPACE namespace Ui { class Widget; } QT_END_NAMESPACEclass Widget : public QWidget {Q_OBJECTQLineEdit *edit1new QLineEdit;// QTextEdit *edit2new QTe…

java版企业工程项目管理系统 Spring Cloud+Spring Boot+Mybatis+Vue+ElementUI+前后端分离 功能清单

Java版工程项目管理系统 Spring CloudSpring BootMybatisVueElementUI前后端分离 功能清单如下&#xff1a; 首页 工作台&#xff1a;待办工作、消息通知、预警信息&#xff0c;点击可进入相应的列表 项目进度图表&#xff1a;选择&#xff08;总体或单个&#xff09;项目显示…