首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >C++模板(初阶) 详解

C++模板(初阶) 详解

作者头像
君辣堡
发布2025-12-20 09:10:52
发布2025-12-20 09:10:52
1320
举报

这一篇我来简单介绍一下一个实用的东西——模板

模板是泛型编程的关键,泛型编程就是编写与类型无关的通用代码是代码复用的一种手段。模板是泛型编程的基础。

1.模板

举例子,我们要实现不同类型的交换函数

代码语言:javascript
复制
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;
}

如图,通过函数重载实现了double类型,char类型,int类型的交换函数。但这存在几个缺点

1.仅仅只是类型不同,导致代码复用率很低,要是再多加几个新类型的交换函数,还得再重写。

2.代码的可维护性低,一个出错可能导致所有重载都得修改

那么,能否告诉编译器一个模板,让编译器根据不同类型,利用模板生成对应类型的代码呢?

所以祖师爷就想到了这点,于是创造了模板。

模板又分为两个:一个是函数模板,一个是类模板

1.1函数模板

函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型,通过他的函数模板生产特定的函数。

函数模板的格式:

代码语言:javascript
复制
template<typename T>
void Swap( T& left, T& right)
{
T temp = left;
left = right;
right = temp;
}

如图,把参数类型都替换成T,然后在原来函数的头上加一行template<typename T>

也可以是多个参数:template<typename T1,typename T2,.... ,typename Tn>

typename T是会替换掉函数内的参数T,并且typename等效于class,也就是你能这么写:

template<class T1,typename T2..... >  class和template随便替换。

(小知识:为什么要用T代表类型?其实T是type的简写,然后类型都喜欢用大写,所以用T。多个参数就可以用T1,T2等来表达,如果没有具体意思一般就是用T,当然你要用X也行,随便。名字随便取的,但最好有代表意思,提高代码规范性。)

1.2函数模板的原理

​​​​需要注意,函数模板本身只是一个蓝图,是编译器生成对应函数的模具。

如图。编译器在编译阶段,会使用模板编译器需要根据传入的参数类型来推演生成对应类型的函数,比如用double类型使用函数模板时,编译器通过对实参的推演,将参数T确定为double类型,然后产生一份专门处理double类型的函数,对于别的也一样。

1.3函数模板的实例化

用不同类型的参数使用函数模板时,称为函数模板的实例化,实例化又分为:显式实例化    隐式实例化

1.3.1隐式实例化

隐式实例化:让编译器根据实参推模板参数的实际类型

代码语言:javascript
复制
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;
    Add(a1, a2);      //根据int类型推演
    Add(d1, d2);      //根据double类型推演
  
    return 0;
}

如图,会根据实参类型进行推演。 但必须类型统一,不然就会编译报错 ,如:Add(a1,d2)   

一个int , 一个double ,编译器不知道如何选择。此时有两种选择,一是强制转换,二是显式实例化。

强制转换:Add(a1,(int)d2),或者 Add((double)a1,d2)

1.3.2显式实例化

显式实例化:在函数名后的< >中指定模板参数的实际类型

代码语言:javascript
复制
int main()
{
    int a = 10;
    double b = 20.0;
    // 显式实例化
    Add<int>(a, b);
    return 0;
}

如图,这就是显式实例化,在函数名后面<>指明类型,图中是int,也可以是double  如果类型不匹配,编译器会尝试进行隐式类型转

,如果无法转换成功,编译器将会报错。  

1.3.3模板参数的匹配原则

第一部分:

一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数。

这句话可能比较难懂,首先模板函数(突出函数)就是由函数模板(突出模板)这个模板生成的函数非模板函数就是 不是由函数模

板生成的函数,自己实现的,真正的函数

举个例子帮助大家理解:

代码语言:javascript
复制
// 专门处理int的加法函数
int Add(int left, int right)
{
    return left + right;
}

// 通用加法函数
template<class T>
T Add(T left, T right)
{
    return left + right;
}

void Test()
{
    Add(1, 2);          // 与非模板函数匹配,编译器不需要特化
    Add<int>(1, 2);     // 调用编译器特化的Add版本
}

如图。第一个函数就是,非模板函数,专门写出来的,为int服务的函数,再下面一个就是通用加法函数,也就是函数模板

test中,Add(1,2) ,编译器会优先匹配 已存在的 非模板 int 加法函数,不进行函数模板实例化。根本原因在于:非模板函数本身就具有更高的匹配优先级

第二部分:

对于非模板函数 与 同名函数模板,如果其他条件相同,在调用时会优先调用非模板函数,不会使用函数模板进行实例化。

如果模板可以产生一个具有更好匹配的函数, 那么将选择模板。

同样我举个例子:

代码语言:javascript
复制
// 专门处理int的加法函数
int Add(int left, int right)
{
    return left + right;
}

