【C++ 学习 ⑦】- 模板初阶(函数模板和类模板)

news/2025/1/11 14:55:05/

目录

一、前言

二、函数模板

2.1 - 基本概念和原理

2.2 - 定义格式

2.3 - 实例化详解

2.3.1 - 隐式实例化

2.3.2 - 显示实例化

2.4 - 模板参数的匹配原则

三、类模板

3.1 - 定义格式

3.2 - 实例化


参考资料

  1. C++函数模板(模板函数)详解 (biancheng.net)。

  2. C++函数模板5分钟入门教程 (biancheng.net)。

  3. C++类模板5分钟入门教程 (biancheng.net)。


一、前言

问题:如何实现一个通用的交换函数?

// 交换两个 int 类型变量的值
void Swap(int& x, int& y)
{int tmp = x;x = y;y = tmp;
}
​
// 交换两个 double 类型变量的值
void Swap(double& x, double& y)
{double tmp = x;x = y;y = tmp;
}
​
// 交换两个 char 类型变量的值
void Swap(char& x, char& y)
{char tmp = x;x = y;y = tmp;
}
​
// ... ...

虽然我们可以重载很多个函数,以满足交换不同类型变量的需求,但是这种方法不太高明

  1. 重载的函数除了形参 xy 以及函数体中临时变量 tmp 的数据类型不同,其他的代码都一样。

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

那么能否只写一遍 Swap 函数,就能用来交换各种类型变量的值呢?因此 "模板" 的概念就应运而生了

众所周知,有了 "模子" 后,用 "模子" 来批量制造陶瓷、塑料、金属制品就变得容易了。程序设计语言中的模板就是用来批量生成功能和形式都几乎相同的代码的。有了模板,编译器就能在需要的时候,根据模板自动生成程序的代码。从同一个模板自动生成的代码,形式几乎是一样的

模板是泛型编程的基础,泛型编程即以一种独立于任何特定类型的方式编写代码


二、函数模板

2.1 - 基本概念和原理

所谓函数模板(Function Template),实际上是建立一个通用函数,它所用到的数据类型(包括形参类型、局部变量类型、返回值类型)可以不具体指定,而是用一个虚拟的类型来代替(实际上是用一个标识符来占位)。编译的时候,编译器推导实参的数据类型,根据实参的数据类型和函数模板,生成特定的函数定义

在函数模板中,数据的值(value)和类型(type)都被参数化了

生成函数定义的过程被称为实例化

2.2 - 定义格式

template<typename T1. typename T2, ...>
返回值类型 函数名(参数列表)
{函数体
}

注意

  1. T1、T2、... 是类型参数(也可以说是虚拟的类型,或者说是类型占位符),类型参数的命名规则跟其他标识符的命名规则一样,不过使用 T、T1、T2、Type 等已经成为了一种习惯

  2. 其中 typename 关键字也可以用 class 关键字替换,它们没有任何区别

    C++ 早期对模板的支持并不严谨,没有引入新的关键字,而是用 class 来指明类型参数,但是 class 关键字本来已经用在类的定义中了,这样做显得不太友好,所以后来 C++ 又引入了一个新的关键字 typename,专门用来定义类型参数。不过至今仍然有很多代码在使用 class 关键字,包括 C++ 标准库、一些开源程序等。

#include <iostream>
using namespace std;
​
// template<typename T>
// 或者
template<class T>
void Swap(T& x, T& y)
{T tmp = x;x = y;y = tmp;
}
​
int main()
{int a = 10, b = 20;Swap(a, b);cout << a << " " << b << endl;  // 20 10double c = 1.1, d = 2.2;Swap(c, d);cout << c << " " << d << endl;  // 2.2 1.1
​char e = 'm', f = 'n';Swap(e, f);cout << e << " " << f << endl;  // n mreturn 0;
}

编译器会根据 Swap 模板自动生成多个 Swap 函数,用来交换不同类型变量的值。

函数模板中定义的类型参数不仅可以用在函数定义中,还可以用在函数声明中

#include <iostream>
using namespace std;
​
template<typename T>
void Swap(T& x, T& y);
​
int main()
{int a = 10, b = 20;Swap(a, b);cout << a << " " << b << endl;  // 20 10return 0;
}
​
template<typename T>
void Swap(T& x, T& y)
{T tmp = x;x = y;y = tmp;
}

