【C++初阶】:泛型编程的代表作---C++初阶模板

🎈主页传送门****:良木生香

🔥个人专栏:《C语言》 《数据结构-初阶》 《程序设计》《鼠鼠的C++学习之路》

🌟人为善,福随未至,祸已远行;人为恶,祸虽未至,福已远离



前言:在上一篇文章中,我们学习了C++的内存管理,学习了如何使用new/delete,他们两个的组成原理以及他们与C语言中malloc/free之间的关系和差别,那么今天我们就再来学习一个新的知识点---C++模板


目录

一、模板的概念

二、函数模板

2.1、函数模板的原理

2.2、模板的实例化

1、使用强制类型转换:

2、显示实例化:

3、真正的使用场景:

2.3、函数模板的匹配原则

三、类模板

3.1、类模板的原理

3.2、类模板的使用



一、模板的概念

这里有一个新的概念,叫做泛型编程,什么是泛型编程呢?不着急,我们本篇文章将会围绕着泛型编程来展开讲解。

先从一个简单的问题开始:当我们想要实现不同数据类型之间的数据交换,我们该怎么做呢?是像下面这样吗?

cpp 复制代码
//各类型的数据交换函数
void Swap_int(int& a, int& b) {
	int temp = a;
	a = b;
	b = temp;
}


void Swap_double(double& a, double& b) {
	double temp = a;
	a = b;
	b = temp;
}

void Swap_char(char& a, char& b) {
	char temp = a;
	a = b;
	b = temp;
}

以上的方法使用函数重载虽然可以实现,但是有以下几个不好的地方:

  1. 重载的函数仅仅是类型不同,代码复用率比较低,只要有新类型出现时,就需要用户自己增加对应的函数
  2. 代码的可维护性比较低,一个出错可能所有的重载均出错
    那能否 告诉编译器一个模子,让编译器根据不同的类型利用该模子来生成代码 呢?
    这时候C++模板就诞生了,语法如下:
cpp 复制代码
// 模板声明关键字 + 模板参数列表
template <typename 类型参数名1, typename 类型参数名2, ...>
// 函数定义
返回值类型 函数名(参数列表) {
    函数体;
}

其中,template是告诉编译器就接下来的函数是个模板函数,<>里面放的是模板参数列表,typename表示的是声明一个泛型类型(也可以是class,现阶段可以认为两者没有差异),类型参数名代表的是自定义类型占位符,可以是T,U等等。

二、函数模板

使用模板的话,Swap()函数就可以写成:

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

那么使用的时候就像下面这样:

cpp 复制代码
#include<iostream>
using namespace std;

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

int main() {

	int a = 10, b = 20;
	char c = 'x', d = 'y';
	double e = 2.5, f = 3.6;
	Swap(a, b);
	cout << a << " " << b << endl;
	Swap(c, d);
	cout << c << " " << d << endl;
	Swap(e, f);
	cout << e << " " << f << endl;

	return 0;
}

运行的结果为:

这时候我们再来想一个问题:三种类型的交换函数,调用的是同一个函数吗?我们调用反汇编看看:


通过反汇编的结果来看,很显然三种类型的交换函数调用的不是同个函数。在我框出来的地方可以清楚的看到,template会根据传进来的参数类型去创建不同类型的函数,而不同类型的函数相当于构成函数构成函数重载,可以参考下面这张图:

2.1、函数模板的原理

函数模板本身就是一个蓝图,它本身并不是函数,就像我们之前学习的类,所以就是将之前本来应该是我们程序员的做的事交给了编译器去做,提升了很大的效率空间,就像我们的洗衣机,洗碗机等等这些半自动化机器。

在template <typename T>中,也可以写成 template<class T>,在现阶段可以认为class和typename没有任何区别,但是有一些场景只能用tpyename,后续的学习中我们会讲到。

2.2、模板的实例化

与类的实例化相似,模板也有实例化,将参数传给模板而生成对应类型。

对于上面所写的Swap()函数,我们在创建模板的时候只写了一个自定义类型占位符,那么在传参的时候只能传同一类型的参数进去,即int和int,double和double,不能int和double混用。如果实在想混用两种不同的类型,有两种方法:

1、使用强制类型转换:

cpp 复制代码
#include<iostream>
using namespace std;

template <typename T>
T add(T a, T b) {
	return a + b;
}

int main() {
	int a = 1;
	double b = 2.5;
	auto ret = add((double)a, b);
	cout << ret << endl;
	
	return 0;
}

