文章目录
1.泛型与模板
泛型是一种将类型参数化以达到代码复用的技术,泛型编程是以独立于任何特定类型的方式编写代码。
C++ 中使用模板来实现泛型。模板是泛型编程的基础。模板是创建类或函数的蓝图或公式。我们给这些蓝图或公式提供足够的信息,让这些蓝图或公式真正地转变为具体的类或函数,这种转变发生在编译时。
模板一般分为函数模板和类模板。
2.函数模板之类型参数
2.1 显式类型指定
模板的定义以 template
关键字开头,后面跟 <>
,<>
里边的叫模板参数列表(模板实参),模板参数前面有一个关键字 typename/class
,<>
里面必须至少有一个模板参数。如果模板参数列表中有多个模板参数,那么就要用多个 typename/class
,中间用逗号分开。
#include <iostream>
using namespace std;
template <typename T>
T Max(T a, T b)
{
return a > b ? a : b;
}
int main()
{
cout << Max<int>(1, 2) << endl; // 显式类型指定,T为int类型
return 0;
}
#include <iostream>
using namespace std;
template <typename T, typename Q> // 虽然没有用到Q,但必须显式指定其类型,否则会报错
T Max(T a, T b)
{
return a > b ? a : b;
}
int main()
{
cout << Max<int, double>(1, 2) << endl; // 显式类型指定,T为int类型,Q为double类型
return 0;
}
2.2 自动类型推导
编译器可以根据用户传入的实参的类型来推导出模板参数的具体类型。
#include <iostream>
using namespace std;
template <typename T>
T Max(T a, T b)
{
return a > b ? a : b;
}
int main()
{
cout << Max(1, 2) << endl; // 自动类型推导,T为int类型
return 0;
}
2.3 不允许隐式类型转换
#include <iostream>
using namespace std;
template <typename T>
T Max(T a, T b)
{
return a > b ? a : b;
}
int main()
{
cout << Max(1, 2.0f) << endl; // 报错,编译器不知道T是int类型还是float类型
return 0;
}
如果编译器无法自动推导出模板参数的具体类型,我们必须显式指定其类型。
#include <iostream>
using namespace std;
template <typename T>
T Max(T a, T b)
{
return a > b ? a : b;
}
int main()
{
cout << Max<int>(1, 2.0f) << endl; // 显式类型指定,T为int类型
return 0;
}
结论:我们在调用泛型函数时,在编译时,必须要让编译器知道这个泛型到底是什么类型。不管是编译器自己可以推导出,还是程序员调用时显式指定,必须要让编译器正确地知道,不要让编译器为难,因为函数在调用时,编译器需要为这个类型分配内存空间。
建议:我们在使用泛型函数的时候,最好都显式指定泛型类型。
3.函数模板➡模板的实例化➡模板函数
函数模板:是不进行编译的,因为类型还不知道。
模板的实例化:在编译时,当编译器看到模板定义时,并不会立即产生代码,只有在看到模板调用时,编译器才会产生对应的实例。也就是说,模板没有被使用时,是不会被实例化出来的。
模板函数:是要被编译器所编译的。
对于每一种类型来说,函数模板的实例化只能产生一份,否则多个同名函数会导致函数的重定义。
例如,在下面代码中,虽然两处调用的 T
都为 int
类型,但 int Max(int a, int b)
这个函数只会被实例化出一份。
#include <iostream>
using namespace std;
template <typename T>
T Max(T a, T b)
{
return a > b ? a : b;
}
int main()
{
cout << Max<int>(1, 2) << endl; // 显式类型指定,T为int类型
cout << Max(1, 2) << endl; // 自动类型推导,T为int类型
return 0;
}
4.函数模板之非类型参数
4.1 显式参数指定
非类型参数代表的是一个值,这些值都必须是常量表达式,只能使用,不能修改,因为函数模板的实例化是在编译时进行的。需要注意的是,非类型参数必须是整数/指针/引用。
#include <iostream>
using namespace std;
template <typename T, int SIZE>
void Sort(T* arr)
{
for (int i = 0; i < SIZE - 1; ++i)
{
for (int j = 0; j < SIZE - 1 - i; ++j)
{
if (arr[j] > arr[j + 1])
{
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
}
}
int main()
{
int arr[] = { 12, 5, 7, 89, 32, 21, 45 };
const int size = sizeof(arr) / sizeof(arr[0]); // 常量表达式,编译时就已经计算出来了
Sort<int, size>(arr); // 显式参数指定,T为int类型,SIZE的值为7
for (int val : arr)
{
cout << val << " ";
}
cout << endl;
return 0;
}
4.2 自动参数推导
#include <iostream>
#include <cstring>
using namespace std;
template <unsigned len1, unsigned len2>
int Compare(const char(&p1)[len1], const char(&p2)[len2])
{
return strcmp(p1, p2);
}
int main()
{
cout << Compare("hello", "world") << endl; // 自动参数推导,len1的值为5,len2的值为5
return 0;
}
5.模板的声明和实现一起放在头文件中
编译器在实例化模板时,必须看到模板的定义才会编译通过。如果将模板的声明和实现分离到头文件和源文件中,那么会导致链接错误。因此,我们要将模板的声明和实现一起放在头文件中,然后在源文件中直接进行 #include 包含。
例如,boost 库大量使用模板,在 boost 开源库中频繁出现的 .hpp 头文件,其实就是 .cpp 实现代码混入 .h 文件当中,定义和实现都包含在同一个文件里。