0. 概述:
模板是泛型编程的基础,泛型编程即以一种 独立于任何特定类型 的方式编写代码。
模板是创建泛型或函数的蓝图或公式。容器、迭代器、算法等,都是泛型编程的例子。
在C++中,模板分为函数模板和类模板两种。
1. 函数模板:
函数模板不是一个实在的函数,编译器不能为其生成可执行代码。
定义函数模板后只是一个对参数功能框架的描述,当它具体执行时,将根据传递的实际参数决定其功能。
编译器由模板自动生成函数时,会用具体的类型名对模板中所有的类型参数进行替换,其他部分则原封不动的保留。
同一个类型参数只能替换为同一种类型,编译器在编译到调用函数模板的语句时,会根据实参的类型推断该如何替换模板中的类型参数。
定义:
template <typename 类型参数1, typename 类型参数2, ...>
返回值类型 函数名(形参列表)
{
函数体
}
其中,“类型参数” 是函数所使用的数据类型的 占位符 名称,“返回值类型”、“形参列表”、“函数体” 中都可以使用类型参数占位符。
编译器由模板自动生成函数的过程叫“模板的实例化”。 由模板实例化而得到的函数称为模板函数。
在某些编译器中,模板只有在被实例化时,编译器才会检查其语法正确性。 如果程序中写了一个模板却没有用到,那么编译器不会报告这个模板中的语法错误。
编译器在对模板进行实例化时,并非只能通过调用语句的实参来实例化模板中的类型参数,模板调用语句可以明确指明要把类型实例化为哪种类型。
方法是:
模板名<实际类型参数1, 实际类型参数2, ...>
举例:
//定义函数模板:一个用于比较两数大小的函数,与类型无关:
template <typename T>
inline T const& Max(T const& a, T const& b)
{
return a < b ? b : a;
}
//函数模板的实例化:
int main() {
int a = 4, b = 5;
int c = Max(a, b);
cout << c << endl;
double x = 8.1, double y = 1.8;
double z = Max(x, y);
cout << z << endl;
//模板调用语句可以明确的指明要把类型参数 实例化为哪种类型:
float f = Max<float>(7.7, 6,6);
cout << f << endl;
}
总结:
函数模板的实例化有两种方式:
- 通过模板调用语句的实参来实例化模板中的类型参数;
- 在模板调用语句中明确指明要把类型参数实例化为哪种类型。
1.1 IM项目中对函数模板的实际应用:
//定义函数模板:
//函数的作用是向*server_list链表中的所有服务器发起connect主动连接,并将连接信息保存到server_list中,
//每个连接对应一个T类型的本地实例,使用模板则可以在多个不同类型的server上复用:
template <typename T>
void serv_init(serv_info_t* server_list, uint32_t server_count)
{
for(uint32_t i = 0; i < server_count; i++) {
T* pConn = new T();
pConn->Connect(serv_list[i].server_ip.c_str(), server_list[i]);
server_list[i].serv_conn = pConn;
server_list[i].idle_cnt = 0;
server_list[i].reconnect_cnt = MIN_RECONNECT_CNT / 2;
}
}
//实例化函数模板:
serv_init<CDBServConn>(g_db_server_list, g_db_server_count);
serv_init<CRouteServConn>(g_route_server_list, g_route_server_count);
2. 类模板:
当有两个及两个以上的类,如果其功能相同,仅仅是数据类型不同时,就可以采用类模板的方式实现代码的复用。
定义:
//类模板 声明:
template <typename 类型参数1, typename 类型参数2, ...>
类声明
//类模板 定义:
template <typename 类型参数1, typename 类型参数2, ...>
class 类名
{
public:
类成员函数
private:
类成员
};
举例:
template <typename T>
class A
{
public:
A(T t) {
this->t = t;
}
T& getT() {
return t;
}
private:
T t;
};
int main() {
A<int> a(100);
a.getT();
return 0;
}
2.1 继承中的类模板语法:
当子类从模板类继承的时候(父类是一个模板类),需要让编译器知道父类的具体数据类型是什么(本质上是为了让编译器知道所占用的内存空间大小)。
例如:
//定义一个模板类做为父类:
template <typename T>
class A
{
public:
A(T x) { t = x; }
void out() { cout << t << endl; }
private:
T t;
};
//一个继承模板类的子类:
//必须让编译器知道父类的具体数据类型:
class B : public A<int> {
public:
B(int a, double x) : A<int>(a) { y = x; }
void out() { A<int>::out(); cout << y << endl; }
private:
double y;
}
2.2 类模板中的static关键字:
关键点:
- 相同类型的类模板的static静态成员是所有类对象共享的,这与正常的类保持一致;
- 由于编译器对模板的编译方式使用的是“二次编译”,所以当类模板被初始化为多种实例类型时,也会出现多个对应的不同类型的static静态成员。
当类模板中出现static修饰的静态成员的时候,我们只要按照正常理解就可以了。static的作用是将类的成员修饰成静态的,所谓的静态类成员就是指类的成员为类级别的,不需要实例化对象就可以使用, 而且的所有对象都共享同一个静态类成员,因为类静态成员是属于类而不是对象的。
那么,类模板的实现机制是通过 “二次编译” 原理实现的。
C++编译器并不是在第一次编译类模板的时候就把所有可能出现的类型都分别编译出对应的类(实际也不可能,C++中内置的数据类型太多了),而是在第一次编译的时候编译一部分,遇到泛型不会替换成具体的类型(此时编译器还不知道具体的类型),而是在第二次编译的时候再将泛型替换成具体的类型(此时编译器通过第一次编译已经知道程序中实例化了哪些具体类型)。
由于类模板的“二次编译”原理再加上static关键字修饰的成员,当它们在一起的时候实际上一个类模板会被编译成多个具体类型的类,所以,不同类型的类模板对应的static成员也是不同的(归属于不同的类),但相同类型的类模板的static成员是共享的(归属于同一个类)。
2.3 IM项目中对类模板的实际应用:
//单例模式的模板类:
//类的构造函数封装为private,通过Instance()成员函数来获取实例:
template <typename T>
class Singleton {
public:
static T& Instance() {
if(Singleton::s_instance == 0) {
Singleton::s_instance = CreateInstance();
return *(Singleton::s_instance);
}
}
private:
static T* s_instance;
private:
static T* CreateInstance() {
return new T();
}
};
//实例化Singleton模板类
class TransferTaskManager : public Singleton<TransferTaskManager>
{
public:
~TransferTaskManager();
private:
TransferTaskManager();
}
3. 关键字typename与class的区别:
在模板语法中关键字 class 与 typename 的作用完全一样。
在模板引入C++后,最初定义模板的方法是:
template<class T>......
在这里class关键字表明T是一个类型,后来为了避免class在这两个地方的使用可能给人带来混淆,所以引入了typename这个关键字,它的作用同class一样表明后面的符号为一个类型,这样在定义模板的时候就可以使用下面的方式:
template<typename T>......
参考内容:
https://debuger.blog.csdn.net/article/details/94622335