c++:详解模版 从初阶到进阶的掌握
大家好,这里是彩妙呀~

在我们将stl容器之前,我们先来简单介绍一下我们后续经常使用的一个新对象:模版。
目录
<分隔符>--------模版进阶模块--------<分隔符>
固定大小容器(如stack,vector等容器,也是非类型模版最常用的场景)
什么是模版(初阶)
泛型编程
假设,我们有一个类似交换的函数:
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++实现泛型编程的一种关键机制。它使得我们可以编写能够处理不同数据类型的代码,而不必针对每种类型都重写相同的功能。
通过使用模板,我们能够以一种更加抽象和通用的方式设计程序,从而显著提高代码的复用性和灵活性。

函数模板格式
template<typename T1, typename T2,......,typename Tn>
返回值类型 函数名(参数列表){}
/*
模版参数列表 <class 类型1,class 类型2 .....>
类比|
函数参数列表 <类型 变量1,类型 变量2 ......>
注意的是,两个可以相互使用:template<class T1,typename T2>,但是不建议这么用
*/
//例如
template<typename T>
void Swap( T& left, T& right)
{
T temp = left;
left = right;
right = temp;
}
注意:
typename是用来定义模板参数关键字,也可以使用class(切记:不能使用struct代替 class)
每一个模版类型 T 只能对应一种类型(如 int,double等),因为编译器是按照咱第一个传参类型来生成对应的函数,所以如果类型不匹配,编译则会出错。
函数模板的原理
函数模版本质上并非是一个函数,而是为了创建所需函数的一个蓝图,这个蓝图可以使编译器用特定的方式来生成特定具体类型的函数:

