一、为什么需要模板
在 C++ 中,我们经常需要为不同的数据类型编写逻辑完全相同的代码。例如一个求最大值的函数,如果为 int、double、std::string 各写一遍,不仅重复,而且难以维护。模板(Template)正是为了解决「代码复用 + 类型安全」这一矛盾而引入的泛型编程机制。
模板的本质是在编译期根据实参类型生成具体代码 ,它不是一个可以直接运行的函数或类,而是一份「代码蓝图」。理解模板,需要抓住三个关键词:参数化类型 、实例化 、编译期展开。
二、函数模板
2.1 基本语法
函数模板以 template 关键字开头,尖括号内声明模板参数。下面的例子实现一个通用的 swap 函数:
cpp
template<typename T> //其中 typename T 也可以写成 class T,二者在模板参数声明中完全等价。
void swap(T& a, T& b)
{
T tmp = a;
a = b;
b = tmp;
}
2.2 模板参数推导与显式指定
调用函数模板时,编译器会根据实参类型自动推导模板参数,这叫模板实参推导。当推导存在歧义或需要强制类型时,可以显式指定:
cpp
swap<double>(3, 7); // 强制 T = double,3 和 7 会被转换为 double
需要注意,如果两个实参类型不同,推导会失败:
cpp
swap(3, 7.5); // 编译错误:T 同时被推导为 int 和 double
此时可以显式指定类型,或者使用多个模板参数。
2.3 非类型模板参数
模板参数不一定是类型,也可以是编译期常量,称为非类型模板参数:
cpp
template <typename T, int N>
T sumArray(const T (&arr)[N]) {
T sum{};
for (int i = 0; i < N; ++i) {
sum += arr[i];
}
return sum;
}
int main() {
int arr[] = {1, 2, 3, 4, 5};
std::cout << sumArray(arr) << std::endl; // N 自动推导为 5
return 0;
}
非类型模板参数在编译期确定,常用于数组长度、固定尺寸容器等场景。
**非类型模板参数和宏比较:**非类型模板参数可以同时给N传不同的值来用,不会像宏一样写死
非类型模板参数只支持整型常量(bool和char也算是整型),在C++20才支持浮点数,类对象和字符串都是不支持做非类型模板参数的
非类型模板参数的应用:array(静态数组)
静态数组的好处在这里是体现在越界检查的问题上,一般数组越界只能在数组空间后设置一到三个标志位,将其的值设置为负一,等程序结束之后检查是否值被改变来判断是否越界,但是要是直接跳过标志位改数组后面位置的值不会进行报错,越界读不检查,越界写进行抽查,静态数组是一个自定义类型,调用函数operator\[\]时在函数内部可以进行强制检查,不论是读写,一检查一个准
静态数组array是在栈上开辟空间的,一般其实使用Vector就开好了还可以给数据初始化,但是Vector是动态开辟,在堆上,要是频繁开辟空间使用静态数组在栈上开空间比堆上的效率还高
2.4 函数模板的重载与二义性
函数模板可以和非模板函数、其他模板函数构成重载。编译器优先选择非模板函数(精确匹配时),其次才是模板实例:
cpp
void print(int x) { std::cout << "non-template: " << x << std::endl; }
template <typename T>
void print(const T& x) { std::cout << "template: " << x << std::endl; }
int main() {
print(42); // 调用非模板版本
print(3.14); // 调用模板版本
return 0;
}
三、类模板
3.1 基本语法
类模板把类型参数化,使得同一个类可以适配多种数据类型。
函数声明和定义时,函数模板代号不同不影响。template<class T> / template<class X>
下面实现一个简单的栈:
cpp
//用模板实现一个栈
template<class T>
class Stack
{
public:
Stack(int N = 4)
:_a(new T[N])
, capacity(N)
, size(0)
{
}
~Stack()
{
delete[] _a;
_a = nullptr;
capacity = size = 0;
}
SPush(const T& data)
{
//法一:
if (capacity == size)
{
T* tmp = (T*)realloc(_a, capacity * 2);
if (tmp == nullptr)
{
perror("realloc failed!");
return;
}
_a = tmp;
capacity *= 2;
}
//法2:
if (capacity == size)
{
T* tmp = new T[capacity * 2];
memcpy(tmp, _a, sizeof(T) * size);
delete[] _a;
_a = tmp;
capacity *= 2;
}
_a[size++] = data;
}
private:
T* _a;
size_t capacity;
size_t size;
};
**类模板和 typedef 的比较:**如上面可以同时实现 int栈,double栈... ,typedef只能同时使用一种类型,类模板允许同时存在
3.2 类模板的成员函数定义
如果成员函数在类外定义,需要重复模板参数列表,并且类名后要带上模板参数:
cpp
template <typename T>
class Box {
public:
void set(const T& value);
T get() const;
private:
T value_;
};
template <typename T>
void Box<T>::set(const T& value) {
value_ = value;
}
template <typename T>
T Box<T>::get() const {
return value_;
}
3.3 类模板的默认模板参数
类模板支持默认模板参数,函数模板在 C++11 之后也支持:
cpp
template <typename T, typename Container = std::vector<T>> //默认适配器是vector
class MyStack {
private:
Container data_;
};
3.4 成员模板与嵌套依赖类型
类模板内部还可以定义模板成员函数,称为成员模板 。在模板内部引用依赖类型时,需要使用 typename 消歧:
cpp
template <typename T>
class Wrapper {
public:
template <typename U>
void assign(const U& value) {
//...
}
// 这里依赖模板类型T,告诉编译器这里是一个类型,不是变量,不是函数,必须加 typename
typename std::vector<T>::iterator begin() { return vec_.begin(); }
private:
T value_{};
std::vector<T> vec_;
};
四、模板的特化
通用模板并不总是适用于所有类型。
在某些特殊情况下,可以对模板中的一些特殊类型特殊处理,比如想要比较两个指针指向的日期类比较,此时比较的是指针的大小(这里到底比较的是指针本身大小还是地址大小?)想要比较指针指向的日期的大小就要将Date这个类型特化(这种方法不推荐,因为这种情况下的模板不使用引用,当传入一些自定义类型时避免不了拷贝的问题,要是加上const T&,在特化中也要加上,要不然就会报错,但是像上面一样把const加在*前面就不是修饰的指针本身而是指针指向的内容,所以还需要调整const的位置,比较坑人,最推荐的处理方法就是直接写成一个函数调用即可)
这时就需要特化。
4.1 全特化
全特化(Full Specialization)为某一组具体类型提供完全独立的实现:
cpp
#include <cstring>
template <typename T>
T maxValue(const T& a, const T& b) {
return a > b ? a : b;
}
// 针对 const char* 的全特化
template <>
const char* maxValue<const char*>(const char* const& a, const char* const& b) {
return std::strcmp(a, b) > 0 ? a : b;
}
int main() {
const char* s1 = "apple";
const char* s2 = "banana";
std::cout << maxValue(s1, s2) << std::endl; // 输出 banana
return 0;
}
注意:函数模板只支持全特化,不支持偏特化。
4.2 类模板的偏特化
类模板既可以全特化,也可以偏特化(Partial Specialization),即为部分模板参数指定具体类型,或对参数形式做限定:
- 关键字template后面接一对空的尖括号<>
- 函数名后跟一对尖括号,尖括号中指定需要特化的类型
cpp
template <typename T, typename U>
class Pair {
public:
void info() { std::cout << "generic Pair" << std::endl; }
};
// 偏特化:第二个参数固定为 int
template <typename T> //!!!
class Pair<T, int> {
public:
void info() { std::cout << "Pair with int" << std::endl; }
};
// 偏特化:两个参数都是指针
template <typename T,typename U> //!!!
class Pair<T*, U*> {
public:
void info() { std::cout << "Pair of pointers" << std::endl; }
};
int main() {
Pair<double, double> p1; p1.info(); // generic Pair
Pair<double, int> p2; p2.info(); // Pair with int
Pair<int*, char*> p3; p3.info(); // Pair of pointers
return 0;
}
4.3 特化的匹配规则
当存在多个候选时,编译器遵循「最特化优先」原则:全特化 > 偏特化 > 主模板。如果两个偏特化同样特化,则会产生二义性错误。
特化是存在于原模板的基础上的,所以先写原模板再去实现特化之后的模板。
4.4 类型T的真面目
当特化版本为指针时,传参是 int*,模板是 T*,此时的T就是代表 int,传二级指针,T就代表的是一级指针。
所以说T代表的是变量的原类型。
cpp
//模板的全特化
template<class T1,class T2>
class Date
{
public:
Date()
{
cout << "Date(T1,T2)" << endl;
}
private:
T1 d1;
T2 d2;
};
template<typename T1,typename T2> //变1
class Date<T1*, T2*>
{
public:
Date()
{
cout << "Date(T1*,T2*)" << endl;
}
private:
T1 d1;
T2 d2; //!!!!!!
};
template<>
class Date<int, char>
{
public:
Date()
{
cout << "Date(int,char)" << endl;
}
private:
int d1;
char d2;
};
//模板的偏特化
template<typename T1>
class Date<T1, char>
{
public:
Date()
{
cout << "Date(T1,char)" << endl;
}
private:
T1 d1;
char d2;
};
五、模板的分离编译
5.1 为什么模板不能像普通函数那样分离编译
普通函数在头文件中声明、在源文件中定义,编译时生成目标文件,链接时由链接器解析符号。但模板不同:模板本身不是代码,只有实例化后才会生成代码 。而实例化发生在使用处,编译器在编译某个 .cpp 文件时,必须能看到模板的完整定义,才能生成对应类型的实例。
如果只在头文件中放声明、在 .cpp 中放定义,那么使用模板的翻译单元看不到定义,无法实例化,链接时就会报「undefined reference」错误。
5.2 方案一:定义放在头文件中(推荐)
最常用、最推荐的做法是把模板的声明和定义都放在头文件中。
这样每个包含该头文件的翻译单元都能看到完整定义,按需实例化。缺点是可能导致代码膨胀,但现代编译器会通过弱符号合并重复实例。
5.3 方案二:显式实例化(不推荐)
如果确实希望把实现隐藏在 .cpp 中,可以在源文件里显式实例化需要的类型。
六、总结
- 依赖名需要 typename :在模板中引用依赖类型(如
T::iterator)时必须加typename,否则编译器无法区分类型与静态成员。 - 模板参数推导不进行隐式转换 :
maxValue(3, 7.5)会推导失败,需要显式指定类型或使用两个模板参数。 - 函数模板不能偏特化 :需要偏特化行为时,可以改用类模板 + 静态成员函数,或使用重载与
std::enable_if。 - 特化必须在使用前可见:如果特化声明出现在首次使用之后,编译器可能已经用主模板实例化,导致行为不符合预期。
- 错误信息冗长 :模板报错往往嵌套很深,善用
static_assert和std::enable_if可以在编译期给出更友好的提示。
模板是 C++ 泛型编程的基石。函数模板通过类型推导实现逻辑复用,类模板把类型参数化以构建通用容器,特化则为特定类型提供定制实现,而分离编译问题则源于「模板必须在使用处可见完整定义」这一本质约束。