21天学会C++:Day14----模板

news2024/12/27 15:03:53

· CSDN的uu们,大家好。这里是C++入门的第十四讲。
· 座右铭:前路坎坷,披荆斩棘,扶摇直上。
· 博客主页: @姬如祎
· 收录专栏:C++专题

目录

1. 知识引入

2. 模板的使用

2.1 函数模板

2.2 类模板

3. 模板声明和定义分离

3.1 同一文件中的声明与定义分离

3.2 分文件的声明与定义分离

4. 非类型模板参数

5. 模版的特化

5.1 模板的全特化

5.2 模板的偏特化

6. 模板总结


1. 知识引入

有一天,我们在写C语言程序的时候,想要交换两个数的值,于是我们很快就写了一个交换两个整形变量的函数:

void Swap(int x, int y)
{
    int tmp = x;
    x = y;
    y = tmp;
}

但是写了一会代码,你发现你又要交换两个double的值,你又要重新写一个Swap double的函数。假如后来你还需要交换其他类型的变量,那么你就需要写更多的Swap函数。是不是偷一下子就变大了。不过别慌,C++带着新的语法走来了!

2. 模板的使用

听到模板这个名词,我们就想到了显示生活中的模具,通过一个模具我们就能制作出很多产品。同理通过一个模板,我们就能实现很多功能,满足你的各种需求!C++的模板是泛型编程的基础,所谓泛型编程:编写与类型无关的通用代码,是代码复用的一种手段

我们来看看模板的语法:

template <typaname T1, typename T2, ··· , typaname TN>

函数/类

 模板通过关键字 template 来定义,template 后面紧跟一个 尖括号 , 尖括号中加上关键字 typename (typename 换成class也行) 然后跟上模板参数,其中尖括号中的写法与函数的形参列表极为相似。其中的T1,T2 ··· 叫做模板参数,名字可以随意更改。根据template下方定义的类型,模板可以分为函数模板类模板

2.1 函数模板

我们先来看看函数模板的用法吧:我们就拿上面的Swap函数来试试吧,看看有了函数模板能省事多少!

//定义函数模板
template<typename T>
void Swap(T& x, T& y)
{
	T tmp = x;
	x = y;
	y = tmp;
}

模板起始就是将类型参数化,在上面的代码中我们将Swap函数的参数类型用模板参数代替,当我们调用函数时,编译器会根据传入参数的类型,自动为T实例化出对应的类型!

通过调试发现,即使我们传入不同类型的参数,也能够做到交换两个变量的值。

你可能会好奇,编译时怎么做到的呢?其实是这样的:当你使用 int 类型调用 Swap 函数那么编译器就会根据函数模板生成一个参数类型为 int 的函数,当你使用 double 类型调用 Swap 函数那么编译器就会根据函数模板生成一个参数类型为 double 的函数。你可能会说,这和直接些两个函数没有什么区别啊!但事实时,我们只写了一个函数模板,多余的事儿我们都交给了编译器,这不香吗?

通过观察汇编代码,可以看到确实是生成了连个不同参数类型的函数:

在C++中,通过函数模板生成函数的过程我们叫做模板的实例化。

上面我们使用函数模板的方式叫做隐式实例化 ,即不指定模板参数的类型,编译器根据参数的类型自动推导模板参数的类型。

但是并不是所有的情况都能通过隐式实例化来完成,那个时候就必须显示实例化啦:

template<typename T>
T* alloc(size_t n)
{
	return new T[n];
}

int main()
{
	int* a1 = alloc(10);

	//显示实例化
	int* a2 = alloc<int>(10);
	return 0;
}

隐式实例化是会报错的,因为他无法通过你传入的参数推导出模板参数T的实际类型。 

2.2 类模板

类模板和函数模板差不多,只不过定义函数的地方改成定义类。

在下面的代码中,我们定义了一个名为Stack的模板类,根据模板实例化时传入的模板参数的类型不同,我们就能实例化出来栈内元素类型不同的栈。在C语言中我们只能通过 typedef 来实现变换栈内元素的类型,但是确做不到在一个工程中同时使用 一个数据元素是 int 的栈, 一个数据元素是 double 的栈(除非你不嫌麻烦,赋值一份栈的代码)。但是有了类模板就能轻松做到。

template<typename T>
class Stack
{
public:

private:
	T* _a;
	int _size;
	int _capacity;
};

