前言
模板是C++中十分重要、强大的功能,本篇博客我们一起学习模板的相关知识。
1. 泛型编程
如果想要实现一个Swap交换函数,可以通过函数重载实现。但是重载有许多缺点:
- 重载的函数仅仅是类型不同,代码复用率不高,只要新类型出现时, 用户就需要自己增加对应的重载函数。
- 代码的可维护性低,一个函数出错了,其他的重载函数都有可能出错。
所谓泛型编程,就是针对广泛的类型,而不是某种具体的类型。编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。
而什么是模板呢?模板就是一个模具,用户给的类型是填充进模具的不同材料,得到的函数、类就是得到的不同铸件。
2. 函数模板
2.1 函数模板的概念
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
2.2 函数模板的格式
template<typename T1, typename T2>
或者template<calss T1, class T2>
其中template和typename是关键字,typename可以用class来替代,但是不能用struct。
template <typename T>
void Swap(T& left, T& right)
{
T temp = left;
left = right;
right = temp;
}
int main()
{
int a1 = 1, a2 = 2;
float b1 = 1.1, b2 = 2.2;
char c1 = 'a', c2 = 'b';
Swap(a1, a2);
Swap(b1, b2);
Swap(c1, c2);
return 0;
}
2.3 函数模板的原理
实际上调用的不是同一个函数,是同一个模板,但是编译器会自动推测类型,自动生成不同的函数来供使用。
在编译阶段,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然后产生一份专门处理double类型的代码。
库里面有swap函数,可以直接用。
2.4 函数模板的实例化
-
隐式实例化 - 让编译器根据实参推演模板参数的实际类型
template<typename 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); // 隐式实例化,T推成int Add(d1,d2); // 隐式实例化,T推成double return 0; }
-
显式实例化 - 在函数名后加上
< >
,并在其中指定特定的模板参数的实际类型
刚才是传了2个相同类型的参数,T可以推导成某个类型,但是当两个参数的类型不同时,就会出现问题:template<typename 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,d1); 这句代码是错误的,它传了int和double两个不同类型的参数进去, 但是我的函数模板的参数列表只有一个t, 编译器无法确定在此处该将T确定为int还是double而报错 此时有两种方法解决这个问题: 1. 强制类型转换,将两个类型强转成同一个类型 2. 使用显式实例化 */ // 1. 强制类型转换 Add(a1, (int)d1); Add((double)a2, d2); // 2. 显式实例化 - 存在一个隐式类型转换 Add<int>(a1, d1); Add<double>(a1, d1); return 0; }
而有些函数无法自动推演,只能显式实例化。
template<typename Ty> Ty* Alloc(int n) { return Ty new[n]; } int main() { Alloc<int>(10); return 0; } // 这种情况下,传参传了10,但不能根据这个10来推演Ty的类型,所以必须显式实例化
2.5 模板参数的匹配原则
-
非模板函数可以和同名的函数模板同时存在,并且该函数模板可以被实例化为这个非模板函数
int Add(int left, int right) { return left + right; } template<class T> T Add(T left, T right) { return left + right; } int main() { Add(1, 2); Add<int>(1, 2); return 0; }
可以看到,这两行代码调用的函数是不同的,一个是Add函数,一个是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; } int main() { Add(1, 2); // 与非模板函数的类型完全匹配,不会调用模板实例化的函数 Add(1, 2.0); // 模板函数可以实例化出更匹配的函数,优先调用模板 return 0; }
-
普通的函数可以允许隐式的自动类型转换,但是模板函数不允许。
// 模板函数 template <typename T> T add(T left, T right) { return left + right; } // 普通函数 int add(int left, int right) { return left + right; } int main() { // 模板函数调用,不会隐式类型转换,所以必须传入相同类型的参数 int result1 = add(3, 5); cout << "Template Function Result: " << result1 << endl; // 普通函数调用,允许隐式的类型转换 int result2 = add(3.5, 2.7); // 隐式地将浮点数转换为整数 cout << "Regular Function Result: " << result2 << endl; return 0; }
3. 类模板
3.1 类模板的定义格式
template<class T1, class T2, ..., class Tn>
class 类模板名
{
// ...
};
下面通过一个动态顺序表来演示:
// 动态的顺序表
// 注意:Vector并不是一个具体的类,而是一个类模板,只有实例化之后才是真的类, Vector<int> 这种才是真的类。
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;
}
T& operator[](size_t pos)
{
assert(pos < _size);
return _pData[pos];
}
private:
T* _pData;
int _capacity;
int _size;
};
template<class T> // 注意,每一个定义前都要加上这句话
Vector<T>::~Vector() // 定义
{
if (_pData)
delete[] _pData;
_size = _capacity = 0;
}
template<class T>
void Vector<T>::PushBack(const T& data) // 定义
{
// ...
}
3.2 类模板的实例化
类模板实例化与函数模板实例化不同,类模板实例化必须在类模板名字后面加上< >
,将实例化的类型放在其中,也就是说必须要显式实例化。类模板并不是一个真正的类,实例化的结果才是真正的类。
普通类:类名和类型是一样的
类模板:类名和类型不一样。对于Vector类,类名:Vector,类型:Vector< T >
一个模板参数给一个域使用。
void Vector<T>::PushBack(const T& data) // 定义
{
// ...
}
3.2 类模板的实例化
类模板实例化与函数模板实例化不同,类模板实例化必须在类模板名字后面加上< >
,将实例化的类型放在其中,也就是说必须要显式实例化。类模板并不是一个真正的类,实例化的结果才是真正的类。
普通类:类名和类型是一样的
类模板:类名和类型不一样。对于Vector类,类名:Vector,类型:Vector< T >
一个模板参数给一个域使用。