在使用函数模板的时候将int强制类型转换成double类型,这样子就能实现两种不同类型的数据进行相加了,同时他们的结果也会被自动推断出double:

现在讲的方法都属于隐式类型转换实例化,下面就来介绍一下第二种方法;

2、显示实例化:

显式实例化就是先将这个函数的返回值直接确定,让不属于这个返回值类型的数据自动转换成符合返回值的类型:

cpp 复制代码
#include<iostream>
using namespace std;

template <typename T>
T add(T a, T b) {
	return a + b;
}

int main() {
	int a = 1;
	double b = 2.5;
	//auto ret = add((double)a, b);
	auto ret = add<double>(a, b);
	auto ret2 = add<int>(a, b);
	cout << ret << endl;
	
	return 0;
}

直接在函数实例化的地方给函数类型进行表明,这样就能保证相加的类型是相同的了。

当然,还有一种邪修(bushi),就是直接修自定义类型参数名:

cpp 复制代码
#include<iostream>
using namespace std;

template<typename T,typename U>
auto add(T a, U b) {
	return a + b;
}

int main() {
	int a = 10;
	double b = 22.5;
	cout << add(a, b) << endl;
	return 0;
}

在返回值的部分让编译器自己决定返回什么类型的。

但是呢,显式实例化的真正用途并不在这里,而是在下面的场景中:

3、真正的使用场景:

当我们在传参是没有用到自定义类型占位符T,那就只能显式实例化了,像这样:

cpp 复制代码
template <typename T>
void Func(size_t n) {
	cout << n*2 << endl;
}

在Func()函数中没有用到类型T,这是后如果在main函数中想要直接调用,就会出现下面这种中编译错误的信息:

就是编译器推导不出T是什么类型的,但是函数中又必须要出现T类型的数据,所以这时候只能我们自己显式实例化:

cpp 复制代码
#include<iostream>
using namespace std;
template <typename T>
void Func(size_t n) {
	cout << n*2 << endl;
}

int main() {
	int a = 10;
	Func<int>(10);
	return 0;
}

2.3、函数模板的匹配原则

讲完了函数模板的实例化,现在我们就来讲讲函数模板的参数匹配规则。

当实现同个功能的函数但类型不同,与模板同时可以存在吗?像这样子:

cpp 复制代码
#include<iostream>
using namespace std;

//模板
template <typename T>
void Swap(T& a, T& b) {
	T temp = a;
	a = b;
	b = temp;
}

//整形交换函数
void Swap(int& a, int& b) {
	int temp = a;
	a = b;
	b = temp;
}

int main() {

	int a = 30, b = 40;
	Swap(a, b);
	cout << a << " " << b << endl;

	return 0;
}

整形的交换函数和模板同时存在,并且主函数中的变量又是整形,编译器会调用模板还是现成的整形交换函数?当然是现成的交换函数:

如果是double的话就只能调用模板了:

调用哪个编译器都能很好的给我们展示出来。

编译器会优先找更匹配的,比如:

cpp 复制代码
#include<iostream>
using namespace std;

//模板
template <typename T1,typename T2>
auto add(T1 a,T2 b){
	return a + b;
}

//整形加法
int add(int a, int b) {
	return a + b;
}
int main() {

	int a = 10;
	double b = 20.333;
	cout << add(a, b) << endl;
	return 0;
}

即使有现成的整形加法,但是使用模板会更加匹配,所以编译器会使用模板:

三、类模板

3.1、类模板的原理

类模板指的是一种模板,而模板类指的是通过类模板实例化出来的类。下面我们就来讲讲类模板

以前我们在实现像栈,队列,链表等数据结构的时候,通常只存储同一类型或者单一类型的数据:

cpp 复制代码
#include<iostream>
using namespace std;

//模板
template <typename T>
class Stack {
private:
	T* _top;
	size_t _size;
	size_t _capacity;
public:
	Stack(size_t n = 4)
		:_size(0),
		_top(new T[n]),
		_capacity(n)
	{
	}
};

int main() {

	Stack<int> st1;
	return 0;
}

之前的自己实现的数据结构都已经够用了,为什么还要在出类模板这么个东西呢?就是为了,同一种数据结构能够存储不同类型式数据,当我有了以上代码之后,我就能将double,int,char等类型的数据都放进同一种数据结构中:

cpp 复制代码
Stack<int> st1;     //存放int类型
Stack<double> st2;    //存放double类型
stack<char> st3;    //存放char类型

