目录
模版的引入
泛型编程
模板的概念
模板的使用
函数模版
函数模板概念
函数模板格式
函数模板的原理
函数模板的实例化
模板参数的匹配原则
类模版
类模板的定义格式
类模板的由来
类模板的实例化
模板的总结
模版的引入
如下代码,我们想实现交换两个值,如果我想交换多种类型呢?
众所周知,C语言中不支持函数重载,如果想实现不同类型的交换,就需要写很多不同的函数。
在C++中,我们如果想实现不同类型的交换,就需要写多个函数重载,如果想交换其它类型的还要写相同类似的代码。每次写一个不同的类型的交换就要写一个相同类似的函数,写了很多不同类似的代码,这样就显得很挫,所以C++就专门设计了模板,使得变成更加高效。
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
void Swap(double& left, double& right)
{
double temp = left;
left = right;
right = temp;
}
void Swap(char& left, char& right)
{
char temp = left;
left = right;
right = temp;
}
int main()
{
int a = 0, b = 1;
Swap(a, b);
double c = 1.11, d = 2.22;
Swap(c, d);
char e = 'A', f = 'Z';
Swap(e, f);
cout << a << " " << b << endl;
cout << c << " " << d << endl;
cout << e << " " << f << endl;
return 0;
}
泛型编程
模板的概念
模板是一种编程工具,允许您创建通用的、可复用的代码,能够处理不同的数据类型。
生活中的例子就可以形象的说明模板的概念:
有一个模具(模板)决定了冰块的形状,可能是正方体或者圆柱体 (这些不同的形状的模具代表不同功能的函数模板,除了交换值的函数,还有容器、算法、类模板)。要制作普通的水冰块,就把水倒入模具;要制作果汁冰块,就把果汁倒入模具。水和果汁就是不同的类型参数。
模板的使用
函数模板和类模板的使用方式有相似之处,首先我们来看函数模板的使用。以下这个模板函数能够实现多种类型的数据交换,这充分体现了泛型编程的理念,下面会详细介绍模板的具体使用方法。
//模板 ->写根类型无关的代码
template<class T>
void Swap(T& x1, T& x2)
{
T x = x1;
x1 = x2;
x2 = x;
}
int main()
{
int a = 0, b = 1;
Swap(a, b);
double c = 1.11, d = 2.22;
Swap(c, d);
char e = 'a', f = 'b';
Swap(e, f);
cout << a << " " << b << endl;
cout << c << " " << d << endl;
cout << e << " " << f << endl;
return 0;
}
泛型编程:
在这个示例中,Swap
函数通过模板实现了对不同类型数据的交换操作,而无需为每种具体类型单独编写一个交换函数。它能够处理 int
、double
、char
等多种类型,体现了泛型编程的思想,即编写与具体类型无关、通用且可复用的代码。
编写与类型无关的调用代码,是代码复用的一种手段,模板是泛型编程的基础。
函数模版
函数模板概念
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本
函数模板格式
template<typename T1, typename T2, , , , , , , typename Tn>
返回值类型 函数名(参数列表)
{}
template<class T> //或者 template<typename T> 模板参数定义类型,这里的T相对于类型的名称可以随便取,一般取T(type)
void Swap(T& x1, T& x2)
{
T x = x1;
x1 = x2;
x2 = x;
}
注意:typename 是用来定义模板参数关键字,也可以使用class,通常使用class (切记:不能使用struct代替class)。
函数模板的原理
下面调用的是否是同一个函数
template<class T>
void Swap(T& x1, T& x2)
{
T x = x1;
x1 = x2;
x2 = x;
}
int main()
{
int a = 0, b = 1;
Swap(a, b);
double c = 1.11, d = 2.22;
Swap(c, d);
char e = 'a', f = 'b';
Swap(e, f);
cout << a << " " << b << endl;
cout << c << " " << d << endl;
cout << e << " " << f << endl;
return 0;
}
在反汇编中,我们会看到每次 Swap
调用对应的是不同的函数指针,指向为特定类型生成的不同函数实现。(调用的都不是同一个函数)
所以模板的原理是什么:
我们写了模板,编译器通过模板实例化出对应的函数或者类,编译器不会编译模板,编译器编译模板实例化出的对应的函数或者类 (编译器只会编译实例化的代码,不会编译模板(模具))。
在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数
以供调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然后产生一份专门处理double类型的代码,对于字符类型、整型类型也是如此。
注意:早期的编译器,写好了模板,在不使用模板的情况下,编译器不编译模板(模具),但是模板的架子不能有问题,模板里面的代码实现编译器不管,也不会去检查里面的语法。
现在的编译器,即使模板没有被实际使用,编译器也可能会对模板的语法进行一定程度的检查,以尽早发现潜在的问题。
函数模板的实例化
用不同类型的参数使用函数模板时,称为函数模板的实例化。
模板参数实例化分为:隐式实例化和显式实例化。
- 1,隐式实例化:让编译器根据实参推演模板参数的实际类型
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.1, d2 = 20.2;
Add(a1, a2); //隐式实例化
Add(d1, d2); //隐式实例化
Add(a1, d1); //这一行编译出错
return 0;
}
Add(a1,d1); 该语句不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演其实参类型 通过实参a1将T推演为int,通过实参d1将T推演为double类型,但模板参数列表中只有一个T, 编译器无法确定此处到底该将T确定为int 或者 double类型而报错 。
注意:在模板中,编译器一般不会进行类型转换操作。
此时有两种处理方式:1. 用户自己来强制转化
2. 使用显式实例化 Add(a1, (int)d1); 把double强制类型转成 int
- 2,显式实例化:在函数名后的<>中指定模板参数的实际类型
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.1, d2 = 20.2;
Add(a1, a2); //隐式实例化
Add<double>(d1, d2); //显示实例化
Add<int>(a1, d1); //显示实例化,类型不一样会进行类型转换成Add<int>,int类型,如果类型转换失败,编译器会报错
return 0;
}
注意:如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错。
模板参数的匹配原则
- 1,一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数
// 专门处理int的加法函数
int Add(int left, int right)
{
return left + right;
}
// 模板:通用加法函数
template<class T>
T Add(T left, T right)
{
return left + right;
}
void Test()
{
Add(1, 2);
}
int main()
{
// 与非模板函数匹配
Add(1, 2);
return 0;
}
对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板函数。
- 2,模板函数不允许自动类型转换,但普通函数可以进行自动类型转换
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.1, d2 = 20.2;
Add(a1, d1); //编译出错, //显示实例化 Add<int>(a1,d1); 或者强转 Add(a1,(int)d1);
return 0;
}
类模版
类模板的定义格式
template<class T1,class T2,...class Tn>
class 类模板名
{
//类内成员定义
};
类模板的由来
C 语言是如何实现一个 Stack
C语言定义栈存在的问题
1,忘记初始化和销毁
2,没有封装
3,没办法支持泛型,如果想同时定义两个栈,一个存int,一个存double,做不到看起来不容易忘记初始化和销毁,实际中,我们很容易忘记。
C语言中没有封装,谁都可以修改内部的值。
如果想用栈存不同类型的数据,就需要写两个不同类型的栈
typedef int STDateType;
typedef struct Stack_C
{
STDateType* _a;
int _size; // 或者取名叫 _top
int _capacity;
}Stack_C;
void Stack_CInit(Stack_C* ps)
{}
void Stack_CDestory(Stack_C* ps)
{}
void Stack_CPush(Stack_C* ps, STDateType x)
{}
void Stack_CPop(Stack_C* ps)
{}
void Test_C()
{
Stack_C st_c;
Stack_CInit(&st_c);
Stack_CPush(&st_c, 1);
Stack_CPush(&st_c, 2);
Stack_CPush(&st_c, 3);
Stack_CPush(&st_c, 4);
//非法修改
st_c._capacity = 0;
Stack_CDestory(&st_c);
}
C++ 是如何实现一个 Stack
要实现存储多种不同类型的栈,我们可以使用类模板,类模板的使用和函数模板都是同理
1,类在定义的时候就会自动调用构造函数,出了作用域自动调用析构函数完成清理工作
2,拥有了封装,有访问限定符的限制,增强了代码的安全性和可维护性
3,有了模板,可以实现多种不同类型的数据储存到栈中
template<class T>
class Stack_CPP
{
public:
Stack_CPP()
{
}
~Stack_CPP()
{
}
void Push(T x)
{
}
private:
T* _a;
int _size;
int _capacity;
};
void Test_CPP()
{
Stack_CPP<int> st_cpp_int;
st_cpp_int.Push(1); //实际也是两个参数,一个是隐含的this指针
st_cpp_int.Push(2);
st_cpp_int.Push(3);
st_cpp_int.Push(4);
//存double类型的栈
Stack_CPP<double> st_cpp_double;
st_cpp_double.Push(1.1);
st_cpp_double.Push(2.2);
st_cpp_double.Push(3.3);
}
类模板的实例化
类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<> 中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。
//vector 类名, vector<int>才是类型
vector<int> s1;
vector<double> s2;
我们现在来实现一个顺序表(动态增长的数组),C语言中的顺序表用SeqList表示,而C++喜欢用vector表示。
#include <iostream>
#include <assert.h>
// C语言中的顺序表SeqLIst,C++中的顺序表喜欢用 vector这个名字
template<class T>
class vector //动态增长的数组
{
public:
vector()
:_a(nullptr)
,_size(0)
,_capacity(0)
{
}
//一般不给空间,不管给不给空间插入都需要判断是否需要增容
vector(size_t n) //重载构造函数,一般不写这个,因为通常不给空间
:_a(new T[n])
, _size(0)
, _capacity(n)
{
}
~vector()
{
delete[] _a;
_a = nullptr;
_size = _capacity = 0;
}
//类里面声明,类外面定义
void push_back(const T& x); //如果传的不是内置类型就会引发无穷递归拷贝构造,所以使用引用,减少拷贝提高效率
void pop_back();
size_t size()
{
return _size; //返回个数
}
T operator[](size_t i) //重载 运算符[]:运算符的重载是为了让内置类型能够像内置类型一样使用该运算符
{
assert(i < _size);
return _a[i];
}
private:
T* _a;
int _size;
int _capacity;
};
//类外面定义,每次都要写一个template<class T>,外面的定义已经和类分离了,脱离了这个类,所以必须再写一个
template<class T>
void vector<T>::push_back(const T& x)
{
if (_size == _capacity)
{
size_t newcapacity = _capacity == 0 ? 2 : _capacity * 2;
T* tmp = new T[newcapacity];
if (_a != nullptr)
{
memcpy(tmp, _a, sizeof(T) * _size);
delete[] _a; //自动调用析构,自动置nullptr
}
_a = tmp;
_capacity = newcapacity;
}
_a[_size] = x;
++_size;
}
int main()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
for (size_t i = 0; i < v.size(); ++i)
{
//[] 读
cout << v[i] << " ";
}
cout << endl;
//可以修改吗??
for (size_t i = 0; i < v.size(); ++i)
{
//[] 写
//v[i] *= 2; //返回值是临时的,具有常性,不能修改
cout << v[i] << " ";
}
return 0;
}
我们只知道传值返回和传引用返回的区别,传值返回会在先把这个变量赋值给临时的变量,而临时变量具有常性,所以不能够修改其中的值。
如果想变成可写的(可修改的),可以使用引用返回,引用就是它的别名,可以修改。
模板的总结
泛型编程: 使用模板,编写跟类型无关的代码。
在使用一些函数和类的时候,针对不同类型需要写很多重复的代码。
函数:比如我们想实现交换int、double、char等等各种类型对象函数swap
类:比如我们想实现一个数据结构栈stack,stack的多个对象 st1 存int,st2 存double,等等。
在没有模板之前,我们得针对各个类型写很多个swap函数 和 stack类。而这些函数和类,逻辑是一样的,只是处理的对象类型不同。
使用模板,我们就可以编写一套通用的代码,无需为每种类型重复编写相同逻辑的函数和类,大大提高了代码的复用性和可维护性。
学到这里, 可以说我们C++入门了, 因为C++的基础语法铺垫已经完成, 接下来才是正式学习C++的开始。加油 ~