int main()
{
	Stack<int> st1;
	Stack<double> st2;
}

我们可以看到:类模板在实例化的时候就只能显示实例化了!没法隐式实例化呢!即使你能够通过类中的构造函数推导出模板参数的类型 ,也不能隐式实例化呢!这是为什么呢?第 3 点会给你答案。

3. 模板声明和定义分离

3.1 同一文件中的声明与定义分离

 我们来看全局的函数模板的声明与定义分离该怎么书写:

//函数模板的声明
template<typename T>
void Swap(T& x, T& y);


//函数模板的定义
template<typename T>
void Swap(T& x, T& y)
{
	T tmp = x;
	x = y;
	y = tmp;
}

在声明的时候需要有 template<typename T> 在定义的时候也需要有 template<typename T> 因为模板参数只能在它下面的第一个函数或者类中使用。

相比全局函数实现 声明与定义分离,我们更喜欢用的是类的成员函数的声明与定义分离:

我们在类中声明了一个push函数,虽然我们在外面实现了push函数,但是编译器依然报错,说明我们实现的方式有问题,你可能会说加一个类域?很棒,但是还是不完全正确!在解决这个问题之前还需要铺垫一个知识:普通类的类名即是一个类型!但是模板类的类名还是一个类型吗?我都这样说了,你肯定知道不是啦!的确不是,模板类的类型需要显示指定模板参数才是该类的类型。

像这样:Stack<int> 这就是一个Stack类型。

ok,我们现在大概能猜出类成员函数的声明与定义分离应该怎么写了吧:使得还需要在函数名的前面加上这个类的类型才行 。

template<typename T>
class Stack
{
public:
	
	int size()
	{
		return _size;
	}

	void push(const T& val);


private:
	T* _a;
	int _size;
	int _capacity;
};

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

像上面这样我们就实现了类成员函数的声明与定义分离了呢!在实际的编程中,我们习惯将那些短小的函数直接在类内定义(默认就是内联函数了),那些比较长的函数实现声明与定义分离。 

现在我们就知道为什么模板类不可能隐式实例化了嘛,因为模板类的类名不是类型,必须指定模板参数后才是类型,只有用类型才能定义变量!

3.2 分文件的声明与定义分离

我们在写C语言的时候就喜欢将函数的定义与声明分文件编写嘛!现在我们来看看模板类的成员函数如果声明与定义分文件编写会发生什么:

//
  test.h /
//
#pragma once
template<typename T>
class Stack
{
public:

	Stack(int capacity = 4)
	{
		_a = new T[capacity];
		_size = 0;
		_capacity = capacity;
	}

	int size()
	{
		return _size;
	}

	void push(const T& val);

	void pop();

private:
	T* _a;
	int _size;
	int _capacity;
};

///
///  test.cpp /
///

#include"test.h"

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

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

///
///  main.cpp /
///
#include<iostream>
#include"test.h"

using namespace std;

int main()
{
	Stack<int> st;

	st.size(); // 不会出问题

	st.pop();
	st.push(1);

}

还有一个奇怪的事儿就是当你注释掉 push 和 pop 函数的调用就不会报错了!这是因为 模板 函数会按需实例化 当你没有调用这个函数时 编译器是不会实例化出来对应的函数的! 

我们发现调用 push 和 pop 函数会报链接错误。这是为啥呢?链接时错误一般都是在函数有声明,没有定义的时候出现的,但是我在 test.cpp 确实是定义了 push 和 pop 函数的啊!

我们慢慢来分析,size函数没有报错是因为,size函数在声明的时候直接就定义了,编译的时候就能直接确定函数的地址。但是对于 push 和 pop 函数,因为他们的定义在另一个文件,只有在链接的时候才能确定函数的地址,当链接的时候去找 push 和 pop 函数的地址没找到,因此报了链接错误。

为什么就没找到呢?

是因为我们的 push 与 pop 的实现在另一个文件,在模板函数所在的cpp文件,不知道模板参数的具体类型,编译器不知道该实例化什么模板参数是什么类型的函数,从而无法为这两个函数确定函数地址。链接的时候自然就找不到这两个函数的地址了!

该怎么解决这个问题呢?

我们可以在 push 与 pop 所在的文件中,显示实例化模板参数,告诉编译器模板参数的类型:

