news 2026/10/5 2:31:28

C++ 模板初阶:函数模板与类模板详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++ 模板初阶:函数模板与类模板详解

C++ 模板初阶:函数模板与类模板详解

1. 泛型编程

在实际编程中,经常会遇到这样的情况:算法完全一样,只是处理的数据类型不同

例如实现交换函数:

voidSwap(int&left,int&right){inttemp=left;left=right;right=temp;}voidSwap(double&left,double&right){doubletemp=left;left=right;right=temp;}voidSwap(char&left,char&right){chartemp=left;left=right;right=temp;}

这几个函数的逻辑完全一样,唯一的区别就是数据类型不同

这种写法存在两个明显问题:

  • 代码重复:出现一种新的类型,就需要重新写一个函数
  • 维护成本高:大量重复代码容易出现修改不一致的问题

因此,C++提供了模板

泛型编程

泛型编程就是编写与具体类型无关的通用代码

它的核心思想是:

把类型作为参数,让同一套代码能够处理不同的数据类型

模板就是实现泛型编程的重要工具

可以把模板理解成一个“代码模具”:

模板 ↓ 填入具体类型 ↓ 生成对应类型的代码

例如:

Swap模板 ↓ 填入 int ↓ Swap(int&, int&) Swap模板 ↓ 填入 double ↓ Swap(double&, double&) Swap模板 ↓ 填入 char ↓ Swap(char&, char&)

这样,原本需要程序员重复编写的代码,可以交给编译器完成


2. 函数模板

2.1 函数模板的概念

函数模板可以理解为一个函数家族

它本身并不是一个真正用于执行的具体函数,而是一个生成具体函数的模板

例如:

template<classT>voidSwap(T&left,T&right){T temp=left;left=right;right=temp;}

这里的T就是一个模板参数

当我们使用:

inta=10;intb=20;Swap(a,b);

编译器可以根据a、b的类型推导:

T → int

于是可以生成一个针对int的Swap

如果:

doublea=1.1;doubleb=2.2;Swap(a,b);

那么:

T → double

从而生成针对double的版本


2.2 函数模板的基本格式

