👑作者主页:@安 度 因
🏠学习社区:StackFrame
📖专栏链接:C++修炼之路
文章目录
- 一、泛型编程
- 二、函数模板
- 1、概念
- 2、格式
- 3、函数模板实例化
- ① 隐式
- ② 显式
- 4、特性
- 五、类模板
- 六、模板使内置类型"升级"
如果无聊的话,就来逛逛 我的博客栈 吧! 🌹
一、泛型编程
C 语言为什么不提供数据结构?因为不支持泛型编程。对于一个栈,它可能只支持存取 int 类型,它并不支持 泛型 – 广泛类型 的存取。
看一个交换函数:
void Swap(int& x1, int& x2)
{
int tmp = x1;
x1 = x2;
x2 = tmp;
}
void Swap(double& x1, double& x2)
{
double tmp = x1;
x1 = x2;
x2 = tmp;
}
int main()
{
int a = 0, b = 1;
double c = 1.1, d = 2.2;
return 0;
}
C++ 对于不同类型的数据交换,可以使用函数重载,实现不同的函数,来交换。
但是每增加一个类型的交换,就得再写一个 Swap ,是不是很麻烦?而且若一个重载函数写错,其他可能也都会写错。那这样好吗?并不好。
那能否 告诉编译器一个模子,让编译器根据不同的类型利用该模子来生成代码呢 ?
生活中,我们浇筑的时候,可以使用不同的颜色填充浇筑目标:
而这些交换函数的区别就是类型,类型就好比是颜色,而函数就是模具。
如果在C++中,也能够存在这样一个模具,通过给这个模具中填充不同材料(类型),来获得不同材料的铸件(即生成具体类型的代码),那将会节省许多头发 。巧的是前人早已将树栽好,我们只需在此乘凉。
泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。
而模板,分为两种:
二、函数模板
1、概念
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
2、格式
template<typename T1, typename T2,…,typename Tn> or template
它们有区别,但是在我们接下来的举例中没有区别,其他点之后我们再看。
改造:
(一般叫 T – typename )
3、函数模板实例化
这三个地方调用的是同一个函数吗?不是,因为它们调用函数的类型不一样,指令也就不一样。
它只是一个模具,供别人使用,它并不是说共用一份函数,而是类型可以通过这个模板,来进行不同类型的函数调用。
对于一个函数模板只能对于一个函数,对于下一个函数,则不支持该模板。
① 隐式
这一过程就叫模板实例化 :
在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然后产生一份专门处理double类型的代码,对于字符类型也是如此。
实际调用的是实例化后的函数 。
汇编验证:
本质没变,但是我们省劲了(只管调用,不管生成)。
② 显式
但是有时候由于类型的原因,隐式不能推导成功:
a1 是 int ,d1 是 double ,矛盾了。
这时可以强转:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-03p7lU3j-1689668344405)(https://anduin.oss-cn-nanjing.aliyuncs.com/image-20230208122238842.png)]
但是有些取巧,所以我们一般使用 显式实例化 :
这种方式,会根据 <>
中的类型,进行实例化。调用时会将参数隐式类型转换为实例化后的参数类型。
4、特性
1)模板参数能写多个 :
template<class T1, class T2>
T1 Add(const T1& left, const T2& right)
{
return left + right;
}
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.0, d2 = 20.0;
Add(a1, d1);
return 0;
}
但是返回值的类型就起了决定性的作用:
比如 Add(a1, d1) 此刻的 T1 是 int ,T2 是 double ,计算时,结果为 double ,但是返回时,返回的是 T1 ,所以又会进行隐式类型转换。需要考虑清楚。
2)函数模板和普通函数可以同时出现,普通函数优先级高 :
因为普通函数可以直接调用;但是函数模板需要推演实例化,再调用。
五、类模板
对于 C 来说,可以通过 typedef
来改变栈中元素类型:
typedef int DataType;
typedef double DataType;
class Stack
{
public:
Stack(int capacity = 4)
:_top(0)
, _capacity(capacity)
{
_a = new int[capacity];
}
~Stack()
{
delete[] _a;
_a = nullptr;
_capacity = _top = 0;
}
private:
DataType* _a;
int _top;
int _capacity;
};
int main()
{
return 0;
}
但是对于这种情况:
Stack st1; // int 类型
Stack st2; // double 类型
这样是不行的,就算 typedef
也不管用。不能同时存储两个类型 。对于这种方式,只能写两个栈,一个存 int ,一个存 double :
class Stackint
{
public:
// ...
private:
int* _a;
// ...
};
class Stackdouble
{
public:
// ...
private:
double* _a;
// ...
};
int main()
{
Stackint st1; // int
Stackdouble st2; // double
}
这样子就很繁琐且冗余,但是没有办法,C 并不存在模板。C++ 为了解决这个问题,就诞生了 类模板 :
template<class T> // 类模板
class Stack
{
public:
Stack(int capacity = 4)
:_top(0)
, _capacity(capacity)
{
_a = new T[capacity];
}
~Stack()
{
delete[] _a;
_a = nullptr;
_capacity = _top = 0;
}
private:
T* _a;
int _top;
int _capacity;
};
int main()
{
Stack<int> st1; // int
Stack<double> st2; // double
return 0;
}
无法推演,因为是创建对象,而并不是函数调用,所以需要显式实例化指定:Stack<int> st1
,Stack 是类名,Stack 是类型
声明和定义分离 :
template<class T>
class Stack
{
// ...
};
template<class T>
void Stack<T>::Push(const T& x) // Stack<T> 指定
{}
int main()
{
Stack<int> st1;
st1.Push(1);
}
当声明和定义分离时,一个模板只能给一个函数用,但是对于类中,一个模板参数对于整个类都能用。
没写模板:
写模板:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KUhKA8ON-1689668344406)(https://anduin.oss-cn-nanjing.aliyuncs.com/image-20230522125554519.png)]
六、模板使内置类型"升级"
当有了模板之后,对于内置类型进行了升级,内置类型也具有了默认构造函数。
vector(size_t n, const T& val = T())
对于内置类型,为了与自定义类型的行为保持一致,使用默认构造函数是必要的。尽管内置类型没有显式的构造函数,但在使用模板时,为了满足容器类型的要求,需要提供一个通用的初始化方式。
使用 T() 作为默认构造函数的默认参数,可以确保在使用容器类型时,无论是自定义类型还是内置类型,都能够正常进行对象的初始化。
这里显示的构造函数泛指 —— 没有代码编写的构造函数:
内置类型没有像自定义类那样通过代码编写的构造函数。在C++中,自定义类可以定义自己的构造函数来控制对象的初始化过程,但内置类型没有这种能力。
内置类型的初始化是由编译器隐式处理的。编译器在声明内置类型的变量时,会自动执行适当的初始化。例如,整数类型会被初始化为0,浮点数类型会被初始化为0.0,布尔类型会被初始化为false这种形式。