2.3 - 实例化详解

2.3.1 - 隐式实例化

让编译器根据实参类型推演模板参数的实际类型

#include <iostream>
using namespace std;
​
template<typename T>
T Add(const T& x, const T& y)
{return x + y;
}
​
int main()
{int a = 10, b = 20;cout << Add(a, b) << endl;  // 30
​double c = 1.1, d = 2.2;cout << Add(c, d) << endl;  // 3.3return 0;
}

注意

cout << Add(a, c) << endl;  
// error:"T Add(const T &,const T &)": 模板 参数 "T" 不明确

上面的语句不能通过编译,是因为在编译期间,编译器通过实参 a 将 T 推演为 int 类型,通过实参 c 将 T 推演为 double 类型,而模板参数列表中只有一个 T,编译器无法确定此处到底该将 T 确定为 int 还是 double 类型而报错。

在模板中,编译器一般不会进行类型转换操作,因为一旦转换出问题,编译器就需要 "背黑锅"。

解决方案一

cout << Add((double)a, c) << endl;  // 11.1
// 或者
cout << Add(a, (int)c) << endl;  // 11
// 以 (int)c 为例,c 强转类型转换后传参的过程大概可以分为以下几个步骤:
// 1. 在另外的地方找一个内存构造一个临时变量 tmp
// 2. 将 c 的整数部分赋值给 tmp
// 3. 用 tmp 传参
// 4. 销毁 tmp
// 因为临时变量具有常性,所以 Add 函数的形参 x 和 y 必须用常引用

解决方案二

#include <iostream>
using namespace std;
​
template<typename T1, typename T2>
T1 Add(const T1& x, const T2& y)
{return x + y;
}
​
int main()
{int a = 10;double c = 1.1;cout << Add(a, c) << endl;  // 11// 若 Add 函数的返回值类型为 T2,则输出的结果为 11.1return 0;
}

解决方案三:显示实例化

2.3.2 - 显示实例化

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

cout << Add<double>(a, c) << endl;  // 11.1
cout << Add<int>(a, c) << endl;  // 11

如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器就会报错。

更常见的使用场景

#include <iostream>
using namespace std;
​
template<typename T>
T* Alloc(int n)
{return new T[n];
}
​
int main()
{int* arr = Alloc<int>(5);for (int i = 0; i < 5; ++i){arr[i] = i;}for (int i = 0; i < 5; ++i){cout << arr[i] << " ";}cout << endl;// 0 1 2 3 4delete[] arr;return 0;
}

2.4 - 模板参数的匹配原则

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

    #include <iostream>
    using namespace std;
    ​
    void Swap(int& x, int& y)
    {int tmp = x;x = y;y = tmp;
    }
    ​
    template<typename T>
    void Swap(T& x, T& y)
    {T tmp = x;x = y;y = tmp;
    }
    ​
    int main()
    {int a = 10, b = 20;Swap(a, b);  // 与模板函数匹配,编译器不需要特化cout << a << " " << b << endl;  // 10 20
    ​Swap<int>(a, b);  // 调用编译器特化的 Swap 版本cout << a << " " << b << endl;  // 20 10return 0;
    }

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

    #include <iostream>
    using namespace std;
    ​
    int Add(int x, int y)
    {return x + y;
    }
    ​
    template<typename T1, typename T2>
    T1 Add(const T1& x, const T2& y)
    {return x + y;
    }
    ​
    int main()
    {int a = 10, b = 20;cout << Add(a, b) << endl;  // 30// 与非模板函数完全匹配,不需要函数模板实例化double c = 1.1;cout << Add(a, c) << endl;  // 11// 模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的 Add 函数return 0;
    }


三、类模板

3.1 - 定义格式

C++ 除了支持函数模板,还支持类模板(Class Template)。函数模板中定义的类型参数可以用在函数声明和函数定义中,类模板中定义的类型参数也可以用在类声明和类实现中。类模板的目的同样是将数据的类型参数化

声明类模板的格式为

template<class T1, class T2, ...>
class 类名
{// TODO;
};

例如

template<class T>
class Stack
{
public:Stack(int default_capacity = 5): _data(new T[default_capacity]), _top(0), _capacity(default_capacity){}~Stack();
private:T* _data;int _top;int _capacity;
};

