【c++】模板初阶

发布于:2025-05-28 ⋅ 阅读:(14) ⋅ 点赞:(0)

hello~ 很高兴见到大家! 这次带来的是C++中关于模板这部分的一些知识点,如果对你有所帮助的话,可否留下你的三连呢?
个 人 主 页: 默|笙

在这里插入图片描述

一、泛型编程

1.1 什么是泛型编程?

泛型编程:泛型编程是一种编程范式,它允许在定义函数、类或数据结构时使用类型参数,这些参数可以在使用时被具体类型替换。泛型编程的核心目标是编写通用、可复用的代码,同时保持类型安全

  1. 即写代码时先用 “通用符号” 代替具体数据类型(比如用 T 代表任意类型),需要用到的时候再把这些“通用符号”换成实际类型(比如整数、字符等)。
  2. 模板正是实现泛型编程的基础,是实现它的具体语法工具

1.2 为什么要有泛型编程?

  1. 在泛型编程出现之前,我们可以通过函数重载为不同类型分别编写交换函数,但无法用同一个函数定义处理多种类型。

例如对整形之间、浮点型之间以及字符之间实现交换操作的交换函数:

void Swap(int& n1, int& n2)//对整数
{
	int tmp = n1;
	n1 = n2;
	n2 = tmp;
}
void Swap(double& n1, double& n2)//对浮点数
{
	double tmp = n1;
	n1 = n2;
	n2 = tmp;
}
void Swap(char& n1, char& n2)//对字符
{
	char tmp = n1;
	n1 = n2;
	n2 = tmp;
}
  1. 我们能够发现,上面三个重载函数除了类型不同之外,逻辑完全相同。这种重复编码不仅繁琐,而且当我们需要修改逻辑时需要对全部重载函数进行修改。那么我们能不能用某个“通用符号”去替代这些类型名称,等到我们要用的时候让编译器代替我们进行替换,从而实现代码复用?当然可以—>模板与泛型编程

函数重载的本质是为不同类型创建多个独立的同名函数,这只是语法层面的 “复用”,而非真正的代码复用。我们仍需写重复冗余代码,且维护成本极高(如修改逻辑时需要同步修改所有重载函数)。

二、模板(template)

我们从泛型编程那里可以了解到:模板是实现泛型编程的基础,是实现它的具体语法工具。接下来让我们走进模板:

  1. 模板就像是一个模具,它的外轮廓是固定的(函数框架),而且它本身不是我们需要的东西,但当我们将材料(具体类型)放入其中生产完成再拿出来时,它就是我们需要的产品(具体函数或类)了

就像是做石膏娃娃的模具:
在这里插入图片描述
倒入白色的膏体出现的自然是白色的石膏娃娃,同样,如果导入的是其他颜色的膏体出现的自然是不同颜色的石膏娃娃,它们相像但并不相同。

  1. 模板分为函数模板和类模板。

2.1 函数模板

2.1.1.函数模板概念

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

  1. 家族->具体成员

2.1.2函数模板格式

//模板参数声明:
template<typename T1, typename T2, typename T3,......,typename Tn>
返回类型 函数名(参数列表) {
   // 函数体,使用 T1,T2 等作为类型
}

typename是用来定义模板参数关键字,也可以使用class(切记:不能使用struct代替class)。

像是之前交换函数的代码,就可以这样写:

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

2.1.3函数模板的原理

  1. 函数模板它只是一个蓝图,一个空空的框架,它本身并不是函数,是编译器根据具体类型用来产生具体函数的模具,编译器将代替我们去书写重复代码。
  2. 对于模板函数的处理在编译器编译阶段,编译器会根据传入参数的类型推演生成具体的函数
    如:
    在这里插入图片描述

2.1.4 函数模板的实例化

函数模板的实例化是指编译器根据模板定义和实际调用时的类型参数,生成具体函数代码的过程。它分为隐式实例化和显式实例化。

1. 隐式实例化

由编译器根据传入实参对推演模板参数的具体类型

我们来看接下来的一段错误代码:

template<typename T>
T Add(T& n1, T& n2)
{
	return n1 + n2;
}
int main()
{
	int n1 = 1;
	float n2 = 2.2f;

	cout << Add(n1, n2) << endl;
	return 0;
}

