范式编程(C++模板)
前言
C++ 模板是支持泛型编程(Generic Programming)的核心工具,它允许我们编写类型无关的代码,从而提高代码的复用性与灵活性。在 C++11 之前,模板已经发展出函数模板、类模板、模板特化、非类型模板参数等一整套机制,使得程序员可以在编译期实现类型选择、递归计算等功能,甚至能在某种程度上模拟函数重载和分支逻辑。
一、 模板是什么?
模板(Template)是 C++ 中的一种编程机制,用于支持泛型编程(Generic Programming)。它允许你编写与类型无关的代码,编译器会在编译期根据传入的具体类型自动生成对应的代码。
二、 模板的定义:
模板是一种代码生成器,让程序员只写一次逻辑,不同类型的数据都能使用。
1.引入模板(为什么要用)
代码如下(示例):
假设你要写一个函数,来交换两个值。你可能会写类似这样的代码:
void SwapInt(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
void SwapDouble(double& a, double& b) {
double temp = a;
a = b;
b = temp;
}
你会发现:代码重复。这时,模板就派上用场了,允许你写一次代码,适用于任何类型。
template<typename T>
void Swap(T& a, T& b) {
T temp = a;
a = b;
b = temp;
}
2.模板的基本语法:
(1)函数模板:
template<typename T> // 或 template<class T>
void Function(T param) {
// 函数体
}
template:告诉编译器这是一个模板,T 是模板参数,表示类型。
T param:在函数体内,T 会被替换为实际的类型。
1.1函数模板的基本使用与类型推导
template<typename T>
void Swap(T& x, T& y)
{
T tmp = x;
x = y;
y = tmp;
}
测试用例:
int a = 0, b = 1;
Swap(a, b); // 推导出 T=int
double c = 1.1, d = 2.2;
Swap(c, d); // 推导出 T=double
Swap 是一个典型的泛型函数,模板参数 T 可被调用时的实参自动推导。
编译器根据参数类型推导出 T 的具体类型,并生成对应版本的函数。
模板函数在编译期实例化,即编译器根据实参生成实际代码(如 Swap < int >、Swap< double >)。
优点是避免代码重复,缺点是对编译器要求较高,错误信息往往难以读懂。
1.2多个模板参数 + 返回值为模板参数
template<typename T1, typename T2>
T1 Func(const T1& x, const T2& y)
{
std::cout << x << " " << y << std::endl;
return x;
}
测试用例:
Func(1, 2); // T1=int, T2=int
Func(1, 2.2); // T1=int, T2=double
Func 支持两个不同类型参数,返回值使用第一个类型 T1。
体现了模板的“多参数”能力。
模板支持多个类型参数,通过 <T1, T2> 传入。
函数返回值可以是某一个模板参数,增强了灵活性。
1.3模板运算函数:类型推导与隐式转换
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
测试用例:
int a1 = 10, a2 = 20;
double d1 = 10.1, d2 = 20.2;
Add(a1, a2); // 推导为 Add<int>
Add(d1, d2); // 推导为 Add<double>
Add(a1, (int)d1); // 显式强转为 int
Add((double)a1, d1); // 显式强转为 double
模板参数类型必须一致,否则无法自动推导。
当参数类型不一致时,可通过强制类型转换或显式指定模板参数解决。
1.4显示指定模板参数(显式实例化)
Add<int>(a1, d1); // 强制当作 int 处理,d1 被截断
Add<double>(a1, d1); // 强制当作 double 处理,a1 转为 10.0
当推导失败或产生歧义时,可显式指定模板类型。
显式实例化可提供更清晰的意图,避免编译器推导错误。
1.5普通函数、函数模板、模板特化的优先级
函数调用时存在明确的优先级规则:当多个候选函数可供选择时,编译器会按照“普通函数 > 模板特化 > 泛型模板” 的顺序进行匹配和调用。也就是说,如果一个函数调用能与普通函数完全匹配,则优先调用普通函数;如果没有普通函数匹配但存在一个针对某种类型的模板特化(即显式特化),则调用特化版本;最后,若既没有普通函数匹配,也没有特化版本,才会退而求其次调用通用的函数模板(泛型匹配)。这个优先级设计使得我们可以在使用模板时,通过添加普通函数或特化来“覆盖”默认的模板行为,以实现更精细的控制。
#include <iostream>
using namespace std;
// 普通函数
void Print(int a) {
cout << "普通函数: int" << endl;
}
// 函数模板
template<typename T>
void Print(T a) {
cout << "模板函数: T" << endl;
}
// 函数模板特化
template<>
void Print<double>(double a) {
cout << "模板函数特化: double" << endl;
}
int main() {
Print(10); // 调用普通函数(优先级最高)
Print(3.14); // 调用模板特化
Print("hello"); // 调用函数模板(泛型匹配)
return 0;
}
1.6 函数重载和函数模板的优先级
函数模板和普通函数之间,也可以形成“重载”,但优先选非模板的重载版本。
void Show(int a) {
cout << "普通函数" << endl;
}
template<typename T>
void Show(T a) {
cout << "模板函数" << endl;
}
template<typename T>
void Show(T* a) {
cout << "模板函数(指针)" << endl;
}
int main() {
int x = 10;
int* p = &x;
Show(x); // 普通函数
Show(p); // 模板函数(指针)
Show(3.14); // 模板函数
}
1.7带返回类型的函数模板:动态分配示例
=template<class T>
T* Alloc(int n)
{
return new T[n];
}
测试用例:
double* p1 = Alloc<double>(10);
展示了模板函数返回类型为指针。
此类函数通常只能显式指定类型,编译器无法从返回值推导 T。
当模板参数无法从参数中推导时(如只有返回值相关),必须显式指定。
常见于分配器、工厂函数等场景。
(2)类模板
template<typename T> // 或 template<class T>
class MyClass {
public:
MyClass(T val) : _data(val) {}
void Print() const {
std::cout << _data << std::endl;
}
private:
T _data;
};
template这一句告诉编译器:我要定义一个类模板,这个类中的类型是泛型 T。
typename 和 class 都可以使用,含义完全相同。
T 是一个类型参数,占位用的,表示将来由用户决定的数据类型。
class MyClass
定义了一个类模板,类名是 MyClass。注意:模板类名与类型名不同,具体类型要写成 MyClass< int >、MyClass< double > 等。
类内部的所有 T 都是“占位类型”,在你实例化类的时候,会用实际类型替换:
MyClass<int> obj1(10); // T 被替换为 int
MyClass<std::string> obj2("hi"); // T 被替换为 std::string
2.1函数模板与类模板协同使用
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>v1 = { 1,2,3,4 };
Print(v1);
}
此时的编译是不会通过的,由于编译不确定Container::const_iterator是类型还是对象。
使用Auto ,或者使用ypename就是明确告诉编译器这里是类型,等模板实例化再去找。
template<class Container>
void Print(const Container& v)
{
// 编译不确定Container::const_iterator是类型还是对象
// typename就是明确告诉编译器这里是类型,等模板实例化再去找
// typename Container::const_iterator it = v.begin();
auto it = v.begin();
while (it != v.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
int main() {
vector<int>v1 = { 1,2,3,4 };
Print(v1);
}
2.2类模板特化(全特化)
指的是 所有模板参数都被指定类型,专门为该组参数写一个新版本。
template<typename T1, typename T2>
class Data {
public:
void Print() {
cout << "General template" << endl;
}
};
// 全特化:只针对 <int, double>
template<>
class Data<int, double> {
public:
void Print() {
cout << "Specialized for <int, double>" << endl;
}
};
调用示例:
Data<int, int> d1; // 一般模板
Data<int, double> d2; // 使用全特化
2.3类模板特化(偏特化)
只特化部分模板参数,比如固定第二个参数为 double,第一个参数任意:
template<typename T1, typename T2>
class Data {
public:
void Print() {
cout << "General template" << endl;
}
};
// 偏特化:第二个类型参数是 double
template<typename T1>
class Data<T1, double> {
public:
void Print() {
cout << "Partial specialization: T1, double" << endl;
}
};
也支持对指针、引用类型进行偏特化:
// 针对两个都是指针的情况
template<typename T1, typename T2>
class Data<T1*, T2*> {
public:
void Print() {
cout << "Partial specialization for pointer types" << endl;
}
};
2.4类模板调用优先级:谁会被选中?
编译器在模板匹配时会:
优先选择能完全匹配的全特化;
如果没有全特化,再看有没有能部分匹配的偏特化;
都没有,就使用主模板。
你可以理解为“匹配越精准,优先级越高”。
#include <iostream>
using namespace std;
// 1. 主模板
template<typename T1, typename T2>
class Data {
public:
void Print() {
cout << "Primary Template" << endl;
}
};
// 2. 偏特化:第二个参数是 double
template<typename T1>
class Data<T1, double> {
public:
void Print() {
cout << "Partial Specialization <T1, double>" << endl;
}
};
// 3. 全特化:<int, double>
template<>
class Data<int, double> {
public:
void Print() {
cout << "Full Specialization <int, double>" << endl;
}
};
int main() {
Data<char, char> d1; // 主模板
Data<int, double> d2; // 全特化
Data<float, double> d3; // 偏特化
d1.Print(); // Primary Template
d2.Print(); // Full Specialization <int, double>
d3.Print(); // Partial Specialization <T1, double>
return 0;
}
2.5使用整型常量作为非类型模板参数
常量表达式:非类型模板参数的值必须在编译时已知,这意味着它们必须是常量表达式(例如 const int 或 constexpr),也就是说,传进去的值必须在编译期间就能确定。
支持类型限制:非类型模板参数的类型通常是有限制的,不能随意使用任何类型(例如,不能直接传递浮点数)。
常用于数组大小:非类型模板参数最常见的用途是用于指定数组的大小,或者作为常量用于算法的参数
#include <iostream>
using namespace std;
template<typename T, int Size> // 非类型模板参数 Size
void printArray(T(&arr)[Size]) {
for (int i = 0; i < Size; ++i) {
cout << arr[i] << " ";
}
cout << endl;
}
int main() {
int arr[5] = {1, 2, 3, 4, 5};
printArray(arr); // N 被推导为 5
return 0;
}
三、申明实现分离
在普通类中,我们可以把声明写在 .h 文件里,把实现写在 .cpp 文件中。
但是模板类不行
模板不是提前编译好的代码,而是在实例化点生成代码。
如果模板实现写在 .cpp 文件中,其他使用它的 .cpp 文件在编译时看不到它的完整实现,就无法实例化出所需代码,最终导致链接错误。
因此,模板类和函数的定义与实现必须放在 .h 文件中,或者通过 #include 方式包含在头文件里。