C++<模板初阶>
这一集讲一下模板, 这东西理解起来不难, 用起来也很简单, 大家顺着看就ok.
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;
}
之前我们写c语言的时候, 有时候遇到不同类型的交换逻辑, 是不是得要写很多个交换函数, 而且每个函数的名字还不能相同.
当我们学了c++之后学到了函数可以重载, 现在我们可以使用函数重载去写不同类型的交换逻辑, 而且名字可以一样. 不过, 还有一点不足就是, tm
的, 明明逻辑一样的东西, 哥们为什么要写那么多次呢?
所以, 现在有了模板 :
C++ 中的模板是一种强大的特性,它允许你编写与类型无关的代码,从而实现代码复用和泛型编程。模板是 C++ 泛型编程的基础,广泛应用于标准库(如 STL)、容器、算法和各种工具类中。
一、模板的基本概念
模板是一种代码生成机制,它定义了一种通用的模式,编译器会根据实际使用时的类型或值,动态生成具体的代码。模板分为两种:
- 函数模板:用于创建通用函数。
- 类模板:用于创建通用类。
//也就是什么呢 ? 编译器帮我们干了这个脏活累活, 不过放心, 机器不是人, 真的可以往死里用.
二、函数模板
1. 语法
template <typename T> // 模板参数列表
//template就是模板的意思
T max(T a, T b) { // 函数模板定义
return (a > b) ? a : b; //三目运算符
}
2. 关键点
template <typename T>
:声明一个模板参数T
,typename
也可以用class
替代。
template <class T> // 模板参数列表
T max(T a, T b) { // 函数模板定义
return (a > b) ? a : b; //三目运算符
}
T
:代表一个待指定的类型。- 实例化:当调用
max(1, 2)
时,编译器自动推导T
为int
,并生成int
版本的函数。 - 这里的实例化, 其实就是编译器识别参数类型, 然后编译器根据参数类型生产相应版本的函数, 这就是实例化, 也就是生成一个能用的函数.
3. 使用示例//推演实例化
int a = max(1, 2); // T 被推导为 int
double b = max(1.5, 2.5); // T 被推导为 double
4. 显式指定类型//显示实例化
auto c = max<double>(1, 2.5); // 显式指定 T 为 double
//auto前面文章说过,自动识别参数类型
//这里显示指定了T为double类型,那么编译器就会根据你指定的类型生产相应的函数
double max(double a, double b) {
return (a > b) ? a : b;
}
//在这里, 1就会被隐式类型转换为double类型, 2.5就会正常传过去
//如果你不显示的指定T的类型,在这里编译器是会报错的
//为什么呢?
//因为我们的模板参数列表就只有一个类型
// template <class T>
//但是这里传过去了两个类型参数(1, 2.5)
//这里编译器就会报错,看下面的图:
需要注意的是, 编译器会根据参数生成两个不同的函数, 而不是使用同一个函数. 下面用汇编给大家瞧一眼:
不过通常我们使用显示指定类型的时候是下面这种情况:
template<class T>
T* f(int n)
{
T* p = new T[n];
return p;
}
int main()
{
double* c = f<double>(1);
}
大家仔细看这个代码, 你们看f(int n)
这个函数的参数和 T 有什么关系么?
我们前面使用函数模板, 编译器都是通过我们传过去的参数识别 T 的类型是什么, 但是这里编译器能识别出来嘛?
很明显不能嘛, 因为这个函数的参数和 T 没关系嘛, 所以这时候只能显示的指定 T 的类型, 这样才能使用.
此外模板参数其实和函数差不多:
// 模板参数语法 很类似函数参数,函数参数定义的形参对象,模板参数定义的是类型
template<class X, class Y>
//你需要不同类型的参数, 就设置不同名字的参数嘛, 和函数参数差不多
//类型交给编译器识别就好
void func(const X& x, const Y& y)
{
cout << x << y << endl;
}
int main()
{
int a=1;
double b=1.1;
func(&a, &b);
//这里的打印只能使用cout, 因为cout不用指定类型打印
//你用printf还得提前告诉编译器你要打印什么类型
//但是在参数传过来之前,你都是不知道参数类型是什么的
//所以这里只能使用cout
}
所以其实总的来说就是编译器帮我们干了一些我们原本不愿意干的活 .
然后这里再给大家讲一些大家可能会有的疑问:
// 普通函数
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(1.1,2.2);//编译器识别类型,实例化出来一个Add(double left, double right);然后用这个
}
这里给大家讲一下编译器遇到这种类型, 编译器会怎么处理:
首先, 如果编译器能找到现成的函数就用现成的.
其次就是, 有合适的就用合适的, 就算没有合适的可以用, 如果你给了编译器模板, 编译器会现做一份.
如果你没有写函数模板呢, 比如下面这种情况:
// 普通函数
int Add(int left, int right)
{
return left + right;
}
int main()
{
Add(1,2);//用普通函数
Add(1.1,2.2);//隐式类型转换,用普通函数
}
如果实在没有现成的且合适的函数, 且编译器没有办法去现做一份, 它就会将就的用了, 明白嘛?
就像你吃饭一样, 有好吃的肯定吃好吃的昂, 如果自己有时间, 而且会做好吃的, 就算没好吃的, 自己也会去做好吃的.
如果实在没条件, 比如学生时代住宿的学生, 晚上实在饿不行了, 你身边有个小面包, 将就一下你也吃了.
就是这么一个道理.
三、类模板
1. 语法
template <typename T>
class Vector {
private:
T* data;
size_t size;
public:
Vector(size_t n) : data(new T[n]), size(n) {}
~Vector() { delete[] data; }
T& operator[](size_t i) { return data[i]; }
};
2. 关键点
- 类模板的成员函数默认也是模板函数。
- 实例化:使用时必须显式指定类型参数,如
Vector<int> v(10)
。 - 也就是说, 你不能像普通类一样这样写:
Vector v(10)
- 必须标明数据类型:
Vector<int> v(10)
3. 使用示例
Vector<int> v(5); // 创建一个存储 int 的 Vector
v[0] = 100;
4. 类模板的成员函数定义
如果你函数声明和定义分离,就要这样写, 否则不用
template <typename T>
T& Vector<T>::operator[](size_t i) {
return data[i];
}
此外, 类模板里面的成员函数声明和定义不能写在两个文件, 也就是说, 你不能把函数声明写在头文件, 然后cpp
文件再去实现. 这个后面再说.
四、模板参数//以下这些目前阶段, 仅作了解, 看一眼就行, 不用看懂
模板参数可以是:
- 类型参数(如
typename T
) - 非类型参数(如
int N
) - 模板模板参数(稍后介绍)
1. 非类型参数示例
template <typename T, int N>
class Array {
private:
T data[N];
public:
T& operator[](int i) { return data[i]; }
int size() const { return N; }
};
// 使用
Array<int, 5> arr; // 创建一个包含 5 个 int 的数组
2. 非类型参数的限制
- 非类型参数必须是编译时常量(如整数、指针、引用等)。
五、模板特化
模板特化允许为特定类型提供定制实现。
1. 全特化
template <> // 全特化 max 函数模板,针对 const char* 类型
const char* max<const char*>(const char* a, const char* b) {
return (strcmp(a, b) > 0) ? a : b;
}
2. 类模板全特化
template <>
class Vector<bool> { // 针对 bool 类型的特化实现
// 定制实现(如位压缩)
};
3. 偏特化(针对类模板)
template <typename T>
class Vector<T*> { // 偏特化:针对指针类型
// 针对指针的定制实现
};
六、可变参数模板(C++11+)
允许模板接受任意数量和类型的参数。
1. 函数模板示例
template <typename T>
T sum(T value) { // 终止函数
return value;
}
template <typename T, typename... Args>
T sum(T first, Args... args) { // 递归展开参数包
return first + sum(args...);
}
// 使用
int total = sum(1, 2, 3, 4); // 计算 1+2+3+4 = 10
2. 类模板示例
template <typename... Ts>
struct Tuple;
template <>
struct Tuple<> {}; // 空元组
template <typename T, typename... Ts>
struct Tuple<T, Ts...> : Tuple<Ts...> {
T value;
Tuple(T t, Ts... ts) : Tuple<Ts...>(ts...), value(t) {}
};
// 使用
Tuple<int, double, char> t(1, 3.14, 'a');
七、模板的优缺点
优点:
- 代码复用:避免为不同类型编写重复代码。
- 性能优化:在编译期生成专用代码,减少运行时开销。
- 类型安全:在编译期检查类型匹配。
缺点:
- 编译时间增加:模板实例化会显著增加编译时间。
- 错误信息复杂:模板错误信息往往冗长且难以理解。
- 代码膨胀:大量实例化可能导致可执行文件变大。
八、注意事项
- 模板定义与声明分离:模板定义通常需要放在头文件中,因为编译器需要看到完整定义才能实例化。
- 模板模板参数:允许模板接受另一个模板作为参数(如
template <template <typename> class Container>
)。 - SFINAE(Substitution Failure Is Not An Error):利用模板替换失败来实现编译期条件选择。
九、常见应用场景
- 标准模板库(STL):容器(如
vector
、map
)、算法(如sort
)。 - 智能指针(如
shared_ptr
、unique_ptr
)。 - 类型特征库(如
std::is_same
、std::enable_if
)。 - 元编程库(如 Boost.MPL)。