告诉编译器帮我实例化模板参数为int 的函数,但是如果我们用到了 Stack<double> 那么我们还需要在这个文件中显示实例化模板参数为 double的函数!

因此 在实际的编程中我们更喜欢将模板类的类成员函数的声明与定义放在同一个文件里面!有的人为这样的文件取名为 .hpp 文件用来标识这是一个模板类!

为什么在同一个文件里面实现定义与分离就不会报错呢?我们在cpp文件中使用这个类,都需要包含这个类的头文件,包含这个头文件之后,我们就在一个cpp文件里面同时有了函数的声明与定义。当我们使用这个模板类,肯定会传入模板参数,从而确定了模板参数的类型,编译过程中,那些定义的函数就知道了模板参数的类型,只需要根据模板参数的类型实例化函数即可!

4. 非类型模板参数

模板的参数不仅仅可以通过< typename T> 将类型参数化。还允许使用整形值充当模板参数!(这里的整形值指的是整形家族,例如 int,char,unsigned int 等)

这有什么作用呢?

现在需要你实现一个静态栈,并且要求多个静态栈的大小要不相同!你会怎么做呢?使用#define 栈的大小能解决问题嘛?显然#define 和上面的typename 陷入了一样的困境,当实例化多个时都无法实现我们的需求,那看看非类型模板参数是怎么做的吧:

template<typename T, size_t N>
class Stack
{
public:


private:
	T _a[N];
	int _size;
	int _capacity;
};

int main()
{
	Stack<int, 100> st1; // 空间大小为 100 的静态栈
	Stack<int, 10> st2; // 空间大小为 10 的静态栈

}

其中那个 N 就是非类型的模板参数, 观察到 N 可以直接用来当作数组的大小。因此这个 N 是一个常量哦!不允许被修改。

下面补充一下 typename 的另一层用途:

我定义了一个类:List,然后 List<T> 中将 ListNode<T> typedef 一下。在类 B 中,我们尝试去取List<T> 中的 Node 来定义一个变量,发现报出了编译错误,这是为什么呢?

原因就在于:向模板类里面取东西,编译器无法确定你取的东西是一个类型还是一个对象(例如:静态成员变量),假设你取的是一个类型,那么 List<T>::Node _node;就不会报错;单如果你取的是一个对象,这条语句就是有问题的!所以为了明确你取的东西,需要加上typename告诉编译器,你取的是一个类型! 这个语法在我后面实现STL容器时会用到!

5. 模版的特化

来看下面的代码:我们实现了一个打印的函数模板,传入什么值就打印什么值,于是我们写出了这样的代码:

template<typename T>
void Print(const T& val)
{
	cout << val << endl;
}

打印都没有问题,但是我有这样一个需求,就是当你传入指针的时候,我希望打印的是指针指向的内容而不是打印指针本身,这个时候应该怎么做呢?这就要使用我们的模板特化了!

通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结 果,需要特殊处理,比如:实现了一个专门用来进行小于比较的函数模板

5.1 模板的全特化

顾名思义全特化就是将模板参数全部都特化成具体的类型。

函数模板的特化步骤:

1. 必须要先有一个基础的函数模板

2. 关键字template后面接一对空的尖括号<>

3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型

4. 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。

比如上面的例子:我们将模板参数T特化,当传入指针类型的时候,我们打印指针指向的内容。

template<typename T>
void Print(const T& val)
{
	cout << val << endl;
}

template<typename T>
void Print(T* val)
{
	cout << (*val) << endl;
}


int main()
{
	Print(5);

	int a = 10;
	Print(&a);

	double b = 20.5;
	Print(&b);
}

注意:一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给出。 该种实现简单明了,代码的可读性高,容易书写,因为对于一些参数类型复杂的函数模板,特化时特别给出,因此函数模板不建议特化 

我们来看看模板类的全特化:

template<class T1, class T2>
class Show
{
public:
	Show()
	{
		cout << "Show(T1, T2)" << endl;
	}
};


template<>
class Show<int, int>
{
public:
	Show()
	{
		cout << "Show(int, int)" << endl;
	}
};

template<>
class Show<int, double>
{
public:
	Show()
	{
		cout << "Show(int, double)" << endl;
	}
};


int main()
{
	Show<double, double> s1;
	Show<int, int> s2;
	Show<int, double> s3;
}

我们看到我们写了特化之后就能针对指定的类型进行特殊处理了:

5.2 模板的偏特化

模板的偏特化,就是值针对一部分模板参数进行特化:

template<class T1, class T2>
class Show
{
public:
	Show()
	{
		cout << "Show(T1, T2)" << endl;
	}
};


template<class T1>
class Show<T1, int>
{
public:
	Show()
	{
		cout << "Show(T1, int)" << endl;
	}
};

template<class T1>
class Show<T1, double>
{
public:
	Show()
	{
		cout << "Show(T1, double)" << endl;
	}
};


int main()
{
	Show<double, double> s1;
	Show<int, int> s2;
	Show<int, double> s3;
}

 总之,模板的特化能够使得我们更加方便的处理特殊化的情况。这一点在我们实现STL中的优先级队列会提到!

6. 模板总结

【优点】

1. 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生。

2. 增强了代码的灵活性。

【缺陷】

1. 模板会导致代码膨胀问题,也会导致编译时间变长。其实这个问题不可避免,如果没有模板,那么就需要你手写这么多的代码了!

2. 出现模板编译错误时,错误信息非常凌乱,不易定位错误。(这个是真的令人头大!)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1040092.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

7实体与值对象 #

本系列包含以下文章&#xff1a; DDD入门DDD概念大白话战略设计代码工程结构请求处理流程聚合根与资源库实体与值对象&#xff08;本文&#xff09;应用服务与领域服务领域事件CQRS 案例项目介绍 # 既然DDD是“领域”驱动&#xff0c;那么我们便不能抛开业务而只讲技术&…

Kotlin异常处理runCatching,getOrNull,onFailure,onSuccess(1)

Kotlin异常处理runCatching&#xff0c;getOrNull&#xff0c;onFailure&#xff0c;onSuccess&#xff08;1&#xff09; fun main(args: Array<String>) {var s1 runCatching {1 / 1}.getOrNull()println(s1) //s11&#xff0c;打印1println("-")var s2 ru…

基于springboot+vue的制造装备物联及生产管理ERP系统

博主主页&#xff1a;猫头鹰源码 博主简介&#xff1a;Java领域优质创作者、CSDN博客专家、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战 主要内容&#xff1a;毕业设计(Javaweb项目|小程序等)、简历模板、学习资料、面试题库、技术咨询 文末联系获取 项目介绍…

序列化对象(ObjectOutputStream,ObjectInputStream)

1、对象序列化 作用&#xff1a;以 内存 为基准&#xff0c;把内存中的对象存储到磁盘文件中去&#xff0c;称为对象序列化使用到的流是对象字节输出流&#xff1a;ObjectOutputStream package com.csdn.d7_serializable; import java.io.*; public class ObjectOutputStreamDe…

Python爬虫程序设置代理常见错误代码及解决方法

Python爬虫程序设置代理是爬虫程序中常用的技巧&#xff0c;可以有效地绕过IP限制&#xff0c;提高爬虫程序的稳定性和效率。然而&#xff0c;在设置代理时&#xff0c;常会出现各种错误代码&#xff0c;这些错误代码可能会影响程序的正常运行&#xff0c;甚至导致程序崩溃。本…

Serverless 数仓技术与挑战(内含 PPT 下载)

近期&#xff0c;Databend Labs 联合创始人张雁飞发表了题为「Serverless 数仓技术与挑战」的主题分享。以下为本次分享的精彩内容&#xff1a; 主题&#xff1a; 「Serverless 数仓技术与挑战」 演讲嘉宾&#xff1a; 张雁飞 嘉宾介绍&#xff1a; Databend Labs 联合创始人…

Jenkins Job的Migrate之旅

场景 使用Jenkins 做为应用的定时任务处理&#xff0c; 在上面建立的800个左右的Job, 这个环境运行了很多年&#xff0c; 当初安装的最新版本是Jenkins 1.642.3&#xff0c; 现在因为OS需要升级等原因&#xff0c; 驻在上面的Jenkins 服务器也需要一并升级&#xff0c;在新的服…

【DevOps】Docker 容器及其常用命令

Docker 容器及其常用命令 1.容器2.Docker 常用命令2.1 Docker 环境信息2.2 容器生命周期管理2.2.1 docker run2.2.2 docker start / stop / restart 2.3 镜像仓库命令2.3.1 docker pull2.3.2 docker push 2.4 镜像管理2.4.1 docker images2.4.2 docker rmi / rm 2.5 容器运维操…