上面的代码仅仅是类的声明,我们还需要在类外定义成员函数。在类外定义成员函数时仍然需要带上模板头,格式为

template<class T>
Stack<T>::~Stack()
{if (_data)delete[] _data;_top = _capacity = 0;
}

注意:除了 template 关键字后面要指明类型参数,类名 Stack 后面也要带上类型参数,只是不加 classtypename 关键字了

3.2 - 实例化

与函数模板不同的是,类模板实例化需要在类模板名字后面跟 <>,然后将实例化的类型放在 <> 中,即类模板必须显示实例化,因为编译器不能根据给定的数据推演出数据类型

类模板名字不是真正的类,而实例化的结果才是真正的类

Stack<int> st1;
Stack<double> st2;
Stack<int>* p1 = new Stack<int>(10);
Stack<double>* p2 = new Stack<double>(15);

对于普通类,类名和类型是一样的;对于模板类,类名和类型不一样,在上面的例子中,类名是 Stack,类型则是 Stack<int>/ Stack<double>


http://www.ppmy.cn/news/87408.html

相关文章

C++ 中智能指针的用法

在 C 中&#xff0c;智能指针是一种封装了动态分配内存的指针类&#xff0c;它们能够自动处理分配和释放内存的操作&#xff0c;从而避免出现内存泄漏的问题。C 中的智能指针有三种&#xff1a;std::unique_ptr、std::shared_ptr 和 std::weak_ptr&#xff08;auto_ptr 在 C11 …

ceph 单节点 ceph-deploy安装部署

ceph单节点部署 1.查询挂载信息 lsblk 2.使用自定义镜像源,默认镜像源地址&#xff08;可选&#xff09; /etc/yum.repos.d/CentOS-Base.repo 替换baseurl路径为本地路径 /root/ceph_install_offline/ceph-package 链接&#xff1a;https://pan.baidu.com/s/180zM5gPcXN5gVke…

深入理解Java虚拟机:JVM高级特性与最佳实践-总结-9

深入理解Java虚拟机&#xff1a;JVM高级特性与最佳实践-总结-9 虚拟机类加载机制类加载的过程准备解析字段解析 方法解析接口方法解析 虚拟机类加载机制 类加载的过程 准备 准备阶段是正式为类中定义的变量&#xff08;即静态变量&#xff0c;被static修饰的变量&#xff09…

判断一维对象数组的对象时间属性值是未来、今天、昨天、一周内、30天内、30天以前,并将该数组按照时间分类组成二维数组用于分时间段渲染

//判断时间 let today [] as any let yesterday [] as any let aWeek [] as any let aMonth [] as any let longlongAgo [] as any//封装时间判断方法 let judgeTime function (time) {let date timelet oneDay 60 * 60 * 24 //date 1684119095 //2023/5/16date date…

Linux之软件包管理

软件包管理 RPM RPM 概述 RPM&#xff08;RedHat Package Manager&#xff09;&#xff0c; RedHat软件包管理工具&#xff0c; 类似windows里面的setup.exe&#xff0c;是Linux这系列操作系统里面的打包安装工具&#xff0c; 它虽然是RedHat的标志&#xff0c; 但理念是通用…

实验7 多用户界面、菜单以及对话框程序设计

实验内容 1.设计一个具有两个页面的程序&#xff0c;第一个页面显示一张封面的图片&#xff0c;第二个页面显示“欢迎进入本系统”,这两个页面之间能相互切换。    2.设计一个具有3个选项的菜单程序&#xff0c;当单击每个选项时&#xff0c;分别跳转到3个不同的页面。 3.设…

Java Applet研究与应用 ——综合测评系统

摘 要 大学期间&#xff0c;综合测评计算是每学期必不可少的工作。人工计算综合测评是一个很繁杂的过程&#xff1a;每个学生先计算自己的综合测评成绩&#xff0c;制成草表&#xff0c;上交给班委&#xff1b;然后班委核对并将成绩录入制成电子文档上交给院系相关部门。在这个…

leetcode-岛屿数量(Java实现)

使用递归算法和并查集两种方式解决岛屿数量 LeetCode原题链接题目描述递归解法并查集解法并查集的知识学习。 LeetCode原题链接 岛屿数量 题目描述 给你一个由 ‘1’&#xff08;陆地&#xff09;和 ‘0’&#xff08;水&#xff09;组成的的二维网格&#xff0c;请你计算网格…