【C++】玩转模板:进阶之路

✨ 坚持用 清晰易懂的图解 + 代码语言, 让每个知识点都 简单直观 !

🚀 个人主页不呆头 · CSDN

🌱 代码仓库不呆头 · Gitee

📌 专栏系列

💬 座右铭 : "不患无位,患所以立。"


【C++】玩转模板:进阶之路

  • 摘要
  • 目录
    • 一、非类型模板参数
      • [1. 认识](#1. 认识)
      • [2. 使用](#2. 使用)
      • [3. 和类类型参数的对比](#3. 和类类型参数的对比)
    • 二、arry容器
      • [1. 认识](#1. 认识)
      • [2. 使用](#2. 使用)
      • [3. 和传统c语言数组的对比](#3. 和传统c语言数组的对比)
    • 三、模板特化
      • [1. 概念](#1. 概念)
      • [2. 函数模板特化](#2. 函数模板特化)
      • [3. 类模板特化](#3. 类模板特化)
        • [3.1 全特化](#3.1 全特化)
        • [3.2 偏特化](#3.2 偏特化)
          • [3.2.1 部分特化](#3.2.1 部分特化)
          • [3.2.2 参数的进一步限制](#3.2.2 参数的进一步限制)
        • [3.3 应用实例](#3.3 应用实例)
    • 四、模板的分离编译
      • [1. 什么是分离编译](#1. 什么是分离编译)
      • [2. 模板的声明和定义为什么不能分离](#2. 模板的声明和定义为什么不能分离)
      • [3. 模板不分离编译解决办法](#3. 模板不分离编译解决办法)
    • 五、总结

摘要

本文围绕 C++ 模板的进阶用法展开,从 非类型模板参数 入手,结合 array 容器对比传统 C 数组,深入分析了 类模板的全特化与偏特化 ,并通过实例展示如何对不同类型进行特化。随后介绍了 模板的分离编译问题,解析了为什么模板声明与定义分离会导致链接错误,以及如何通过显式实例化或头文件内定义来解决。文章配合代码与图解,力求让读者清晰理解模板的底层机制和实际应用。


目录

一、非类型模板参数

1. 认识

模板参数分为类类型形参非类型形参

  • 类类型模板参数:出现在模板参数列表中,跟在class或者typename之类的参数类型名称。
  • 非类型模板参数:就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。

2. 使用

例如,我们要实现一个静态数组的类,就需要用到非类型模板参数。

cpp 复制代码
template<class T, size_t N> //N:非类型模板参数
class StaticArray
{
public:
	size_t arraysize()
	{
		return N;
	}
private:
	T _array[N]; //利用非类型模板参数指定静态数组的大小
};

int main()
{
	StaticArray<int, 10> a1; //定义一个大小为10的静态数组
	cout << a1.arraysize() << endl; //10
	StaticArray<int, 100> a2; //定义一个大小为100的静态数组
	cout << a2.arraysize() << endl; //100
	return 0;
}

3. 和类类型参数的对比

特点 类型模板参数 (Type Template Parameter) 非类型模板参数 (Non-type Template Parameter)
参数形式 类型(如 int, double, std::string 编译期常量值(如整数、枚举、指针、引用、C++20起支持浮点和字面量类)
传参方式 Array<int> Array<int, 10>
常见用途 泛型编程、容器、算法 固定大小容器、编译期策略选择、哈希参数等
编译期要求 只要类型合法即可 必须是编译期常量
C++17 支持范围 所有类型 整数、枚举、指针/引用常量
C++20 新增支持 - 浮点数、字面量类类型(满足 constexpr
举例 template <typename T> class Vec {} template <typename T, int N> class Array {}

二、arry容器

1. 认识


我们可以看出arry容器也使用的非类型模板参数来管理数组的固定大小。


2. 使用

cpp 复制代码
#include<iostream>
#include<array>
using namespace std;

int main()
{
	//array容器的使用
	array<int, 8> dh; //定义一个大小为8的静态数组
	dh[0] = 6;        //第一个数据初始化为6
	dh[7] = 99;       //最后一个数据初始化为99

	for (auto e : dh) //输出dh数组
	{
		cout << e << ' ';
	}
	cout << endl;

	return 0;
}

我们发现对未进行赋值的数据并没有对其进行初始化,反而是给出的随机值,那他和我们c语言中的传统数组有什么不同呢?又有什么优势?


3. 和传统c语言数组的对比

cpp 复制代码
#include<iostream>
#include<array>
using namespace std;

int main()
{
	//array容器的使用
	array<int, 8> dh; //定义一个大小为8的静态数组
	dh[0] = 6;        //第一个数据初始化为6
	dh[7] = 99;       //最后一个数据初始化为99

	for (auto e : dh) //输出dh数组
	{
		cout << e << ' ';
	}
	cout << endl;

	//c语言传统数组
	int arr[8];
	arr[0] = 6;
	arr[7] = 99;
	for (auto e : arr) //输出dh数组
	{
		cout << e << ' ';
	}
	cout << endl;

	return 0;
}

同样的,都没有进行初始化,而且c语言中的数组也支持array中的大部分数组,其实其中的不同就是array容器对越界的检查更加的严格,可以检查出越界读和越界写。

看下面的两个错误写法:

但是c语言中也可以检查出部分越界写


三、模板特化

1. 概念

我们通过一个代码的例子来理解模板的特化

cpp 复制代码
#include<iostream>
using namespace std;

// 函数模板 -- 参数匹配
template<class T>
bool IsEqual(T x, T y) //判断是否相等
{
	return x == y;
}

int main()
{
	cout << IsEqual(1, 1) << endl; //1
	cout << IsEqual(1.1, 2.2) << endl; //0
	
	char a1[] = "2021bsdt";
	char a2[] = "2021bsdt";
	cout << IsEqual(a1, a2) << endl; //0

	return 0;
}

判断结果是这两个字符串不相等,这很好理解,因为我们希望的是该函数能够判断两个字符串的内容是否相等 ,而该函数实际上判断是确实这两个字符串所存储的地址是否相同 ,这是两个存在于栈区的字符串,其地址显然是不同的。类似于上述实例,使用模板可以实现一些与类型无关的代码,但对于一些特殊的类型可能会得到一些错误的结果,此时就需要对模板进行特化,即在原模板的基础上,针对特殊类型进行特殊化的实现方式


2. 函数模板特化

函数模板的特化步骤:

  1. 必须要先有一个基础的函数模板
  2. 关键字template后面接一对空的尖括号<>
  3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型
  4. 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误

我们知道当传入的类型是char时,应该依次比较各个字符的ASCII码值进而判断两个字符串是否相等,或是直接调用strcmp函数进行字符串比较,那么此时我们就可以对char类型进行特殊化的实现。

cpp 复制代码
#include<iostream>
using namespace std;

//特化步骤一:基础的函数模板
template<class T>
bool IsEqual(T x, T y)
{
	return x == y;
}

//对于char*类型的特化
//特化步骤二:关键字template后面接一对空的尖括号<>
template<>
//特化步骤三:函数名后跟一对尖括号,尖括号中指定需要特化的类型
//特化步骤四:函数形参表必须要和模板函数的基础参数类型完全相同,否则不同的编译器可能会报一些奇怪的错误。
bool IsEqual<char*>(char* x, char* y)
{
	return strcmp(x, y) == 0;
}

int main()
{
	cout << IsEqual(1, 1) << endl; //1
	cout << IsEqual(1.1, 2.2) << endl; //0
	
	char a1[] = "2021bsdt";
	char a2[] = "2021bsdt";
	cout << IsEqual(a1, a2) << endl; //0

	return 0;
}
  1. 通常来讲,当遇到函数模板需要进行特化的时候,我们一般直接进行函数重载,函数重载后,编译器在进行调用的时候会优先调用与自身类型最匹配的或者现成已经实例化的函数
cpp 复制代码
//特化步骤一:基础的函数模板
template<class T>
bool IsEqual(T x, T y)
{
	return x == y;
}

bool IsEqual(char* x, char* y)
{
	return strcmp(x, y) == 0;
}

3. 类模板特化

3.1 全特化

全特化是将模板参数列表中的全部参数都确定化

cpp 复制代码
// 演示:类模板全特化
#include <iostream>
using namespace std;

// 通用类模板定义
template<typename T1, typename T2>
class dh
{
public:
    // 构造函数:名字必须和类名相同(C++语法规定)
    // 在创建对象时自动调用,用于初始化或打印提示
    dh()
    {
        cout << "dh<T1,T2>" << endl;  // 输出说明调用的是通用模板
    }
private:
    T1 _d;  // 成员变量,类型为 T1
    T2 _h;  // 成员变量,类型为 T2
};

// 类模板的全特化版本
// 针对 <double,double> 这种情况,提供单独的实现
template<>
class dh<double, double>
{
public:
    // 构造函数:同样名字必须与类名一致
    dh()
    {
        cout << "dh<double,double>" << endl;  // 输出说明调用的是特化版本
    }
private:
    double _d;
    double _h;
};

int main()
{
    // 使用通用类模板实例化对象
    dh<int, int> a;  
    // 调用 dh<int,int> 的构造函数
    // 输出:dh<T1,T2>

    // 使用特化类模板实例化对象
    dh<double, double> a1;  
    // 调用 dh<double,double> 的构造函数
    // 输出:dh<double,double>

    return 0;
}

当我们实例化一个对象时,编译器会自动调用其默认构造函数,我们若是在构造函数当中打印适当的提示信息,那么当我们实例化对象后,通过观察控制台上打印的结果,即可确定实例化该对象时调用的是不是我们自己特化的类模板了。


3.2 偏特化

偏特化:任何针对模板参数进行进一步的条件限制设计的特化版本。那么以下面的类模板为基础类模板进行设计

cpp 复制代码
//偏特化
template<typename T1,typename T2>
class dh
{
public:
//构造函数
	dh()
	{
		cout << "dh<T1,T2>" << endl;
	}
private:
	T1 _d;
	T2 _h;
};

争对以上的模板,我们可以偏特化成两种模式,如下。


3.2.1 部分特化

将模板参数列表中的部分参数进行特化

cpp 复制代码
//部分偏特化
template<typename T2>
class dh<int,T2>
{
public:
	dh()
	{
		cout << "dh<int,T2>" << endl;
	}
private:
	int _d;
	T2 _h;
};

3.2.2 参数的进一步限制

可以针对模板参数更进一步进行条件限制设计出来的特化版本,同时我们进行设计的特化的类模板中的成员没有必要追求和原类模板的成员一样,应该具体根据实际的应用场景去进行设计成员

cpp 复制代码
//针对指针的进一步限制
template<typename T1,typename T2>
class dh<T1*, T2*>
{
public:
	dh()
	{
		cout << "dh<T1*,T2*>" << endl;
	}
private:
	T1 _d;
	T2 _h;
};

//针对引用的进一步限制
template<typename T1, typename T2>
class dh<T1&, T2&>
{
public:
	dh()
	{
		cout << "dh<T1&,T2&>" << endl;
	}
private:
	T1 _d;
	T2 _h;
};

3.3 应用实例
cpp 复制代码
//部分偏特化
template<typename T1, typename T2>
class dh
{
public:
	dh()
	{
		cout << "dh<T1,T2>" << endl;
	}
private:
	T1 _d;
	T2 _h;
};

template<typename T2>
class dh<int,T2>
{
public:
	dh()
	{
		cout << "dh<int,T2>" << endl;
	}
private:
	int _d;
	T2 _h;
};

//针对指针的进一步限制
template<typename T1,typename T2>
class dh<T1*, T2*>
{
public:
	dh()
	{
		cout << "dh<T1*,T2*>" << endl;
	}
private:
	T1 _d;
	T2 _h;
};

//针对引用的进一步限制
template<typename T1, typename T2>
class dh<T1&, T2&>
{
public:
	dh()
	{
		cout << "dh<T1&,T2&>" << endl;
	}
private:
	T1 _d;
	T2 _h;
};

int main()
{
	//dh<int, int> a;
	//dh<double, double> a1;
	dh<double, int> a;
	dh<int, double> a1;

	dh<int*, int*> a2;
	dh<int&, int&> a3;

	return 0;
}

四、模板的分离编译

1. 什么是分离编译

什么是分离编译:

一个程序(项目)由多个源文件共同实现,而每个源文件单独编译形成目标文件,最后将所有的目标文件链接起来形成单一的可执行文件的过程称为分离编译模式

2. 模板的声明和定义为什么不能分离

cpp 复制代码
//stack.h
#pragma once
#include <deque>
namespace dh
{
	template<typename T, typename Container = deque<T>>
	class stack
	{
	public:
		void push(const T& val);

		T& top()
		{
			return _con.back();
		}

	private:
		Container _con;
	};
}
cpp 复制代码
//stack.c
using namespace std;

#include "Stack.h"

namespace dh
{
	template<typename T, typename Container>
	void stack<T, Container>::push(const T& val)
	{
		_con.push_back(val);
	}
}
cpp 复制代码
//test.c
#include <iostream>
using namespace std;
#include "Stack.h"

int main()
{
	dh::stack<int> st;

	st.push(1);
	st.top();

	return 0;
}

在 C++ 中,模板的实例化依赖于具体的类型参数。

  • 如果模板的定义不在头文件里,而是放在 .cpp 文件中,那么在别的 .cpp 中使用这个模板时,编译器只看到了声明,却找不到定义,导致无法实例化出具体函数的实现。
  • test.cpp 里,stack<int> 确定了类型,所以编译器尝试实例化。像 top 这种在头文件里有定义的函数就能成功实例化;但 push 只有声明,没有定义,实例化失败,缺少地址。
  • Stack.cpp 里虽然有 push 的定义,但因为没有遇到 stack<int> 这种具体类型,编译器不会生成对应的实例化代码,导致符号表里也没有函数实体。
  • 链接阶段,push 找不到对应的实现,就会报链接错误。

👉 所以模板代码一般要写在头文件里,保证在需要实例化的地方编译器能同时看到声明和定义;如果非要把定义放在 .cpp 里,就需要显示实例化(template class stack<int>;)来告诉编译器生成对应的函数实现。


3. 模板不分离编译解决办法

声明和定义分离编译之后将声明和定义放在同一个源文件中

cpp 复制代码
//stack.h
#pragma once
#include <deque>
namespace dh
{
	template<typename T, typename Container = deque<T>>
	class stack
	{
	public:
		void push(const T& val);

		T& top()
		{
			return _con.back();
		}

	private:
		Container _con;
	};

	template<typename T, typename Container>
	void stack<T, Container>::push(const T& val)
	{
		_con.push_back(val);
	}
}
cpp 复制代码
//test.c
#include <iostream>
#include "Stack.h"
using namespace std;

int main()
{
	dh::stack<int> st;

	st.push(1);
	st.top();

	return 0;
}

当模板的定义写在头文件里时,包含头文件的 .cpp 文件在编译阶段就能同时看到声明和定义。

  • test.cpp 中使用 stack<int> 时,模板参数 T 被确定为 int,编译器会立即实例化 stack<int>
  • 因为 pushtop 都在头文件中有完整的定义,所以这两个函数也会被实例化,生成对应的函数实体。
  • 汇编时为它们分配地址,目标文件 test.o 的符号表里就包含了 pushtop 的地址。
  • 链接时这些符号表合并,函数实现都已存在,不会再出现找不到地址的情况,因此程序能正常调用 pushtop

五、总结

通过本文的学习,我们可以系统掌握 C++ 模板的关键知识点:

  1. 非类型模板参数 ------ 让编译期常量参与模板实例化,常用于固定大小容器与编译期策略选择。
  2. 类模板特化 ------ 对特定类型提供专门实现,解决通用模板在特殊场景下的局限。
  3. 模板分离编译问题 ------ 由于模板实例化依赖具体类型,声明与定义分离时容易导致链接错误,必须放在头文件或显式实例化。

👉 模板的核心价值在于 泛型编程 + 编译期计算,它不仅提升了代码复用性,也让程序在保持灵活性的同时具备更高性能。理解这些机制,能帮助我们在写库、写框架时更好地驾驭 C++ 的高级特性。


不是呆头将一直坚持用清晰易懂的图解 + 代码语言,让每个知识点变得简单!

👁️ 【关注】 看一个非典型程序员如何用野路子解决正经问题

👍 【点赞】 给"不写八股文"的技术分享一点鼓励

🔖 【收藏】 把这些"奇怪但有用"的代码技巧打包带走

💬 【评论】 来聊聊------你遇到过最"呆头"的 Bug 是啥?

🗳️ 【投票】 您的投票是支持我前行的动力

技术没有标准答案,让我们一起用最有趣的方式,写出最靠谱的代码! 🎮💻

相关推荐
夜晚中的人海2 小时前
【C++】异常介绍
android·java·c++
m0_552200823 小时前
《UE5_C++多人TPS完整教程》学习笔记60 ——《P61 开火蒙太奇(Fire Montage)》
c++·游戏·ue5
charlie1145141913 小时前
精读C++20设计模式——行为型设计模式:迭代器模式
c++·学习·设计模式·迭代器模式·c++20
Le1Yu3 小时前
2025-9-28学习笔记
java·笔记·学习
小欣加油3 小时前
leetcode 1863 找出所有子集的异或总和再求和
c++·算法·leetcode·职场和发展·深度优先
C++chaofan3 小时前
项目中为AI添加对话记忆
java·数据结构·人工智能·redis·缓存·个人开发·caffeine
老华带你飞3 小时前
机电公司管理小程序|基于微信小程序的机电公司管理小程序设计与实现(源码+数据库+文档)
java·数据库·vue.js·spring boot·微信小程序·小程序·机电公司管理小程序
拾忆,想起4 小时前
AMQP协议深度解析:消息队列背后的通信魔法
java·开发语言·spring boot·后端·spring cloud
PH = 74 小时前
Spring Ai Alibaba开发指南
java·后端·spring