发生报错的原因:

  1. 模板参数推导错误:传入参数第一个是整数类型,第二个是浮点数类型。编译器根据 n1 推导出 T = int,根据 n2 推导出 T = float(模板参数推导是并行,而非顺行),而 T 的值必须唯一,它无法同时匹配 int 和 float,编译失败。
  2. 引用绑定限制:即使允许类型转换(如将 float 转换为 int),转换过程中也会产生临时变量(具有常性),而函数参数为非 const 引用(T&),无法绑定临时值(权限不能放大),进一步导致编译错误。"。

修改方法:

  1. 增加模板参数
template<typename T, typename U>//增加模板参数,让 int 和 float 都能匹配上
T Add(T& n1, U& n2)
{
    return n1 + n2;
}
int main()
{
    int n1 = 1;
    float n2 = 2.2f;

    // 自动推导:T=int, U=float
    cout << Add(n1, n2) << endl;

    return 0;
}

  1. 强制转换类型
template<typename T>
T Add(const T& n1, const T& n2)//加上const用来修饰,如此可绑定临时变量
{
    return n1 + n2;
}
int main()
{
    int n1 = 1;
    float n2 = 2.2f;

    // 自动推导:T=int, U=float
    cout << Add(n1, (int)n2) << endl;//强制转换为 int 类型,形成临时变量
     return 0;
}
  1. 使用显式实例化(在下面的显式实例化里)。
  2. 去掉函数参数里的引用符号,避免绑定问题,虽然会产生额外拷贝开销。
2. 显式实例化

由我们用户主动在函数名后的<>中指定模板参数的实际类型

template<typename T>
T Add(const T& n1, const T& n2)
{
    return n1 + n2;
}
int main()
{
    int n1 = 1;
    float n2 = 2.2f;

    // 自动推导:T=int, U=float
    cout << Add<int>(n1, n2) << endl;
    return 0;
}
  • 倘若 n1, n2 类型不同,编译器会先尝试进行隐式类型转换为指定类型,转换失败编译报错。

2.1.5 模板函数的匹配原则

  1. 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数。
  2. 非模板函数允许实参和形参之间隐式类型转换,模板函数则不允许,它要求实参和形参的精准匹配
  3. 对于同名的非模板函数与模板函数,如果非模板函数的参数与调用实参完全匹配(不用类型转换),则会优先调用非模板函数,不会从模板函数里产出一个实例。若需要需要转换才能匹配非模板函数,而模板函数又能产出一个匹配度更高的函数,那么编译器会选择函数模板的实例化版本
//int类型加法函数
int Add(int n1, int n2)
{
	cout << "int Add(int n1, int n2)" << endl;
	return n1 + n2;
}

//模板加法函数
template<typename T, typename U>
T Add(T n1, U n2)
{
	cout << "T Add(T n1, U n2)" << endl;
	return n1 + n2;
}
int main()
{
	Add(1, 2);//实参完全与非模板函数匹配,不会从模板函数里产出一个实例
	Add(1, 2.2);//第二个实参需要进行隐式转换才能匹配上非模板函数,
	//而模板函数能够产出一个匹配度更高的函数,选择模板函数的实例化版本
	return 0;
}

在这里插入图片描述

2.2 类模板

2.2.1 类模板格式

template<typename T1, typename T2, ..., typename Tn>
class 类模板名
{
	// 类内成员定义,可以使用T1,T2,...,Tn作为类型
};

2.2.2 类模板的实例化

类模板实例化必须显式实例化,在类模板名字后跟<>,然后将实例化的类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。

  • 类模板名<实际类型1, 实际类型2, …, 实际类型n> 对象名;//实际类型一一对应类参数
template<typename T>
class Stack { /* ... */ };
//对于实例化:Stack只是类模板名,Stack<int>才是具体的类型名
Stack<int> intStack;   // 显式实例化为 int类型的 Stack

今天的分享就到此结束啦,如果对读者朋友们有所帮助的话,可否留下宝贵的三连呢~~
如果可以, 那就让我们共同努力, 一起走下去!