C++:模板初阶详解

前言:为什么 C++ 要有模板?

我们可以从一个非常简单的 Swap交换函数 开始。

如果没有模板,为了交换不同类型的数据,我们可能写:

cpp 复制代码
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;
}

你会发现,这几个函数除了:

cpp 复制代码
int
double
char

不同之外,算法完全一样:

cpp 复制代码
temp = left
left = right
right = temp

这就产生两个问题

第一,代码复用率低。每来一个新类型,就要再写一遍

第二,可维护性差。如果交换逻辑需要修改,就可能每一个重载版本都要改

所以我们真正想要的是:

cpp 复制代码
我只写一次"交换算法"
        ↓
至于到底交换 int、double、char
        ↓
让编译器自己生成对应版本

这就是模板思想

一 什么是泛型编程?

在具体讲解模板之前,我们先了解一下泛型编程

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

以前我们写:

cpp 复制代码
int Add(int x, int y);

这个函数只能处理 int。

而泛型编程想表达的是:关心的是"怎么加",而不是"加的是哪种具体类型"

于是我们把"类型"先抽象出来。

你可以把模板想象成模具":

复制代码
同一个模具
   ↓
放入不同材料
   ↓
得到不同成品

在 C++ 里就是:

复制代码
同一个模板
   ↓
传入不同类型
   ↓
生成不同类型的代码

二 函数模板到底是什么?

我们先了解最基本的函数模板:

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

函数模板代表一个函数家族,它与具体类型无关,使用时根据实参类型产生对应的具体函数版本

注意这里:

cpp 复制代码
T

现在并不是某个真正的具体类型。

它只是一个:

类型占位符。

你可以理解成:

复制代码
T = "以后再告诉我是什么类型"

于是:

cpp 复制代码
T temp = left;

不是说存在一种真正叫 T 的数据类型,而是说:

cpp 复制代码
如果以后 T 是 int
这里就是 int temp

如果 T 是 double
这里就是 double temp

如果 T 是 char
这里就是 char temp

三 template<typename T> 又是什么?

拆开来看:

cpp 复制代码
template<typename T>

意思可以理解成:

我接下来定义的是一个模板,其中有一个类型参数,名字叫 T

然后:

cpp 复制代码
void Swap(T& left, T& right)

表示:

left 和 right 的类型先不确定,由以后调用时再决定。

模板参数也可以有多个:

cpp 复制代码
template<typename T1, typename T2>

比如:

cpp 复制代码
template<typename T1, typename T2>
T1 Add(T1 x, T2 y)
{
    return x + y;
}

此时:

cpp 复制代码
T1
T2

可以是不同类型。

四 typename 和 class 有什么区别?

我们在之前学过了类与对象,初步了解到了class的相关知识,其实在这个地方,我们也可以用class来定义模板类型参数的

所以:

cpp 复制代码
template<typename T>

也可以写:

cpp 复制代码
template<class T>

所以:

cpp 复制代码
template<typename T>

和:

cpp 复制代码
template<class T>

在这个阶段可以看成一样

五 模板本身并不是一个真正的函数

函数模板是一个蓝图,本身并不是函数,而是编译器根据使用方式生成具体函数的模具

例如你写:

cpp 复制代码
template<typename T>
void Swap(T& a, T& b)
{
    T tmp = a;
    a = b;
    b = tmp;
}

这时不要理解成:已经生成了一个万能 Swap 函数

更准确地说:现在只有一张"生成函数的图纸"

然后:

cpp 复制代码
int a = 1, b = 2;
Swap(a, b);

编译器看到:两个参数都是 int

于是推导:

cpp 复制代码
T = int

然后生成一个类似:

cpp 复制代码
void Swap(int& a, int& b)
{
    int tmp = a;
    a = b;
    b = tmp;
}

如果又写:

cpp 复制代码
double x = 1.1, y = 2.2;
Swap(x, y);

编译器又推导:

cpp 复制代码
T = double

生成类似:

cpp 复制代码
void Swap(double& a, double& b)
{
    double tmp = a;
    a = b;
    b = tmp;
}

所以真正过程是:

cpp 复制代码
函数模板
   ↓
根据实参推导 T
   ↓
生成具体函数
   ↓
调用具体函数

六 模板实例化

使用不同类型参数调用函数模板时,产生具体版本的过程称为函数模板实例化

例如:

cpp 复制代码
Swap(a, b);

如果:

cpp 复制代码
a、b 是 int

就产生:

cpp 复制代码
Swap<int>

这就是一次实例化。