一键制作纯文字背景视频,让你轻松制作短视频!

你是否曾经被繁琐的视频制作流程所困扰&#xff1f;不用担心&#xff0c;现在有了纯文字视频制作神器&#xff0c;让你成为视频制作大师的梦想不再遥远&#xff01; 首先&#xff0c;让我们来看看纯文字视频制作神器主要的功能之一&#xff1a;一键生成短视频。只需输入你的文…

已解决: Microservice Error: Timeout Error: Service didn‘t respond in time

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页: &#x1f405;&#x1f43e;猫头虎的博客&#x1f390;《面试题大全专栏》 &#x1f995; 文章图文并茂&#x1f996…

ubuntu18.04安装docker

ubuntu18.04安装docker 文章目录 ubuntu18.04安装docker一.安装1.更新软件库索引2.安装一些必要的软件包3.添加Docker的官方GPG密钥4.添加Docker软件库5.再次更新软件库索引6.安装Docker CE7.启动Docker并设置开机启动8.验证Docker安装9.(若要让非root用户可以运行Docker命令)可…

【广州华锐互动】马属直肠检查3D虚拟仿真课件

随着科技的发展&#xff0c;医疗行业也在不断地进行创新。其中&#xff0c;广州华锐互动开发的马属直肠检查3D虚拟仿真课件&#xff0c;为医学教育和实践操作带来了新的可能性。它不仅可以帮助医生提高诊断准确率&#xff0c;还可以让医学生在没有真实病人的情况下进行实践操作…

admin后台管理

admin后台管理 django 提供了比较完善的后台管理数据库的接口&#xff0c;可供开发过程中调用和测试使用 django 会搜集所有已注册的模型类&#xff0c;为这些模型类提拱数据管理界面&#xff0c;供开发者使用 admin配置步骤 创建后台管理帐号- 该账号为管理后台最高权限账号…

删除表中的数据

MySQL从小白到总裁完整教程目录:https://blog.csdn.net/weixin_67859959/article/details/129334507?spm1001.2014.3001.5502 语法格式: delete from 表名 where 记录匹配条件; 说明&#xff1a;如果**不写where**子句&#xff0c;表示无条件&#xff0c;删除表中的**所有记…

9月25日星期一,今日早报简报微语报早读

9月25日&#xff0c;星期一&#xff0c;早报简报微语早读分享。 1、祝贺中国队&#xff01;开幕首日中国队20金7银3铜&#xff0c;共计30枚奖牌&#xff0c;位列奖牌榜第一名&#xff1b; 2、NBL深蓝官宣&#xff1a;陕西罢赛遭重罚 罚款100万取消评奖资格&#xff1b; 3、中…

NPDP是什么?考了有用吗?

1&#xff09;NPDP是什么&#xff1f; NPDP&#xff0c;全称为New Product Development Professional&#xff0c;即新产品开发专业人员。NPDP认证是由世界产品开发协会&#xff08;PDMA&#xff09;推出的一项专业认证。它旨在评估和认可个人在新产品开发领域的专业知识和技能…

github代码提交过程详细介绍

1、下载github上面的代码 &#xff08;1&#xff09;在github网站上&#xff0c;找到想要下载的代码仓库界面&#xff0c;点击Code选项就可以看到仓库的git下载地址&#xff1b; &#xff08;2&#xff09;使用命令下载&#xff1a;git clone 地址&#xff1b; 2、配置本地git…

基于YOLOv8模型的垃圾满溢检测系统(PyTorch+Pyside6+YOLOv8模型)

摘要&#xff1a;基于YOLOv8模型的垃圾满溢检测系统可用于日常生活中检测与定位车辆垃圾&#xff08;garbage&#xff09;、垃圾桶&#xff08;garbage_bin&#xff09;和垃圾满溢&#xff08;overflow&#xff09;目标&#xff0c;利用深度学习算法可实现图片、视频、摄像头等…

离散小波变换(概念与应用)

目录 概念光伏功率预测中,如何用离散小波变换提取高频特征概念 为您简单地绘制一些示意图来描述离散小波变换的基本概念。但请注意,这只是一个简化的示意图,可能不能完全捕捉到所有的细节和特性。 首先,我将为您绘制一个简单的小波函数和尺度函数的图像。然后,我会提供一…