// 通用加法函数
template<class T1, class T2>
T1 Add(T1 left, T2 right)
{
    return left + right;
}

void Test()
{
    Add(1, 2);     // 与非函数模板类型完全匹配,不需要函数模板实例化
    Add(1, 2.0);   // 模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的Add函数
}

如图。第一个是专门处理int的加法函数,第二个是函数模板,是两个不同参数的模板

test中,Add(1,2)  ,两个参数都是int,有直接匹配的非模板函数,所以不需要使用函数模板。  但是 Add(1,2.0),一个是int,一个

是double,用函数模板可以生成更匹配的版本(类型匹配,一个int,一个double),所以编译器会使用函数模板进行实例化。

小知识:模板函数不允许自动类型转换,但普通函数可以进行自动类型转换

1.4类模板

类模板与函数模板类似,我们直接展开讲,首先是类模板的定义格式

代码语言:javascript
复制
// 类模版
template<typename T>              //定义模板
class Stack
{
public:
Stack(size_t capacity = 4)
{
    _array = new T[capacity];     //改为开辟T类型的空间,表示存放T类型
    _capacity = capacity;
    _size = 0;
}

private:
    T* _array;                     //改为了T
    size_t _capacity;
    size_t _size;
};

(首先知道:模版不建议声明和定义分离到两个文件.h 和.cpp,会出现链接错误)

如图,这就是类模板的应用。大体上与函数模板相同,把需要更换的类型换成T类型,就行了。

但由于 传实参 利用构造函数创建 类对象 的时候,参数可能不传T类型(如图构造函数是传int,没传T,因为没传T,所以编译器不知

道你的类需要什么类型的T,编译器便无法利用模板推演),这样就无法创建对象。所以利用 类模板 实例化 模板类 的时候,

必须显式实例化:

代码语言:javascript
复制
int main()
{
    Stack<int> st1;        //显式实例化,存int
    Stack<double> st2;     //显式实例化,存double
    return 0;
}

为什么不用typedef int T  ,这样不是也能代替模板吗?

 其实不能代替T为int后,double怎么办?其他类型比如char又该怎么办? 因为类不能重载,所以将Stack类 typedef int T 后,就不能像图中实例化double类型了,除非拷贝一份stack类,然后改名stack1或者别的,重新创建一个类,这样代码重复率很高,不划算。        浓缩理解:typedef int T 是类型别名,只能固定一种类型(如 int)。

原本类的定义中,类名是类型,在 模板类 / 类模板 中就不是了。因为Stack<int> 和Stack<double> 本质上不是同一个类型。(因为存储的数据类型就不一样)

模板类 / 类模板 中,只有加上模板参数,才是类型。

在这里,Stack 本身不是类型,只有当它绑定具体模板参数(如 Stack<int>Stack<double>)后,才成为真正的 “类型”。

代码语言:javascript
复制
//类内声明定义不分离的Push函数:
template<class T>                        
void Push(const T& data)
{
    // 扩容
    _array[_size] = data;
    ++_size;
}
private:
    //参数               
};

这是从上面的类模板中摘下来的部分代码(合在一起太长了不方便观看)。如图,类内函数也需要声明模板 template<class T>  。

为什么已经对类class Stack整体声明过模板参数了,到里面的类函数也需要声明模板参数?

因为实际上 类模板 中的函数就是 函数模板 ,而函数模板就需要声明模板参数。     类都是类模板了,那类其中的成员函数当然也是函数模板。

1.5模板参数可以给缺省值

如标题,这是一个模板的语法,就是模板参数可以给缺省值。举例:

代码语言:javascript
复制
template<class T = int>
class A
{
private:
    T a1;
    T a2;  
};

int main()
{
    A<> aa1;        //全缺省构造 A模板类,不传模板参数默认int
    A<double> aa2;   //传模板参数了,不使用缺省值 
    return 0;
}

也可以是多个模板参数,多个缺省值

代码语言:javascript
复制
template<class T1 = int,class T2 = double>
class B
{
private:
    T1 b1;
    T2 b2;  
};

int main()
{
    B<> bb1;               //全缺省,默认第一个是int,第二个是double
    B<double> bb2;         //半缺省,T1 变成 double ,T2 维持缺省值
    B<double,int> bb3;     //T1变double   T2变int  ,不使用缺省值
    return 0;
}

和之前学的是一样的,缺省值只能从右往左给。

模板初阶到这里就讲完了,感谢大家支持~后面我会慢慢拓展模板进阶,以及模板的使用,请大家多多点赞!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-12-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.模板
    • 1.1函数模板
    • 1.2函数模板的原理
    • 1.3函数模板的实例化
      • 1.3.1隐式实例化
      • 1.3.2显式实例化
      • 1.3.3模板参数的匹配原则
    • 1.4类模板
    • 1.5模板参数可以给缺省值
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档