如果:

cpp 复制代码
double x, y;
Swap(x, y);

又产生:

cpp 复制代码
Swap<double>

函数模板实例化又分两种:

复制代码
隐式实例化
显式实例化

1. 隐式实例化:让编译器自己猜 T

例如:

cpp 复制代码
template<class T>
T Add(const T& left, const T& right)
{
    return left + right;
}

然后:

cpp 复制代码
int a1 = 10, a2 = 20;
double d1 = 10.0, d2 = 20.0;

Add(a1, a2);
Add(d1, d2);

第一句:

cpp 复制代码
Add(a1, a2);

编译器看到:

cpp 复制代码
a1 → int
a2 → int

于是推导:

cpp 复制代码
T = int

第二句:

cpp 复制代码
Add(d1, d2);

推导:

cpp 复制代码
T = double

这就是:隐式实例化

你没有写:

cpp 复制代码
Add<int>

也没有写:

cpp 复制代码
Add<double>

而是编译器自己根据实参类型推导

七 函数模板最容易出错的地方:同一个 T 推导出了两个结果

考虑:

cpp 复制代码
template<class T>
T Add(const T& left, const T& right)
{
    return left + right;
}

然后:

cpp 复制代码
int a = 10;
double d = 20.0;

Add(a, d);

这时候编译器开始推导。

根据:a

得到:

cpp 复制代码
T = int

但是根据:d

又得到:

cpp 复制代码
T = double

问题来了:T到底是谁?

模板只有:T一个模板参数。

它不可能同时:

cpp 复制代码
T = int
T = double

所以编译器无法确定具体类型,会产生错误

八 解决混合类型问题

1.方法一:自己转换

比如:

cpp 复制代码
int a = 10;
double d = 20.0;

可以:

cpp 复制代码
Add(a, (int)d);

现在参数变成:

cpp 复制代码
int
int

于是:

cpp 复制代码
T = int

没有冲突

2.方法二:显式指定模板参数

另一种方法:

cpp 复制代码
Add<int>(a, d);

这里:

cpp 复制代码
<int>

相当于直接告诉编译器:

不需要你推导了,我明确指定 T = int。

这就是:显式实例化

在函数名后面的 <> 中直接指定模板参数的实际类型

所以:

cpp 复制代码
Add<int>(a, d);

相当于指定:

cpp 复制代码
T = int

于是模板生成:

cpp 复制代码
int Add(const int& left, const int& right);

此时第二个参数原本是:

cpp 复制代码
double d

因为目标参数类型已经确定为 int,编译器会尝试进行类型转换。

显式实例化之后,如果实际参数与指定类型不完全相同,编译器会尝试隐式转换,无法转换才会报错

3.方法三:使用两个模板参数

cpp 复制代码
template<class T1, class T2>
T1 Add(T1 left, T2 right)
{
    return left + right;
}

这时候:

cpp 复制代码
Add(1, 2.0);

就不再要求两个参数必须对应同一个 T。

而是:

cpp 复制代码
T1 = int
T2 = double

所以能够成功实例化

九 普通函数和函数模板

1.能不能同名?

答案是可以

比如:

cpp 复制代码
int Add(int left, int right)
{
    return left + right;
}

template<class T>
T Add(T left, T right)
{
    return left + right;
}

这里同时存在:

cpp 复制代码
普通函数 Add

函数模板 Add

是允许的

2.谁先被调用?

上方代码中同时存在:

cpp 复制代码
普通函数 Add

函数模板 Add

那么到底调用谁?

答案是:普通函数和模板一样匹配时,优先普通函数

如果非模板函数和函数模板都能匹配,并且匹配程度相同,那么优先调用非模板函数。 06.模板初阶

所以:

cpp 复制代码
int Add(int left, int right);

template<class T>
T Add(T left, T right);

执行:

cpp 复制代码
Add(1, 2);

两个都能处理。

普通函数:

复制代码
Add(int, int)

直接完美匹配。

模板也可以实例化:

复制代码
Add<int>(int, int)

也完全匹配。

既然一样好:优先选择普通函数,所以不会没必要地再从模板生成一个版本

3.那如果我们想强制调用模板,该怎么做呢?

1.显示实例化

我们可以写:

cpp 复制代码
Add<int>(1, 2);

只要你显式写了:

cpp 复制代码
<int>

就明确表示:我要用模板版本了

2.模板匹配更佳

例如:

cpp 复制代码
int Add(int left, int right)
{
    return left + right;
}

同时:

