前言

继上一篇内存管理之后,我们继续深入 C++ 的核心特性——模板。模板让代码与类型解耦,实现零开销的泛型编程,是 STL 容器与算法的基石。此外模板分为函数模板和类模板,下面我们会一一引入。

目录

前言

(一)C++引入模板的意义

(二)函数模板

1.函数模板的概念

2.函数模板的格式

3.函数模板的原理

4.函数模板的实例化

1.隐式实例化

2.显式实例化

5.模板参数的匹配原则

(三)类模板

1.类模板的定义格式

2.类模板的实例化

(四)补充

1. 模板参数可以给默认值(仅类模板支持)

2. 一个小坑:模板参数不能是局部类型(C++98限制,C++11已放宽)

小结


(一)C++引入模板的意义

C++ 引入模板的核心意义是实现泛型编程——即编写与类型无关、可在多种数据类型上复用的代码,同时保持零开销抽象和类型安全。

用我们以前学过的内容写一个swap函数实现两个参数的交换,我们通常要显式地写出参数列表,如果我们要写多种类型的交换,只能使用函数重载,但是有以下几个不好的地方:

1. 重载的函数仅仅是类型不同,代码复用率比较低,只要有新类型出现时,就需要用户自己增加对应的函数。

2. 代码的可维护性比较低,一个出错可能所有的重载均出错。

例如:

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

在C++中,存在一个“模具”,通过给这个模具填充补充的材料(类型),就能获得不同材料的铸件(即生成该类型写出的代码)。

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


(二)函数模板

1.函数模板的概念

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

2.函数模板的格式

template<typename T1, typename T2,......,typename Tn>

返回值类型 函数名(参数列表){}

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

注意:typename也可以替换成class。

3.函数模板的原理

函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器。

在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然后产生一份专门处理double类型的代码,对于字符类型也是如此。

4.函数模板的实例化

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

1.隐式实例化

编译器通过实参推演模板参数的实际类型:

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);
Add(d1, d2);
return 0;
}

当实参类型不同时:

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, d1);
//该语句不能编译通过,通过实参a1推演T为int,通过实参d1推演T为double,但模板参数列表只有一个T
}

此时只有两种处理方式:1.强制转换    2.显式实例化

//强制类型转换
Add(a1, (int)d1);

2.显式实例化

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

template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
int main(void)
{
int a = 10;
double b = 20.0;
// 显式实例化
Add<int>(a, b);
return 0;
}

5.模板参数的匹配原则

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

// 专门处理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版本
}

总结为:有现成的不需要再搞一个新的。

2. 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板。

// 专门处理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函数
}

3. 模板函数不允许自动类型转换,但普通函数可以进行自动类型转换。

普通函数:调用时如果实参类型与形参类型不完全匹配,编译器会尝试隐式类型转换(如 int → double,char* → string 等),使调用成功。

函数模板:在模板参数推导阶段,编译器不会进行隐式类型转换。如果推导出的类型不一致(或无法推导),就会报错。

对比:

1.普通函数允许隐式转换:

void func(double x) {}

int main() {
    int a = 10;
    func(a);        // 合法:int 隐式转换为 double
}

func期望double,但传入了int,编译器自动将a 提升为double。

2.函数模板不允许隐式转换

template<typename T>
void func(T x) {}

int main() {
    int a = 10;
    double b = 20.0;
    func(a);      // 推导 T = int,ok
    func(b);      // 推导 T = double,ok
    func(a, b);   // 错误!只有一个模板参数 T,但两个实参类型不同,无法推导
}

(三)类模板

1.类模板的定义格式

template<class T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员定义
};
#include<iostream>
using namespace std;
// 类模版
template<typename T>
class Stack
{
public:
Stack(size_t capacity = 4)
{
_array = new T[capacity];
_capacity = capacity;
_size = 0;
}
void Push(const T& data);
private:
T* _array;
size_t _capacity;
size_t _size;
};
template<class T>
void Stack<T>::Push(const T& data)
{
// 扩容
_array[_size] = data;
++_size;
}
int main()
{
Stack<int> st1; // int
Stack<double> st2; // double
return 0;
}

注意:模板不建议声明和定义分离到两个文件.h 和.cpp会出现链接错误

2.类模板的实例化

类模板实例化与函数模板实例化不同,类模板实例化需要再类模板名字后面跟上<>,然后将实例化的类型放在里面即可,类模板不是真正的类,实例化后得到的结果才是真正的类。

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

注意:Stack是类名,Stack<int>才是类型。

(四)补充

1. 模板参数可以给默认值(仅类模板支持)

函数模板不能有默认模板参数(C++11 起允许),类模板可以:

template<typename T = int>   // 默认类型为 int
class Stack {
    // ...
};

Stack<> st;   // 相当于 Stack<int>

2. 一个小坑:模板参数不能是局部类型(C++98限制,C++11已放宽)

早期 C++ 禁止将局部类作为模板参数,C++11 后允许了。如果读者是 C++11 环境,现代 C++ 已无此限制。

小结

本篇介绍了 C++ 模板的基础知识,包括函数模板与类模板的定义、实例化及匹配规则。模板的核心价值在于实现泛型编程:将算法与数据类型解耦,在不损失运行时性能的前提下大幅提升代码复用性。

  • 函数模板允许编译器根据实参类型自动生成对应函数,支持隐式与显式实例化。需注意模板参数推导阶段不进行隐式类型转换,这有别于普通函数的行为。

  • 类模板用于定义可适配任意类型的类,实例化时必须显式指定模板实参(除非提供默认值)。模板的声明与定义通常应放在同一头文件中,以避免链接错误。

  • 当普通函数与函数模板共存时,重载决议会优先选择参数匹配度更高的版本,这可能导致模板优于普通函数或反之。

掌握模板初阶是进一步学习 STL 源码、容器适配器及模板进阶(特化、变参模板、SFINAE 等)的必要基础。正确运用模板能显著提升代码质量与开发效率。

好啦,模板基础就到这里。接下来,我们终于要正式走进 STL 的世界了。第一站,就是大家最熟悉、却也最容易用出问题的容器string。它为什么能像普通类型一样赋值、相加?我们怎样运用模板?下一篇,我们一起揭开string的神秘面纱。

更多推荐