目录
前言
一般情况下,template<class / typename T> 中 class 与 typename 没有什么区别,但是有一些特殊情况,typename 与 class 是有区别的
例如:我们泛型了Print函数,实现可以打印任意容器内容的功能
template<class Container>
void Print(const Container& v)
{
Container::const_iterator it = v.begin();
while (it != v.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
int main()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
Print(v);
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
Print(lt);
return 0;
}
但是编译不通过,编译器要求必须在 Container::const_iterator 之前加上 typename 作为前缀,即
typename Container::const_iterator it = v.begin();
问题:为什么需要加上typename呢?
- 这是因为编译器无法分辨 Container::const_iterator 究竟是什么,因为此时Container还没有实例化,所以 Container::const_iterator 它可能表示的是类的静态成员变量,也可能是类的内嵌类型(内部类或typedef的类型)
class A { public: int begin() { return 0; } static int const_iterator; }; int main() { A aa; A::const_iterator it = aa.begin(); 编译器报错,因为A::const_iteratot本身是变量,不是类型 所以,如果没有typename,编译器是不知道它究竟是类型还是变量 return 0; }
- Container可能是类型或对象,若为类型,语法可以通过;若为对象,语法错误
解决:
在 Container::const_iterator 前加上typename,以此来告诉编译器后面的Container::const_iterator是一个类型,后面的行为是合乎语法的,等Container实例化之后,在确定后面是什么类型
总结:
- Container是一个没有实例化的模板,那么都需要在Container::iterator 之前加上typename,即使是vector<T>类型
- 只要我们使用没有实例化的类模板时,都需要加上typename声明,例如priority_queue模板参数less在使用未实例化的模板时就加上了typename
这里我们也可以使用auto关键字,
auto it = v.begin();
就不需要typename声明 。在Container之后 auto 自动推导类型
一、非类型模板参数
有时候,我们可能需要非类型的模板参数,来实现所需的类
例如:当我们实现静态的栈时,需要提前知道数组需要开辟多大的空间,但是如果都统一的开同样大的空间,难免会遇到空间冗余或太小等情况。所以,当我们在实例化对象时,如果可以指定数组空间大小,那么就可以解决上面的问题,此时C++11就升级了模板,增加了非类型模板参数,以实现将一个常量作为模板参数
template<class T, size_t N>
class Stack
{
private:
T _a[N];
int _top;
};
int main()
{
Stack<int, 10> st1;
Stack<int, 100> st2;
return 0;
}
对于非模板类型参数
1. 只能为整型(char也可以),但例如double、string等类型都不可以
2. 是常量
因为非模板类型参数大部分都是用在T _a[N]情况,所以最初设计的是整型
在std中还有一个容器array,它就使用了size_t非模板类型参数
array<int, 10> a;
它对比C语言的数组几乎没有任何优势,这就显得委员会更新的很鸡肋,array的优点几乎只有对越界情况检查,越界读写都能检查,而普通数组不能检查越界读,少部分越界写可以检查,但如果仅此优点,vector完全可以替代。
二、类模板的特化
1. 概念
在类名或函数名后用<>指定特化的参数,特化后的类的成员变量根据自己的需求编写,因为它与原模版已经成为了不同的类,成为了分支。一般特化的类都是很小的类
顾名思义,对模板类型进行特殊化处理,如果调用时,实参类型与特化的模板参数类型匹配,则优先调用特化的模板函数或类。
没有特化也可以,但是有特化会更方便
2. 函数模板特化
template<class T>
bool Less(T left, T right)
{
return left < right;
}
int main()
{
cout << Less(1, 2) << endl;
int a = 1, b = 2;
cout << Less(&a, &b) << endl;
return 0;
}
当我们实参传递地址时,比较的是地址的大小,这与我们的意愿相反,这时我们可以使用特化这一功能,针对 int* 类型特化。在调用时,有现成的函数的就用现成的;没现成的用模板实例化合适的函数:
template<class T>
bool Less(T left, T right)
{
return left < right;
}
template<>
bool Less<int*>(int* left, int* right)
{
cout << "bool Less<int*>(int* left, int* right)" << endl;
return *left < *right;
}
但是,如果只是为了比较int型指针指向的值的大小,我们可以不使用模板,直接重载函数即可
bool Less(int* left, int* right)
{
cout << "bool Less(int* left, int* right)" << endl;
return *left < *right;
}
如果是多类型指针,那么还是需要使用模板
template<class T>
bool Less(T* left, T* right)
{
cout << "bool Less(T* left, T* right)" << endl;
return *left < *right;
}
3. 类模板特化
上面是针对函数模板举例,对于函数模板的特化,可以使用重载替代,但是对于类模板的特化就不同了
template<class T1, class T2>
class Data
{
public:
Data()
{
cout << "Data<T1, T2>" << endl;
}
private:
T1 _d1;
T2 _d2;
};
template<>
class Data<int, double>
{
public:
Data()
{
cout << "Data<int, double>" << endl;
}
};
又例如priority_queue中Less仿函数的特化,因为在Less仿函数编写时,已经讲解了如果实参是Date*类型,那么每次比较的是指针的大小,这是错误的,所以我们可以特化类型Date*
template<class T>
class Less
{
public:
bool operator()(const T& x, const T& y)
{
return x < y;
}
};
template<>
class Less<Date*>
{
public:
这里去掉&符号,因为const修饰不到&,会优先修饰*
bool operator()(const Date* x, const Date* y)
{
return *x < *y;
}
};
特化后,就可以在实例化时只传一个参数,编译器用第一个模板参数去匹配,若与特化版本类型相同,则直接使用特化版本
priority_queue<Date*> pq;
还可以改为模板T*,适配所有指针类型
template<class T1, class T2>
class Less<T1*, T2*>
{
public:
bool operator()(const T* x, const T* y)
{
return *x < *y;
}
};
4. 全特化
如果将全部的模板类型都特化即为全特化,全特化后template<>内将没有内容
template<>
class Data<int, double>
{
public:
Data()
{
cout << "Data<int, double>" << endl;
}
private:
//T1 _d1;
//T2 _d2;
};
这样的特化,会在实参类型为int、double时被调用
5. 偏特化
5.1 特化部分参数
只将一部分模板类型特化即为偏特化
适配此类型(T, double)时,优先调用
template<class T1>
class Data<T1, double>
{
public:
Data()
{
cout << "Data<T1, double>" << endl;
}
private:
//T1 _d1;
};
若第二个不是double,则会调用T1, T2模板
5.2 对某些类型的进一步限制
例如:限制参数类型全部都是指针类型时
template<class T1, class T2>
class Data<T1*, T2*>
{
public:
Data()
{
cout << "Data<T1*, T2*>" << endl;
}
private:
};
template<>
class Data<int, double>
{
public:
Data()
{
cout << "Data<int, double>" << endl;
}
private:
//T1 _d1;
//T2 _d2;
};
也可以特化引用
template<class T1, class T2>
class Data<T1&, T2&>
{
public:
Data()
{
cout << "Data<T1&, T2&>" << endl;
}
private:
};
三、模板的分离编译
1. 概念
2. 分离编译
// a.h
template<class T>
T Add(const T& left, const T& right);
// a.cpp
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
// main.cpp
#include"a.h"
int main()
{
Add(1, 2);
Add(1.0, 2.0);
return 0;
}
C/C++程序运行要经历四个阶段
预处理——>编译——>汇编——>链接
预处理 :替换头文件,将.h文件在cpp文件展开,生成 .i 文件
编译:对程序按照语言特性进行词法、语法、语义分析是否错误,并确认已定义的函数的地址,只有声明的函数没有地址,检查无误后生成 .s 汇编文件。因为有声明,这是一种承诺,函数的定义部分编译器会在链接步骤拿着修饰后的函数名去其他文件符号表寻找,所以编译检查可以通过。
汇编:生成 .o 文件,即二进制文件
链接:将文件链接起来,生成 a.out文件
程序链接错误,Add找不到函数地址,这就是模板分离编译的坏处
3. 解决方法
1. 显式实例化
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
template
class Add<int>;
template
class Add<double>;
- 这样就失去了模板的意义,对于每一种类型都需要手动显式实例化,所以不推荐这种解决方法
2. 在一个文件内写声明和定义
- 类内部一般将代码量较短的直接写在类内部,成为内联函数,对于代码量较长的模板函数,可以在类内部声明,在类外部但在同一文件内定义
四、模板总结
1. 优点
- 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库STL因此而产生
- 增强了代码的灵活性,如适配器、仿函数
2. 缺点
- 模板会导致代码膨胀问题,导致编译时间变长
- 出现模板编译错误时,错误信息很乱,不容易定位错误
总结
模板还是有很多细节需要掌握的,了解模板知识后再练习大多数问题就可以解决了
最后,如果小帅的本文哪里有错误,还请大家指出,请在评论区留言(ps:抱大佬的腿),新手创作,实属不易,如果满意,还请给个免费的赞,三连也不是不可以(流口水幻想)嘿!那我们下期再见喽,拜拜!