cpp 复制代码
template<class T1, class T2>
T1 Add(T1 left, T2 right)
{
    return left + right;
}

调用:

cpp 复制代码
Add(1, 2.0);

普通函数需要把:

cpp 复制代码
2.0 double

转换成:

cpp 复制代码
int

才能调用

而模板可以直接生成:

cpp 复制代码
T1 = int
T2 = double

于是:模板版本匹配得更准确

所以:

如果模板能够生成一个匹配程度更好的函数,就选择模板版本

十 类模板

1.类模板的定义

基本格式:

cpp 复制代码
template<class T1, class T2, ..., class Tn>
class 类模板名
{
    // 类成员
};

例如:

cpp 复制代码
template<typename T>
class Stack
{
public:
    Stack(size_t capacity = 4)
    {
        _array = new T[capacity];
        _capacity = capacity;
        _size = 0;
    }

private:
    T* _array;
    size_t _capacity;
    size_t _size;
};

这里最关键:

cpp 复制代码
T* _array;

以前:

cpp 复制代码
int* _array;

只能保存 int。

现在:

cpp 复制代码
T* _array;

具体存什么,等实例化时决定

2.Stack<T> 到底发生了什么?

例如:

cpp 复制代码
Stack<int> st1;

编译器看到:

cpp 复制代码
T = int

那么可以帮助理解成生成了一个具体版本:

cpp 复制代码
class Stack<int>
{
private:
    int* _array;
    size_t _capacity;
    size_t _size;
};

而:

cpp 复制代码
Stack<double> st2;

相当于:

cpp 复制代码
T = double

于是内部:

cpp 复制代码
double* _array;

所以同一份类模板:

cpp 复制代码
template<typename T>
class Stack

既能产生:

cpp 复制代码
Stack<int>
Stack<double>

这就是类模板的意义

3.类模板中 Stack 本身不是一个真正的类型

Stack 是类模板名,Stack<int> 才是类型

所以:

cpp 复制代码
Stack st;

按照规则是不完整的

因为编译器不知道:T 是什么?

应该写:

cpp 复制代码
Stack<int> st1;

或者:

cpp 复制代码
Stack<double> st2;

4.函数模板和类模板在实例化上的区别

函数模板:

cpp 复制代码
template<class T>
void Func(T x);

调用:

cpp 复制代码
Func(10);

编译器通常可以根据:

cpp 复制代码
10 是 int

自己推导:

cpp 复制代码
T = int

所以可以不写:

cpp 复制代码
Func<int>(10);

但是类模板实例化时需要:

cpp 复制代码
Stack<int>

显式把类型写到:<>里面

5.类模板成员函数放到类外怎么写?

类:

cpp 复制代码
template<typename T>
class Stack
{
public:
    void Push(const T& data);
};

如果想在类外定义push函数可以这么写:

cpp 复制代码
template<class T>
void Stack<T>::Push(const T& data)
{
    ...
}

一定注意:

不是:

cpp 复制代码
void Stack::Push(...)

而是:

cpp 复制代码
void Stack<T>::Push(...)

为什么?

因为:

cpp 复制代码
Stack

本身只是模板名字。

我们现在定义的是:

cpp 复制代码
某个 T 对应的 Stack<T> 的成员函数

所以必须写:

cpp 复制代码
Stack<T>::

而且前面还要再次声明:

cpp 复制代码
template<class T>
相关推荐
辣知1 小时前
辣知·化智69 西周青铜器何尊的宅兹中国
学习
AOI小白新手上路1 小时前
韦东山 3-5 I.MX6ULL 的 LED 编程 · 学习笔记
arm开发·笔记·单片机·学习·架构·硬件架构
知识分享小能手1 小时前
C++ 学习教程,从入门到精通,C++类继承 — 完整知识点详解(13)
开发语言·c++·学习
YYRAN_ZZU1 小时前
如何编一个gcc的交叉工具链
linux·c++
弹简特1 小时前
【Java项目-企悦抽】15-活动管理模块02-活动创建测试与活动列表实现
java·开发语言·springboot
AI4ELAB2 小时前
品茗科技,3431万收购了一家新加坡公司,刚刚完成首次交割
经验分享
Frank_refuel2 小时前
【C++】手撕JRpc框架(梳理RPC流程)
c++
Joe_Wang52 小时前
【从0到1学习JVM · 25】同样都要停顿所有线程,Parallel比Serial到底强在哪
java·jvm·学习·垃圾回收
wuminyu2 小时前
JVM虚拟线程的底层实现原理分析
java·linux·c语言·jvm·c++