template<typenameT1,typenameT2,...,typenameTn>返回值类型 函数名(参数列表){// 函数体}

最常见的形式:

template<typenameT>voidSwap(T&left,T&right){T temp=left;left=right;right=temp;}

其中:

template<typenameT>

表示下面定义的是一个模板

T是模板参数


2.3typename和class

定义模板参数时:

template<typenameT>

也可以写成:

template<classT>

两者在这里含义相同

例如:

template<typenameT>voidSwap(T&a,T&b){T tmp=a;a=b;b=tmp;}

等价于:

template<classT>voidSwap(T&a,T&b){T tmp=a;a=b;b=tmp;}

但是不能写成:

template<structT>

模板类型参数这里使用的是typename或class


3. 函数模板的原理

理解这一部分非常重要

函数模板:

template<classT>TAdd(constT&left,constT&right){returnleft+right;}

它本身不是一个具体函数

可以把它看成一份“函数生成规则”

例如:

inta=10;intb=20;Add(a,b);

编译器看到实参以后,会推导:

T = int

相当于产生一个处理int的具体版本

如果:

doublea=10.0;doubleb=20.0;Add(a,b);

则:

T = double

产生处理double的具体版本

所以模板的核心思想就是:

程序员只负责写一份通用代码,编译器根据实际使用情况生成具体版本

这个过程发生在编译阶段


4. 函数模板的实例化

使用不同类型的参数调用函数模板时,就会发生函数模板实例化

函数模板实例化主要分为:

  • 隐式实例化
  • 显式实例化

4.1 隐式实例化

隐式实例化就是:

由编译器根据函数实参自动推导模板参数

例如:

template<classT>TAdd(constT&left,constT&right){returnleft+right;}

调用:

inta1=10;inta2=20;doubled1=10.0;doubled2=20.0;Add(a1,a2);Add(d1,d2);

第一次:

Add(a1,a2);

根据:

a1 → int a2 → int

推导:

T → int

第二次:

Add(d1,d2);

推导:

T → double

所以可以分别生成:

Add<int> Add<double>

4.2 一个容易出错的情况

例如:

inta=10;doubled=20.0;Add(a,d);

如果模板只有一个参数:

template<classT>TAdd(constT&left,constT&right){returnleft+right;}

那么编译器会遇到:

第一个参数: a → int 第二个参数: d → double

此时同一个T到底应该是什么?

T = int? T = double?

无法确定,因此无法正常推导

所以这种调用不能依靠这种形式的模板参数推导


4.3 解决方法一:强制类型转换

可以让两个参数变成相同类型:

Add(a,(int)d);

此时:

a → int (int)d → int

于是:

T → int

模板就可以实例化


4.4 解决方法二:显式实例化

也可以直接告诉编译器:

T 就是int

写法:

Add<int>(a,d);

这里的:

<int>

就是明确指定模板参数

编译器知道:

T = int

然后再处理:

double→int

如果能够进行转换,就可以完成调用;如果无法转换,则编译报错


5. 模板参数的匹配原则

函数模板和普通函数可以同时存在

例如:

intAdd(intleft,intright){returnleft+right;}template<classT>TAdd(T left,T right){returnleft+right;}

此时:

Add(1,2);

既可以匹配普通函数:

intAdd(int,int);

也可以让模板实例化出:

Add<int>(int,int);

但是在其他条件相同的情况下:

Add(1,2);

会优先匹配普通函数

也就是说:

普通函数和模板都能完全匹配时,优先使用普通函数

如果模板能够产生一个匹配程度更高的版本,则可能选择模板


5.1 显式指定模板参数

如果明确写:

Add<int>(1,2);

那么就是明确要求使用模板,并指定:

T = int

因此:

Add(1,2);

和:

Add<int>(1,2);

含义并不完全相同

前者会参与普通函数与模板之间的匹配

后者明确指定使用模板


6. 模板中的类型转换

模板参数推导时,编译器一般不会为了让模板匹配而随意进行类型转换

例如:

template<classT>TAdd(T left,T right){returnleft+right;}

然后:

inta=10;doubleb=20.0;Add(a,b);

此时:

T = int

和:

T = double

产生冲突

不能简单地认为:

int 自动转换成 double

然后就可以完成模板参数推导

如果确实希望使用某种类型,可以明确指定:

Add<double>(a,b);

或者:

Add(a,(int)b);

核心要区分:

普通函数调用 ↓ 可以发生常规的类型转换 模板参数推导 ↓ 不会依靠这种类型转换来解决模板参数推导冲突

7. 多模板参数

模板不一定只能有一个类型参数

例如:

template<classT1,classT2>T1Add(T1 left,T2 right){returnleft+right;}

这里有两个模板参数:

T1 T2

调用:

Add(1,2.0);

编译器可以推导:

T1 → int T2 → double

因此这种情况下就不会出现前面“两个实参争夺同一个T”的问题


8. 类模板

函数模板解决的是函数通用化问题

而类模板解决的是类通用化问题

例如实现一个栈

栈的基本结构无论存储什么数据,整体逻辑都可以保持一致

可能存:

int double char

如果每种类型都重新写一个Stack,会产生大量重复代码

因此可以使用类模板


8.1 类模板的定义格式

基本格式:

template<classT1,classT2,...,classTn>class类模板名{// 类内成员};

例如:

template<typenameT>classStack{public:Stack(size_t capacity=4){_array=newT[capacity];_capacity=capacity;_size=0;}voidPush(constT&data);private:T*_array;size_t _capacity;size_t _size;};

这里:

T*_array;

表示栈底层数组中的数据类型由T决定

如果:

T = int

那么相当于:

int*_array;

如果:

T = double

那么相当于:

double*_array;

因此,同一个类模板可以生成不同类型的栈


9. 类模板成员函数的定义

如果类模板的成员函数在类外定义,需要注意模板写法

例如类内声明:

template<classT>classStack{public:voidPush(constT&data);private:T*_array;size_t _capacity;size_t _size;};

类外定义:

template<classT>voidStack<T>::Push(constT&data){_array[_size]=data;++_size;}

这里有两个非常容易混淆的地方:

template<classT>

表示这是一个模板

而:

Stack<T>

表示当前定义的是Stack<T>这个类模板的成员函数

所以完整结构:

template<classT>voidStack<T>::Push(constT&data){// ...}

10. 类模板为什么通常不建议声明和定义分离

类模板的成员函数通常不建议像普通类那样:

Stack.h Stack.cpp

把声明和定义完全分开

例如:

// Stack.htemplate<classT>classStack{public:voidPush(constT&data);};

然后:

// Stack.cpptemplate<classT>voidStack<T>::Push(constT&data){// ...}

这种写法容易产生链接错误

原因在于模板真正需要的是:

使用某个具体类型实例化模板时,编译器需要看到模板的完整定义

例如:

Stack<int>st;st.Push(10);

编译器需要根据:

T = int

生成:

Stack<int>::Push(...)

如果当前编译单元只看到了模板声明,却看不到完整的模板定义,就无法正常完成对应的实例化

因此实际编程中通常直接把模板的声明和定义放在一起,或者放在.h/.hpp中


11. 类模板的实例化

类模板和函数模板的实例化方式有一个重要区别

函数模板可以通过函数参数进行类型推导:

Swap(a,b);

而类模板实例化时必须明确指定类型

例如:

Stack<int>st1;Stack<double>st2;

这里:

Stack

只是类模板名,并不是一个真正的具体类型

真正的类型是:

Stack<int>

和:

Stack<double>

可以理解为:

Stack ↓ 类模板 Stack<int> ↓ 具体的 int 类型栈 Stack<double> ↓ 具体的 double 类型栈

因此:

Stack<int>st1;

表示:

使用 Stack 模板 T = int 生成 Stack<int> 创建对象 st1

而:

Stack<double>st2;

表示:

使用 Stack 模板 T = double 生成 Stack<double> 创建对象 st2

12. 函数模板与类模板的核心区别

项目函数模板类模板
作用生成通用函数生成通用类
类型来源通常可以根据实参推导通常需要显式指定
使用方式Add(a, b)Stack<int> st
模板本身不是具体函数不是具体类
实例化结果具体函数具体类
常见用途通用算法、操作通用数据结构

最需要记住的是:

template<classT>TAdd(T a,T b);

这是函数模板

而:

template<classT>classStack{};

这是类模板

模板本身都是“模具”,真正参与程序工作的,是根据具体类型实例化出来的具体版本

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/5 2:31:28

计算机科学导论25题拆解:链式存储、进制转换与位运算复习地图

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/5 2:31:09

HTML+CSS学习笔记.4

1关于否定伪类,ui伪类和目标伪类&#xff1a; 在使用选择器时于选择器前方加上:not(),用于排除满足括号中条件的元素&#xff0c;通过这种方法可以使值不会作用于某些特定的元素上。UI伪类里有很多种小类&#xff0c;主要用于根据表单&#xff0c;按钮和输入框的状态自动切换样…

作者头像 李华
网站建设 2026/10/5 2:28:58

AnyJev护城河在校准

抄得走协议&#xff0c;抄不走校准一个开源项目用六天时间证明&#xff1a;决策模型真正的壁垒&#xff0c;不在接口 查证日期&#xff1a;2026-10-01 &#xff5c; AnyJev 数据均为项目自报&#xff08;GitHub README&#xff0c;Apache 2.0&#xff09;一、闭源模型被"抄…

作者头像 李华
网站建设 2026/10/5 2:28:55

搞懂打印耗材匹配逻辑,才能避开办公效率陷阱

打印机买回家&#xff0c;为什么越用越糟心&#xff1f; 你是否遇到过这样的情况&#xff1a;新买的打印机用了不到半年&#xff0c;打印模糊、频繁卡纸&#xff0c;换上便宜的兼容硒鼓后甚至直接报错停机&#xff1f;在晋江、石狮这样的电商活跃区域&#xff0c;一家小型外贸公…

作者头像 李华