函数模板大多数适用多种类型,可以隐式实例化,但是类模板通常是针对一种类进行操作,做好显式实例化,通过这种方法就实现了我们之前说的:泛型编程。即有了一个蓝图就能实现各种各类型的数据操作。

3.2、类模板的使用

有了类模板这个好东西,我们就衍生出了STL中的容器,像<stack>,<queue>之类的数据结构,他们的本质就是类模板,通过传进来数据自动推导数据类型。以后写数据结构就能调用STL中的函数了,就不用自己像在C语言阶段那样自己造轮子了。

还有一点就是,以前我们写代码通常是将声明放在.h中,将定义放在.cpp中,但是类模板就不能将定义和声明分离,如果真想分离,那也只能在用一个文件中分离:

cpp 复制代码
#include <iostream>
using namespace std;

// 1. 类模板声明 + 定义(类体)
template <typename T>
class Stack
{
private:
    T* _data;
    int _top;
    int _capacity;

public:
    // 只在类里声明,不写实现
    Stack(int cap = 4);

    // 析构函数
    ~Stack();

    // 成员函数只声明
    void push(const T& val);
    T top() const;
    void pop();
    bool empty() const;
};

// -------------------------------------------------------
// 2. 类外实现成员函数(真正的"声明与定义分离")
// -------------------------------------------------------

// 构造函数
template <typename T>
Stack<T>::Stack(int cap)
{
    _capacity = cap;
    _data = new T[_capacity];
    _top = 0;
}

// 析构函数
template <typename T>
Stack<T>::~Stack()
{
    delete[] _data;
}

// push
template <typename T>
void Stack<T>::push(const T& val)
{
    _data[_top++] = val;
}

// top
template <typename T>
T Stack<T>::top() const
{
    return _data[_top - 1];
}

// pop
template <typename T>
void Stack<T>::pop()
{
    --_top;
}

// empty
template <typename T>
bool Stack<T>::empty() const
{
    return _top == 0;
}

// -------------------------------------------------------
// 3. main 测试
// -------------------------------------------------------
int main()
{
    Stack<int> st;

    st.push(10);
    st.push(20);
    st.push(30);

    cout << st.top() << endl;
    st.pop();
    cout << st.top() << endl;

    return 0;
}

这就是类模板的声明和定义分离的使用。

还有一点就是,类模板的模板参数可以实现缺省参数:

单一参数时:

cpp 复制代码
#include<iostream>
using namespace std;

template <typename T = int>
class A {
public:
	T x1;
	T x2;
		
};

int main() {

	A<> a1;    //不传入参数时就会默认为int类型
	A<double> a2;

	return 0;
}

当有多个参数时,就可以参考缺省参数那一块的知识:

cpp 复制代码
#include<iostream>
using namespace std;

//template <typename T1 = int ,typename T2 = double,typename T3 = int>  //全缺省
template <typename T1, typename T2 = int, typename T3 = double>
class B{

};

int main() {

	B<int> b1;
	B<int, int > b2;
	B<int, int, double> b3;

	return 0;
}

缺省值只能从右往左给。


那么以上就是本次所有的内容了

文章是自己写的哈,有什么描述不对的、不恰当的地方,恳请大佬指正,看到后会第一时间修改,感谢您的阅读~~~~


播主手写笔记:

相关推荐
过河卒_zh15667661 小时前
技术狂奔之后:数字虚拟人走向规则时代
人工智能·算法·aigc·生成式人工智能·算法备案
网域小星球2 小时前
C++ 从 0 入门(一)|C++ 基础语法、命名空间、引用、IO 输入输出
开发语言·c++·引用·命名空间·cin/cout
北顾笙9802 小时前
day27-数据结构力扣
数据结构
boss-dog2 小时前
3D视觉机器人中手眼标定的精度提升方法记录——ICP算法
算法·3d·机器人·手眼标定·icp
yashuk2 小时前
C语言中强制类型转换:不同数据类型间的转换方法与示例
c语言·强制类型转换·示例代码·注意事项·数据类型转换
雾岛听蓝2 小时前
Qt按钮与标签控件详解
开发语言·经验分享·笔记·qt
黑牛儿2 小时前
AI Agent\+PHP实现智能接口限流,避开算力成本陷阱(结合今日AI热点)
开发语言·人工智能·php
郝学胜-神的一滴2 小时前
Softmax 从入门到精通:多分类激活函数的优雅解法
人工智能·python·算法·机器学习·分类·数据挖掘
xianyinsuifeng2 小时前
C语言性能优化实战:从 printf 到 write,再到批量输出(性能提升30%+)
算法