2、模板
约 4871 字大约 16 分钟
2025-06-22
泛型编程 模板建立通用的模具,大大提高复用性
两种模板机制
函数模板和类模板 template< typename T> 声明后面跟着写函数就是函数模板,跟着写类就是类模板
函数模板
建立一个通用函数,其返回值类型和形参类型可以不具体制定,用一个虚拟类型代表 声明好后紧跟着定义的函数就是函数模板,一个模板只能用于一个函数
template<typename T>
//反转ab
void myswap(T &a, T &b){
T temp=a;
a=b;
b=temp;
}
void pri(T &a,T &b){//error
}语法:
声明或定义 template< tapename T> template< class T> tapename和class区别很小,当T包含子类的类时,用class编译器会误认为是对象声明
告诉编译器T是一个通用的数据类型
template<typename T>
//反转ab
void myswap(T &a, T &b){
T temp=a;
a=b;
b=temp;
}
int main(){
int a=1;
int b=2;
double a1=1.1;
double b1=2.2;
cout<<a<<b<<endl;
myswap<int>(a,b);
cout<<a<<b<<endl;
cout<<a1<<b1<<endl;
myswap<double>(a1,b1);
cout<<a1<<b1<<endl;
return 0;
}myswap< int>(a,b)中的< int>是告诉编译器T的数据类型
模板必须确定出T的数据类型才能使用
template<typename T>
void func(){
cout<<"hello world"<<endl;
}
int main(){
func(); //error
return 0;
}可把函数模板声明注释也可以显示指定类型进行调用 不用传参指定任意数据类型都行
func<int>();
func<char>();使用方法
自动类型推导
int a=1;
int b=2;
myswap(a,b);不指明T的数据类型,让编译器自己推导出T的数据类型,但要求传入的参数数据类型相同
显示指定类型
int a=1;
int b=2;
myswap<int>(a,b);普通函数和函数模板的区别
普通函数调用时可发生自动类型转换(隐式类型转换) 函数模板调用时若使用自动推导类型就不会发生隐式类型转换,若使用显示指定类型就可以发生隐式类型转换
//普通函数
int add(int a,int b){
return a+b;
}
//函数模板
template<typename T>
T add1(T a,T b){
return a+b;
}
int main(){
int a=1;
int b=2;
char c='a';//a对应的ASCII码为97
cout<<add(a,b)<<endl;//3
cout<<add(a,c)<<endl;//98
//自动类型推导
cout<<add1(a,b)<<endl;//3
//cout<<add1(a,c)<<endl;//error //显式指定类型
cout<<add1<int>(a,c)<<endl;//98
return 0;
}普通函数与函数模板调用规则
1、如果函数模板和普通函数都可以实现优先调用普通函数
//普通函数
void print(int a,int b){
cout<<"调用普通函数"<<endl;
}
//函数模板
template<typename T>
void print(T a,T b){
cout<<"调用函数模板"<<endl;
}
int main(){
int a=1;
int b=2;
print(a,b);//调用普通函数
return 0;
}2、可以通过空模板参数列表来强制调用函数模板
报错
//普通函数
void print(int a,int b);
//函数模板
template<typename T>
void print(T a,T b){
cout<<"调用函数模板"<<endl;
}
int main(){
int a=1;
int b=2;
print(a,b);//调用普通函数
return 0;
}使用空模板参数列表
print<>(a,b);//调用函数模板3、函数模板也可以发生重载
//普通函数
void print(int a,int b){
cout<<"调用普通函数"<<endl;
}
//函数模板
template<typename T>
void print(T a,T b){
cout<<"调用函数模板"<<endl;
}
template<typename T>
void print(T a,T b,T c){
cout<<"函数模板重载"<<endl;
}
int main(){
int a=1;
int b=2;
print(a,b,3);//函数模板重载
return 0;
}4、如果函数模板可以产生更好的匹配有限调用函数模板
//普通函数
void print(int a,int b){
cout<<"调用普通函数"<<endl;
}
//函数模板
template<typename T>
void print(T a,T b){
cout<<"调用函数模板"<<endl;
}
int main(){
char c1='a';
char c2='b';
print(c1,c2);//调用函数模板
return 0;
}如果调用普通函数需要把char转成int,调用函数模板只需要推导出T就可以调用,函数模板更好匹配,因此调用函数模板
模板的局限性
模板不是万能的,有些特定的数据类型需要用具体化的方法做特殊实现
对于自定义的数据类型函数模板就不会很好的运行 可以使用运算符重载等方法解决 可以利用具体化自定义类型的版本实现,具体优化调用
class Person{
public:
string m_name;
int m_age;
Person(string name,int age){
m_name=name;
m_age=age;
}
};
template<typename T>
bool compare(T a,T b){
if (a==b){
return true;
}
else{
return false;
}
}
template<>bool compare(Person a,Person b){
if (a.m_name==b.m_name && a.m_age==b.m_age){
return true;
}
else{
return false;
}
}
int main(){
int a=1;
int b=1;
int c=2;
Person p1("tom",18);
Person p2("tom",18);
Person p3("jerry",18);
cout<<compare(a,b)<<endl;//1
cout<<compare(a,c)<<endl;//0
cout<<compare(p1,p2)<<endl;//1
cout<<compare(p1,p3)<<endl;//0
return 0;
}类模板
类模板中需要几个参数可定义几个通用数据类型
template<class Nametype,class Agetype>
class Person{
public:
Nametype myname;
Agetype myage;
Person(Nametype name,Agetype age){
myname=name;
myage=age;
}
void showPerson(){
cout<<myname<<"\t"<<myage<<endl;
}
};类模板没有自动推导的使用方式
Person<string,int>p1("tom",18);
//Person p2("jerry",16);//error类模板在模板参数列表中可以有默认参数
template<class Nametype,class Agetype=int>
Person<string>p1("tom",18);指定Agetype的默认参数类型后调用类模板不指定有默认参数类型的通用类型 也能正常调用
类模板和普通类的区别
普通类中的成员函数一开始就可以创建 类模板中的成员函数调用时才创建
class Person1{
public:
void showPerson1(){
cout<<"Person1 is show"<<endl;
}
};
class Person2{
public:
void showPerson2(){
cout<<"Person2 is show"<<endl;
}
};
template<class T>
class Myclass{
public:
T obj;
void func1(){
obj.showPerson1();
}
void func2(){
obj.showPerson2();
}
};类模板不调用就无法确定T的参数类型,因此编译可以通过
Myclass<Person1>m;
m.func1();//Person1 is show
//m.func2();//error因为指定T的数据类型是Person1所以可以调用func1不能调用func2
类模板做函数参数
类模板实例化的对象做函数传入的参数
template<class T1,class T2>
class Person{
public:
T1 myname;
T2 myage;
Person(T1 name,T2 age){
myname=name;
myage=age;
}
void showPerson(){
cout<<myname<<"\t"<<myage<<endl;
}
};
int main(){
Person<string,int> p("tom",18);
print_person1(p);
print_person2(p);
print_person3(p);
return 0;
}三种方式
1、指定传入类型 直接显示对象的数据类型
//指定传入类型
void print_person1(Person<string,int>&p){
p.showPerson();
}2、参数模板化 将对象中的参数变为模板进行传递
//参数模板化
template<typename T1,typename T2>
void print_person2(Person<T1,T2>&p){
p.showPerson();
//查看推导出T1和T2的数据类型
cout<<typeid(T1).name()<<endl;
cout<<typeid(T2).name()<<endl;
}3、整个类模板化 将这个对象类型模板化进行传递
//整个类模板化
template<typename T>
void print_person3(T &p){
p.showPerson();
cout<<typeid(T).name()<<endl;
}类模板与继承
当子类继承的父类是一个类模板时,子类在声明的时候要指明父类模板中通用类型的数据类型
template<class T>
class Base{
T m;
};
class Son:public Base<int>{};如果不指定,编译器无法给子类分配内存 如果想灵活指定出父类中通用类型的数据类型子类也需要变成类模板
template<class T>
class Base{
T m;
};
template<class T1,class T2>
class Son:public Base<T2>{
T1 obj;
};
int main(){
Son<int,char>s;
return 0;
}模板子类继承普通父类
必须要在子类中构造A类否则会报错
//普通类A
class A{
public:
int a;
A(int a):a(a){cout << "A构造"<<endl;}
void func1() {cout << "func1()"<<a<< endl;}
};
//模板类B
template<class T1, class T2>
class B:public A{
public:
T1 x;
T2 y;
B(const T1 x, const T2 y,int a):A(a),x(x),y(y) {cout << "B构造"<<endl;}
void func2() const {cout << "func2()"<<x<<y<<endl;}
};
int main() {
B<int,string> b(6,"Aeolian",6);
}类模板成员函数类外实现
template<class T1,class T2>
class Person {
public:
T1 myname;
T2 myage;
Person(T1 name, T2 age);
void showPerson();
};
template<class T1,class T2>
Person<T1,T2>::Person(T1 name, T2 age){
myname=name;
myage=age;
}
template<class T1,class T2>
void Person<T1,T2>::showPerson(){
cout<<myname<<"\t"<<myage<<endl;
}
int main(){
Person<string,int>p("tom",18);
p.showPerson();
return 0;
}类模板分文件编写
类模板成员函数是在调用时创建的,导致分文件编写时链接不到
解决方法
此时person.h里的代码为
#pragma once
#include <iostream>
using namespace std;
template<class T1,class T2>
class Person {
public:
T1 myname;
T2 myage;
Person(T1 name, T2 age){
myname=name;
myage=age;
}
void showPerson(){
cout<<myname<<"\t"<<myage<<endl;
}
};1、直接包含.cpp文件
此时链接到person.h的源文件中代码为
#include <iostream>
using namespace std;
#include "person.h"
int main(){
Person<string,int>p("tom",18);//error
p.showPerson();//error
return 0;
}在clion中可正常运行,在有些编译器中会报错
此时只需要把person.h改为person.cpp即可
2、将声明和实现写到同一个文件中,并改后缀名为.hpp,.hpp是约定的名称不是强制
person.hpp文件中的代码和person.h一样 此时只需要把person.h改为person.hpp即可
类模板与友元
全局函数类内实现
直接在类内声明友元即可
template<class T1,class T2>
class Person {
//全局函数,类内实现
friend void printPerson(Person<T1,T2>p){
cout<<p.myname<<"\t"<<p.myage<<endl;
}
public:
Person(T1 name, T2 age){
myname=name;
myage=age;
}
private:
T1 myname;
T2 myage;
};全局函数类外实现
需要让编译器提前知道全局函数的存在
template<class T1,class T2>
class Person;
//把全局函数放上面让编译器在运行到类内声明的全局函数时知道这个全局函数的存在
//但全局函数中用到了Person类,因此要在上面也声明一下Person类
template<class T1,class T2>
void printPerson(Person<T1,T2>p){
cout<<p.myname<<"\t"<<p.myage<<endl;
}
template<class T1,class T2>
class Person {
//全局函数,类外实现
//加空模板参数列表
friend void printPerson<>(Person<T1,T2>p);
public:
Person(T1 name, T2 age){
myname=name;
myage=age;
}
private:
T1 myname;
T2 myage;
};
int main(){
Person<string,int>p("tom",18);//error
printPerson(p);
return 0;
}模板具体化、实例化
模板具体化
函数模板具体化
为某一特定的类型重写函数模板,声明的含义是使用独立的,专门的函数定义显示地为 特定类型生成函数定义。 它能够处理模板函数所不能处理的特殊情况。显式具体化显式具体化也是基于函数模板的,只不过在函数模板的基础上,添加一个专门针对特定类型的、实现方式不同的具体化函数。 编译器只在要调用函数的时候才使用到函数,如果不使用显示实例化,每次调用函数时,模板都会消耗性能去推导使用的是哪个类型的函数,增加了程序运行时的负担;使用了显示实例化,则在编译时就已经处理了函数选择。
在编写函数模板具体化的函数时其定义必须放在同名函数模板的后面,不然就没有对应名称的模板函数进行匹配 函数模板具体化的应用 模板具体化使用模板原型参与重载,优先级低于非模板函数。因此写库的时候可以用具体化,这样用户如果不想用你的具体化,他可以自己写一个非模板函数来取代你的具体化。
模板函数也可以重载,其操作与常规函数一致。 常规模板,具体化模板,非模板函数的优先调用顺序: 非模板函数(普通函数)> 具体化模板函数 > 常规模板 如果希望使用函数模板可以使用空模板参数强制使用函数模板
using std::cout;
using std::endl;
struct job {
char name[20];
int salary;
};
//函数模板实现
template<class T> void swap(T &a, T &b) {
cout<<"普通模板函数"<<endl;
T temp;
temp = a;
a = b;
b = temp;
}
void showJob(const job &a) {
cout << " " << a.name << " = " << a.salary;
}
int main() {
int a = 4;
int b = 5;
cout << "Before swap a = " << a << " b=" << b << endl;
swap(a, b);
cout << "After swap a = " << a << " b=" << b << endl;
job jobA = {"coder", 10000};
job jobB = {"manager", 1000};
cout << "Before swap";
showJob(jobA);
showJob(jobB);
cout << endl;
swap(jobA, jobB);
cout << "After swap";
showJob(jobA);
showJob(jobB);
cout << endl;
};如果job类只想互换salary而不互换其他成员变量值就有以下解决方法
1. 显式具体化
显式具体化也是基于函数模板的,只不过在函数模板的基础上,添加一个专门针对特定类型的、实现方式不同的具体化函数。
语法:
template<>返回值类型 函数名<参数类型>(形参列表) {函数体}
//或
template<>返回值类型 函数名(形参列表) {函数体}实现:
template<>void swap<job>(job &a, job &b) {
cout<<"函数模板显式具体化"<<endl;
int temp;
temp = a.salary;
a.salary = b.salary;
b.salary = temp;
}2. 定义同名常规函数
实现:
void swap(job &a, job &b){
cout<<"同名常规函数"<<endl;
int temp;
temp = a.salary;
a.salary = b.salary;
b.salary = temp;
}模板函数分文件编写
函数模板只是函数的描述,没有实体,创建函数模板的代码放在头文件中
函数模板具体化有实体,编译的原理和普通函数一样,所以声明放在头文件中,定义放在源文件中
类模板具体化
类模板具体化和函数模板具体化一样也都是要把具体化版本放在模板之后 类模板具体化分为部分具体化和全部具体化,函数模板具体化没有这个分类
普通类模板
// 类模板
template<class T1, class T2>
class A {
public:
T1 x;
T2 y;
A(const T1 x, const T2 y):x(x), y(y){ cout<<"类模板构造"<<endl;}
void show() const{cout<<"类模板:"<<x<<y<<endl;}
};类模板部分具体化
// 类模板部分显示具体化
template<class T1>
class A<T1, string>{
public:
T1 x;
string y;
A(const T1 x, const string y) :x(x),y(y){cout <<"部分具体化构造"<<endl;}
void show() const{cout <<"部分具体化:"<< x<< y<< endl;}
};类模板全部具体化
// 类模板完全具体化
template<>
class A<int, string> {
public:
int x;
string y;
A(const int x, const string y):x(x), y(y){cout<<"完全具体化构造"<<endl;}
void show() const{ cout <<"完全具体化:"<<x<< y<< endl;}
};main函数
int main() {
A<int,string> a(6,"Aeolian");
a.show();
}类模板函数调用顺序: 类模板全部具体化>类模板部分具体化>普通类模板 具体化程度高的类优先于具体化程度低的类,具体化程度低的类优先于没有具体化的类
模板实例化
类模板本身不是类型、对象或任何其他实体。仅包含模板定义的源文件不会生成任何代码。为了出现任何代码,必须实例化模板:必须提供模板参数,以便编译器可以生成实际的类(或函数,来自函数模板)。
类模板必须实例化才能作为一个类来声明和定义类对象,类模板实例化成为模板类,同一个类模板不同的实例之间相互独立。
隐式实例化
发生隐式实例化的条件
- 当代码使用类模板定义对象时,需要在上下文中引用完全定义类型。(例如,当构造此类型的对象时,而不是在构造指向此类型的指针时。 )
- 当类型的完整性影响代码时,并且该特定类型尚未显式实例化时,就会发生隐式实例化。 此外除非类模板成员在程序中使用,否则它不会被实例化,也不需要定义
template<typename T>
class MyClass {
public:
MyClass(T t) {}
string getType() const {return typeid(T).name();}
};
template<typename T>
bool isSmaller(T fir, T sec) {return fir < sec;}
int main() {
cout << boolalpha;
vector vec{1, 2, 3, 4, 5}; // (1)
cout << "vec.size(): " << vec.size() << endl;
MyClass myClass(5); // (2)
cout << "myClass.getType(): " << myClass.getType() << endl;
cout << "isSmaller(5, 10): "<< isSmaller(5, 10) << endl; // (3)
cout << "isSmaller<double>(5.5f, 6.5): "<< isSmaller<double>(5.5f, 6.5) << endl; // (4)
}这个自动过程非常舒适,但也有一些缺点。
- 当你隐式实例化一个模板时,模板的定义通常在头文件中可见。也许,你不想公开这个定义。
- 当你需要一个特定的模板实参时,如果它在具体的翻译单元中不可用,编译器就会实例化。一个翻译单元是C预处理器处理后的源文件。通常情况下,链接器会删除所有多余的模板实例并保留一个。这是对时间和空间的浪费。
这两个问题都可以通过显式模板实例化来解决。
显式实例化
显式实例化定义的语法:template <template declaration>
显式实例化声明的语法:extern template <template declaration>
模板参数
非类型参数(表达式参数)
模板参数不一定非得是类型,它们还可以是普通的数值( Nontype Template Parameters) 类型参数用于指定一个类型,非类型参数用于指定一个量。 可以把错误放在编译期解决或者性能优化在编译期解决
非类型参数只能是以下类型:
- 整型常量
- 枚举
- 指向对象/函数/成员变量的指针
- 对象/函数的左值引用
- std::nullptr_t
- 浮点类型(C++20之后)
- String等类(C++20之后)
因此template< double n>是不合法的,但template< double * p>是合法的。 模板代码中不能修改非类型参数的值,也不能使用其地址,所以n++,&n等操作是不合法的。这也造成了模板类创建数组的一大缺点,数组创建后无法动态改变大小。
template<int n> //n为一个非类型参数,或称为表达式参数
void func(){
cout<<n<<endl;
}
int main() {
func<9>();
}类模板作用于类模板参数
template<class T, int len>//链表类模板。
class LinkList {
public:
T *mhead;//链表头结点
int mlen = len; //表长
void insert() {cout << "链表插入"<<endl;}
void ddelete() {cout << "链表删除"<<endl;}
void update() {cout << "链表更新"<<endl;}
};
template<class T, int len>
// 数组类模板。
class Array {
public:
T *mdata;// 数组指针
int mlen = len; //表长
void insert() {cout << "数组插入"<<endl;}
void ddelete() {cout << "数组删除"<<endl;}
void update() {cout << "数组更新"<<endl;}
};
//线性表模板类:tabletype-T1线性表类型,datatype-T2线性表的数据类型。
template< template<class, int >class T1 , class T2, int len>
class LinearList{
public:
T1<T2, len> mtable;// 创建线性表对象。
void insert() {mtable.insert();}// 线性表插入操作。
void ddelete() {mtable.ddelete();}// 线性表删除操作。
void update() {mtable.update();}// 线性表更新操作。
void oper(){
cout << "len=" <<mtable.mlen<<endl;
mtable.insert();
mtable.update();
} //按业务要求操作线性表。
};
int main() {
// 创建线性表对象,容器类型为链表,链表的数据类型为int,表长为20
LinearList<LinkList, int, 20> a;
a.insert();
a.ddelete();
a.update();
// 创建线性表对象,容器类型为数组,,数组的数据类型为string,表长为20
LinearList<Array, string,20> b;
b.insert();
b.ddelete();
b.update();
}类模板作用于函数参数
template<class Q>
void func(Array<Q,56> q){
cout<<"func"<<endl;
}普通的函数模板参数可以是未知类型,但是该未知类型的模板功能是受限的。如果将类模板作为函数模板的参数,就进一步扩大了函数模板的功能范围。
贡献者
版权所有
版权归属:wynnsimon
