☰
C++:模板初阶详解
2026/10/1 21:56:25 网站建设 项目流程

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

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

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

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; }

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

int double char

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

temp = left left = right right = temp

这就产生两个问题

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

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

所以我们真正想要的是:

我只写一次“交换算法” ↓ 至于到底交换 int、double、char ↓ 让编译器自己生成对应版本

这就是模板思想

一 什么是泛型编程?

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

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

以前我们写:

int Add(int x, int y);

这个函数只能处理int。

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

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

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

同一个模具 ↓ 放入不同材料 ↓ 得到不同成品

在 C++ 里就是:

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

二 函数模板到底是什么?

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

template<typename T> void Swap(T& left, T& right) { T temp = left; left = right; right = temp; }

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

注意这里:

T

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

它只是一个:

类型占位符。

你可以理解成:

T = “以后再告诉我是什么类型”

于是:

T temp = left;

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

如果以后 T 是 int 这里就是 int temp 如果 T 是 double 这里就是 double temp 如果 T 是 char 这里就是 char temp

三template<typename T>又是什么?

拆开来看:

template<typename T>

意思可以理解成:

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

然后:

void Swap(T& left, T& right)

表示:

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

模板参数也可以有多个:

template<typename T1, typename T2>

比如:

template<typename T1, typename T2> T1 Add(T1 x, T2 y) { return x + y; }

此时:

T1 T2

可以是不同类型。

四typename和class有什么区别?

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

所以:

template<typename T>

也可以写:

template<class T>

所以:

template<typename T>

和:

template<class T>

在这个阶段可以看成一样

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

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

例如你写:

template<typename T> void Swap(T& a, T& b) { T tmp = a; a = b; b = tmp; }

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

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

然后:

int a = 1, b = 2; Swap(a, b);

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

于是推导:

T = int

然后生成一个类似:

void Swap(int& a, int& b) { int tmp = a; a = b; b = tmp; }

如果又写:

double x = 1.1, y = 2.2; Swap(x, y);

编译器又推导:

T = double

生成类似:

void Swap(double& a, double& b) { double tmp = a; a = b; b = tmp; }

所以真正过程是:

函数模板 ↓ 根据实参推导 T ↓ 生成具体函数 ↓ 调用具体函数

六 模板实例化

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

例如:

Swap(a, b);

如果:

a、b 是 int

就产生:

Swap<int>

这就是一次实例化。

如果:

double x, y; Swap(x, y);

又产生:

Swap<double>

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

隐式实例化 显式实例化

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

例如:

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

然后:

int a1 = 10, a2 = 20; double d1 = 10.0, d2 = 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>

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

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

考虑:

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

然后:

int a = 10; double d = 20.0; Add(a, d);

这时候编译器开始推导。

根据:a

得到:

T = int

但是根据:d

又得到:

T = double

问题来了:T到底是谁?

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

它不可能同时:

T = int T = double

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

八 解决混合类型问题

1.方法一:自己转换

比如:

int a = 10; double d = 20.0;

可以:

Add(a, (int)d);

现在参数变成:

int int

于是:

T = int

没有冲突

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

另一种方法:

Add<int>(a, d);

这里:

<int>

相当于直接告诉编译器:

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

这就是:显式实例化

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

所以:

Add<int>(a, d);

相当于指定:

T = int

于是模板生成:

int Add(const int& left, const int& right);

此时第二个参数原本是:

double d

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

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

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

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

这时候:

Add(1, 2.0);

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

而是:

T1 = int T2 = double

所以能够成功实例化

九 普通函数和函数模板

1.能不能同名?

答案是可以

比如:

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

这里同时存在:

普通函数 Add 函数模板 Add

是允许的

2.谁先被调用?

上方代码中同时存在:

普通函数 Add 函数模板 Add

那么到底调用谁?

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

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

所以:

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

执行:

Add(1, 2);

两个都能处理。

普通函数:

Add(int, int)

直接完美匹配。

模板也可以实例化:

Add<int>(int, int)

也完全匹配。

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

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

1.显示实例化

我们可以写:

Add<int>(1, 2);

只要你显式写了:

<int>

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

2.模板匹配更佳

例如:

int Add(int left, int right) { return left + right; }

同时:

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

调用:

Add(1, 2.0);

普通函数需要把:

2.0 double

转换成:

int

才能调用

而模板可以直接生成:

T1 = int T2 = double

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

所以:

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

十 类模板

1.类模板的定义

基本格式:

template<class T1, class T2, ..., class Tn> class 类模板名 { // 类成员 };

例如:

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; };

这里最关键:

T* _array;

以前:

int* _array;

只能保存int。

现在:

T* _array;

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

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

例如:

Stack<int> st1;

编译器看到:

T = int

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

class Stack<int> { private: int* _array; size_t _capacity; size_t _size; };

而:

Stack<double> st2;

相当于:

T = double

于是内部:

double* _array;

所以同一份类模板:

template<typename T> class Stack

既能产生:

Stack<int> Stack<double>

这就是类模板的意义

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

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

所以:

Stack st;

按照规则是不完整的

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

应该写:

Stack<int> st1;

或者:

Stack<double> st2;

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

函数模板:

template<class T> void Func(T x);

调用:

Func(10);

编译器通常可以根据:

10 是 int

自己推导:

T = int

所以可以不写:

Func<int>(10);

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

Stack<int>

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

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

类:

template<typename T> class Stack { public: void Push(const T& data); };

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

template<class T> void Stack<T>::Push(const T& data) { ... }

一定注意:

不是:

void Stack::Push(...)

而是:

void Stack<T>::Push(...)

为什么?

因为:

Stack

本身只是模板名字。

我们现在定义的是:

某个 T 对应的 Stack<T> 的成员函数

所以必须写:

Stack<T>::

而且前面还要再次声明:

template<class T>

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询