在编译器的编译阶段,当使用模板函数时,编译器会根据传递的实际参数类型来推断并生成对应类型的函数以供调用。
例如,若使用double类型来调用函数模板,编译器便会通过推断实参类型,将T确定为double类型,进而生成一份专门处理double类型数据的代码。
同样地,对于字符类型,编译器也会进行相应的类型推断和代码生成。
函数模版的实例化
不同类型参数使用函数模版时,被称为函数实例化
而模版参数实例化分为:隐式实例化与显式实例化
template<typename T>
T add(T a, T b) {
return a + b;
}
int main() {
int result , result2;
// 编译器推导过程:
// 1. 看到 add(5, 10)
// 2. 推导 T = int
// 3. 生成 int add<int>(int, int)
result = add(5, 10);
// 4. 看到 add(3.14, 2.71)
// 5. 推导 T = double
// 6. 生成 double add<double>(double, double)
result2 = add(3.14, 2.71);
return 0;
}
隐式实例化
让编译器按照所传第一个实参,自己去推断函数所使用的类型:
#include <iostream>
template<class 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;
int x = Add(a1, a2);
std::cout << x << std::endl;
double y = Add(d1, d2);
std::cout << y << std::endl;
//错误:不可以在只定义一个模版的前提下传两种类型的参数:
// double z = Add(a1,d1);
// std::cout << z << std::endl;
}
但前面我们说过,如果传参的类型不一样,编译器会报错。那么我们该怎么去解决这个问题呢?
有多种解决方式:
设计多个模版来匹对多种类型的参数:
#include <iostream>
// 基本形式
template<typename T1, typename T2>
T1 max(T1 a, T2 b) {
return a > b ? a : b;
}
int main() {
// 使用
int x = max(3, 5); // T1,T2 推导为 int
std::cout << x << std::endl;
double y = max(3.14, 2.71); // T1,T2 推导为 double
std::cout << y << std::endl;
int m = max(3, 2.11);// T1 推导为int , T2推导为double
std::cout << m << std::endl;
return 0;
}
强转传参函数,从而保持函数类型一致
template<class T>
void s_swap(T a, T b)
{
T temp = a;
a = b;
b = temp;
}
int main()
{
int x = 1, y = 2;
double n = 1.1, m = 2.2;
s_swap(x, y);
//强转
s_swap(x, (int)n);
return 0;
}
这种方法虽然可以解决上述的问题,但是不推荐:碰到有关类的参数,强转很可能会丢失数据。
第三种:使用显式实例化
显示实例化
显示实例化是程序员明确要求编译器生成特定类型的实例。
只需要在使用函数模版时,在中间加一个<类型>就可以:
template<class T>
void s_swap(T a, T b)
{
T temp = a;
a = b;
b = temp;
}
int main()
{
int x = 1, y = 2;
double n = 1.1, m = 2.2;
s_swap<int>(m,x);
return 0;
}
有些情况下,必须要使用显式实例化:
template<class T , class K> K* fun1(int n) { return new K[n]; } int main() { /*int x = 1, y = 2; double n = 1.1, m = 2.2;*/ fun1(122); //error:没有与参数列表匹配的函数模板"fun1"实例 return 0; }上面代码要构建一个数组,但是函数模版中没有K对应的传参(编译器无法推导K的参数类型),所以这里只能由我们来显式实例化这里的函数fun1.
模板参数的匹配原则
如果同时存在同名的函数与函数模版,且函数模版还可以被实例化。那么,编译器优先会调用同名函数而非使用函数模版来推演函数。
#include <iostream>
using namespace std;
// 专门处理int的加法函数
int Add(int left, int right)
{
cout << "no-template-T"<<endl;
return left + right;
}
// 通用加法函数
template<class T>
T Add(T left, T right)
{
cout << "template-T"<<endl;
return left + right;
}
void Test()
{
int a = Add(1, 2); // 与非模板函数匹配,编译器不需要特化
int b = Add<int>(1, 2); // 调用编译器特化的Add版本
}
int main() {
Test();
}
类模版
类模板的定义格式
template<class T1, class T2, ..., class Tn>
//可以使用class 与 typename,本质上没有区别
class 类模板名
{
// 类内成员定义
};
我们先看一个例子:
#include<iostream>
using namespace std;
// 类模版,使用栈来说明
template<typename T>
class Stack
{
public:
Stack(size_t capacity = 4)
{
_array = new T[capacity];
_capacity = capacity;
_size = 0;
}
//函数中传参类型基本上用模版T,且尽量使用引用来传值传参
void Push(const T& data)
{
//因为我们的类是类模版创建出来的,所以寻常的relloc之类的扩容方法基本上不适用
//所以我们得自己写方法来进行扩容
//最简单的方法就是另开辟一个空间,大小为现在的2倍,在逐一拷贝一一对应的值(即深拷贝)
//最后别忘了delete原来空间,防止内存泄漏
if (_size == _capacity)
{
//手动异地扩容
T* tem = new T[_capacity * 2];
//拷贝数据
memcpy(tem, _array, _capacity);
//防止内存泄漏,释放原地址
delete _array;
_array = tem;
_capacity *= 2;
}
_array[_size++] = data;
}
~Stack()
{
delete[] _array;
_size = 0;
_capacity = 0;
}
private:
T* _array;
size_t _capacity;
size_t _size;
};
int main() {
Test();
}
类模板的实例化
类模板的实例化与函数模板的实例化有所不同。
在类模板实例化时,需要紧跟在类模板名称之后使用尖括号“<>”,并在其中指定实例化的类型。
类模板的名称本身并不代表一个真正的类,只有经过实例化后所产生的结果才是一个具体的类。
int main()
{
//注意 :类模版都是显式实例化
Stack<int> st1;
st1.Push(1);
st1.Push(2);
st1.Push(3);
Stack<double> st2;
st2.Push(1);
st2.Push(2);
st2.Push(3);
return 0;
}
注意:
模版不建议声明和定义分离到两个文件.h 和.cpp会出现链接错误(因为前置知识储备不足,所以彩妙模版进阶会详细说明)。
<分隔符>--------模版进阶模块--------<分隔符>
下面是要了解一定C++知识后在学习的知识。推荐各位学完例如容器适配器,深层次的STL(学到deque即可)再来看比较舒服一点~
非类型模板参数
非类型模板参数(Non-type Template Parameter):模板参数的一种,接收编译期中的常量值(如整数、指针地址等),而非 typename/class 指定的类型。
//定义
template<class T1,size_t N = 10>
void add(T1 a,size_t n = N) {
}
// 定义一个模板类型的静态数组
template<class T, size_t N = 10>
class array
{
public:
T& operator[](size_t index){return _array[index];}
const T& operator[](size_t index)const{return _array[index];}
size_t size()const{return _size;}
bool empty()const{return 0 == _size;}
private:
T _array[N];
size_t _size;
};
非类型模版参数本质上是在这个模版所在的类域中定义了一个常量(上述代码中,这个常量就是size_t N),并且,这个常量会只会存在于这个类域中,随着类域的销毁而销毁。
它与类型模版参数的区别:
类型
示例
核心差异
类型模板参数
template<class T>
接收类型(如 int、string)
非类型模板参数
template<int N>
接收编译期常量值(如 5、&global_var)
当然,不是所有的类型都支持这么写,下面会列出支持的参数类型(按照C++版本迭代来看):
1. 基础支持类型(C++11 及之前)
- 整型家族:int、size_t、long long、bool、char 等(必须是常量表达式)。
- 指针类型:指向全局 / 静态对象 / 函数的指针(含 nullptr)。
- 引用类型:绑定全局 / 静态对象 / 函数的左值引用。
- 枚举类型:有作用域(enum class)或无作用域枚举的常量值。
- std::nullptr_t(C++11 新增)。
2. 扩展支持类型(C++17 及之后)
- auto 自动推导(C++17):无需显式声明参数类型,编译器根据实参自动推断。
- 浮点数(C++20):仅支持字面量形式(如 3.14),不支持浮点变量(避免精度歧义)。
- 字面量类类型(C++20):满足特定条件的类类型(如 std::string_view 字面量)。
使用场景
固定大小容器(如stack,vector等容器,也是非类型模版最常用的场景)
为了更好地提高性能,避免动态内存分配,我们可以定义一个非类型模版来使得容器先按固定大小配置:
#include <iostream>
using namespace std;
// 固定大小数组(类似 std::array 的实现)
template<class T, size_t N = 5> // N 是非类型模板参数(数组大小)
class FixedArray {
private:
T data[N]; // 编译期确定数组大小,静态分配
public:
constexpr size_t size() const { return N; }
T &operator[](size_t idx) { return data[idx]; }
};
int main() {
// 实例化:
FixedArray<int> arr1; // 大小为 5 的 int 数组
cout << arr1.size() << endl;
FixedArray<double, 10> arr2; // 大小为 10 的 double 数组
cout << arr2.size() << endl;
}
编译期计算(模板元编程)
这么设计会有一个好处:我们在使用非类型模版时可以直接传值进去:
#include <iostream>
using namespace std;
template <class T,size_t A = 0,size_t B = 0>
class text {
public:
T add() {
return A+B;
}
};
int main() {
//这里就可以直接传值进去,非常方便
text<int,1,2> a;
cout << a.add() << endl;
}
模板的特化
在我们使用模版时(假如要用到指针/地址/自定义类时),模版对应的模版函数的结构可能会出错,而对于那些特殊类型(如指针,引用,特殊的类等),我们需要做出特殊处理:
#include <iostream>
using namespace std;
// 先定义一个日期类(用于演示指针比较的场景)
class Date {
public:
Date(int year = 2024, int month = 2, int day = 8)
: _year(year), _month(month), _day(day) {}
// 日期类的小于比较运算符(核心:比较日期内容)
bool operator<(const Date& d) const {
if (_year != d._year)
return _year < d._year;
if (_month != d._month)
return _month < d._month;
return _day < d._day;
}
// 打印日期(方便测试)
void Print() const {
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year, _month, _day;
};
// 基础函数模板:通用的小于比较(适用于普通类型,但指针类型会出错)
template<class T>
bool Less(T left, T right) {
return left < right;
}
int main() {
// 测试1:普通类型(int)—— 基础模板正常工作
cout << "Less(1, 2) = " << Less(1, 2) << endl; // 输出1(true),正确
// 测试2:指针类型
Date d1(2024, 2, 8); // 早日期
Date d2(2024, 2, 9); // 晚日期
Date* p1 = &d1;
Date* p2 = &d2;
// 注意:如果没有特化版本,这里会比较p1和p2的地址(随机值),结果不可控
cout << "Less(p1, p2) = " << Less(p1, p2) << endl;
cout << "Less(p2, p1) = " << Less(p2, p1) << endl;
return 0;
}

我们可以观察发现:结果与预期不一样,此时,就需要对模板进行特化。
即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。
模板特化中分为函数模板特化与类模板特化。
函数模板特化
函数模版特化的语法:
//必须有一个基础的函数模版
template<class T>
T text(){
// 实现方法
}
// 此时才能定义先前基础函数模版的特化模版
// 先是在template后接上一对空的尖括号
template<>
// 后面相当于函数重载,但是内容必须是你所要特化的类型
// 以指针来举例子
bool text<int*> (int *a,int *b){
// 特殊的实现方法
}
// 注意:要按照函数形参表来传参!
// 函数形参表: 必须要和模板函数的基础参数类型完全相同
// 如果不同编译器可能会报一些奇怪的错误。
// 例如
template<class T>
bool Less(T left, T right) {
return left < right;
}
template<>
bool Less<Date*>(Date* left, Date* right) {
return *left < *right;
}
这里要注意:
在一般情况下,如果我们的函数模版不可以正确的去处理或处理后输出错误的类型,我们可以直接按照函数重载来解决。这样简单暴力明了,且易操作,代码可读性也高。
所以,函数模版在一般情况下都不建议特化。
template<class T> bool Less(T left, T right) { return left < right; } bool Less(Date* left, Date* right) { return *left < *right; }这里编译器会“图省事”,会直接使用传参类型最相符的,而不去使用模版函数。
类模板特化
相对于函数模版来说,类模版也有自己的特化:类模版特化,并且分为两类:全特化(Full Specialization)与偏特化(Partial Specialization),核心区别是是否指定「所有模板参数」:
| 类型 | 定义 | 适用场景 |
| 全特化 | 为所有模板参数指定具体值 / 类型 | 单个特殊参数组合(如 T=int、N=0) |
| 偏特化 | 为部分模板参数指定值 / 类型,或对参数类型做限制 | 一类特殊参数(如指针类型、引用类型、数组类型) |
全特化
顾名思义:全特化就是将模版所有的参数列表中所有的类型都做特化(语法与函数模版类似,具体可以看示例代码):
// 泛化模板(通用版本)
template <typename T>
class Printer {
public:
void print(const T& value) {
std::cout << "通用版本:" << value << std::endl;
}
};
// 全特化:针对 T=int 提供定制实现
template <>
class Printer<int> {
public:
void print(int value) {
std::cout << "int 特化版本:" << value * 2 << std::endl; // 定制逻辑:int 类型翻倍输出
}
};
// 全特化:针对 T=std::string 提供定制实现
template <>
class Printer<std::string> {
public:
void print(const std::string& value) {
std::cout << "string 特化版本:\"" << value << "\"" << std::endl; // 字符串加引号
}
};
int main(){
// 测试:编译器自动匹配对应特化版本
Printer<double> p1;
Printer<int> p2;
Printer<std::string> p3;
p1.print(3.14); // 输出:通用版本:3.14
p2.print(5); // 输出:int 特化版本:10
p3.print("hello"); // 输出:string 特化版本:"hello"
}
偏特化
仅指定部分模板参数,或对参数类型做「限定性约束」(如指针、引用、数组、const 修饰),剩余参数仍保留泛化。
这里要注意:只有类模版才能支持偏特化,而函数模版只能使用函数重载(也推荐使用函数重载来特化)。
偏特化在C++中有两种形式:
(1)只特化部分范式参数:将模板参数类表中的一部分参数特化
#include <iostream>
using namespace std;
using ll = long long;
//先声明基础的模版
template <class T1,class T2>
class text {
public:
void print() {
cout << "T1& - T2& template"<<endl;
}
};
//偏特化:将T2特化为int,方便实现特化函数
template <class T>
class text<T,int> {
public:
void print() {
cout << "T& - int" << endl;
}
};
int main() {
text<ll,ll> t1;
t1.print();
text<ll,int> t2;
t2.print();
}
(2)使得参数有更进一步的限制(例如加引用或者解引用):
#include <iostream>
#include <string>
using namespace std;
// ==================== 通用模板(3个模板参数) ====================
template <typename T, typename T1, typename T2>
class Printer {
public:
void print(const T& value) {
cout << "通用版本: " << value << endl;
}
void add(const T1& a, const T2& b) {
cout << "通用add: " << (a + b) << endl;
}
};
// ==================== 偏特化示例:当T为指针类型时 ====================
template <typename T, typename T1, typename T2>
class Printer<T*, T1, T2> {
public:
void print(T* value) {
if (value) cout << "指针偏特化: 值=" << *value << endl;
else cout << "指针偏特化: nullptr" << endl;
}
void add(const T1& a, const T2& b) {
cout << "指针偏特化add: " << (a + b) << endl;
}
};
int main() {
// 正确实例化:所有模板参数均为类型(非变量),数量严格匹配3个
int val = 42;
Printer<int*, int, int> p4; // 匹配指针偏特化
p4.print(&val); // 指针偏特化: 值=42
p4.add(10, 20); // 指针偏特化add: 30
return 0;
}
大家可以自己实践一下,比如自己去实现一个比大小,然后再看能不能用比大小来排序之类的。
模板分离编译(了解)
什么是分离编译
字面意思:将模版的声明与定义分开实现,可以确保代码的私密性。
严格来说:一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。
具体的原理大家得需要学习《编译原理》才能懂,具体实现就不在此赘述。
解决方法
- 将声明和定义放到一个文件 "xxx.hpp" 里面或者xxx.h其实也是可以的。推荐使用这种。
- 模板定义的位置显式实例化。这种方法不实用,不推荐使用。
好啦,本篇博客到此就结束啦,下节我们会开启stl之路啦,喜欢文章的小伙伴可以点点赞,关注彩妙,获取更多优质好文吧~

更多推荐

所有评论(0)