模板和STL简介
一、泛型编程
1、通用交换函数的实现
(1)代码
cpp
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;
}
//............
(2)总结
- 使用函数重载虽然可以实现一个通用的交换函数,但是使用这种方法有几个不好的地方。
- 重载的函数只有形参的类型不同,其他的都一样,这样的代码复用率比较低,当要进行操作的对象的类型不同时,就需要用户自己添加对应类型的函数。
- 代码的可维护性比较低,一个函数出错可能所有的重载函数均会出错。
2、泛型编程的概念
- 泛型:不使用具体的数据类型,而是使用一种通用类型 T 来进行程序设计;这里的T 只是一个占位符,实际在 T 的位置,它的真实数据类型取决于用户的需求;占位符的替换由编译器在编译阶段完成。
- 泛型编程:为了避免因数据类型的不同,而被迫重复编写大量相同业务逻辑的代码,因此发展了泛型及泛型编程技术;即泛型编程就是独立于任何特定类型的方式去编写代码。
- 编写与类型无关的通用代码,是代码复用的一种手段。
3、模板的概念
建立一个通用函数或类,其类内部的类型和函数的形参类型不具体指定,用一个虚拟的类型来代表。这种通用的方式称为模板。而模板是泛型编程的基础。
二、函数模板
1、概念
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,即编译器自动依据实参的类型推演函数形参的类型,再根据推演出来的类型生成具有特定类型的函数版本。
2、格式
- template<typename T1, typename T2,...,typename Tn>
- 返回值类型 函数名(参数列表){}
- typename是用来定义模板参数的关键字,它可以使用class替换,但不能使用struct代替class。虽然class和struct都有类的定义,但在此处它们的作用是不相同的。
- typename后面的类型名字T是随便取的,取T是因为采用type的缩写,还可以写为Ty、K、V等等,但一般都是大写字母或者单词首字母大写。
- T 代表的是一个模板类型(虚拟类型)。
3、代码
cpp
template<typename T>
void Swap( T& left, T& right)
{
T temp = left;
left = right;
right = temp;
}
4、原理
函数模板就像一个蓝图,它本身并不是一个函数,而是编译器用使用方式产生特定具体类型函数的模具。所以模板就是将本来应该我们做的重复事情交给编译器去做,即还是会生成特定的函数,而不是说调用函数时直接使用模板进行操作。
- 例如,当用double类型的参数使用函数模板时,编译器通过对实参类型进行推演,将T确定为double类型,然后产生一个专门处理double类型的函数,对于其他类型的参数也是如此。
三、函数模板实例化
1、概念
用不同类型的参数使用函数模板时,称为函数模板的实例化。而实例化有两种,即隐式实例化和显式实例化。
2、隐式实例化
(1)概念
在发生函数调用的时候,如果没有找到相匹配的函数存在,编译器就会寻找同名函数模板,如果进行参数类型推演可以成功,就对函数模板进行实例化。
(2)代码
cpp
template<typename T>
T Add(const T& x, const T& y)
{
return x + y;
}
void Test1()
{
int a1 = 10, a2 = 20;
double d1 = 10.5, d2 = 20.5;
cout << Add(a1, a2) << endl;
cout << Add(d1, d2) << endl;
//cout << Add(a1, d2) << endl;
cout << Add((double)a1, d2) << endl;
return 0;
}
(3)运行结果
(4)错误代码与编译器报错
(5)错误代码解释
- 该语句之所以不能通过编译,是因为在编译期间,当编译器执行到该实例化语句时,需要推演其形参类型,而通过实参a1将T推演为int类型,通过实参d1将T推演为double类型,但模板参数列表中只有一个T。
- 因此,编译器无法确定此处到底该将T确定为int类型还是double类型而报错。
- 在模板中,编译器一般不会进行类型转换的操作,因为一旦转化出问题,编译器就需要背黑锅。
- 此时有两种处理方式,第一种为用户自己将实参进行强制类型转化,使传递的实参只有一个类型。如代码中的最后一条语句;第二种为使用显式实例化。
3、显式实例化
(1)概念
在函数名后的<>中指定模板参数的实际类型,使函数模板生成特定类型的函数,从而避免在使用模板时重复编译相同的模板代码,或者模板T只有一个类型,而传递的实参的类型却不止有一个时,即编译器推演的形参类型与模板的参数类型不匹配时,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错。
(2)代码
cpp
int main()
{
int a = 10;
double d = 10.5;
cout << Add<int>(a, d) << endl;
cout << Add<double>(a, d) << endl;
return 0;
}
(3)运行结果
4、模板参数的匹配原则
(1)原则
- 一个非模板函数和一个同名的函数模板可以同时存在,而且该函数模板还可以被实例化为这个非模板函数。
- 对于非模板函数和同名函数模板,如果其他条件都相同,在调用时会优先调用非模板函数而不会从函数模板中实例化一个函数。如果模板函数可以产生一个参数匹配更好的函数,那么将选择函数模板去实例化一个函数。
- 模板函数不允许自动类型转换,但普通函数却可以进行自动类型转换。
(2)示例代码1
cpp
int Add(int x, int y)
{
return x + y;
}
template<typename T>
T Add(const T& x, const T& y)
{
return x + y;
}
int main()
{
int a1 = 10, a2 = 20;
cout << Add(a1, a2) << endl;
cout << Add<int>(a1, a2) << endl;
return 0;
}
- 第一个Add函数调用时,将调用非模板函数,第二个因为使用显式实例化,所以它将用函数模板实例化出一个非模板函数,而这个实例化出来的函数和已存在的非模板函数一样。
(3)示例代码2
cpp
int Add(int x, int y)
{
return x + y;
}
template<class T1,class T2>
T1 Add(const T1& x, const T2& y)
{
return x + y;
}
int main()
{
cout << Add(10, 20) << endl;
cout << Add(10, 20.5) << endl;
return 0;
}
- 第一个Add函数调用时,它的实参与非模板函数的形参类型完全匹配,所以不需要使用函数模板去实例化一个函数。
- 第二个Add函数调用时,模板函数可以生成更加匹配的函数,则编译器会根据实参使用模板函数去生成一个更加匹配的Add函数。
四、类模板
1、格式
cpp
template<class T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员定义
};
2、代码
cpp
template<typename T>
class Stack
{
public:
Stack(size_t capacity = 0)
{
if (capacity > 0)
{
_a = new T[capacity];
_capacity = capacity;
_top = 0;
}
}
~Stack()
{
delete[] _a;
_a = nullptr;
_capacity = _top = 0;
}
void Push(const T& x);
void Pop()
{
assert(_top > 0);
--_top;
}
bool Empty()
{
return _top == 0;
}
const T& Top()
{
assert(_top > 0);
return _a[_top - 1];
}
private:
T* _a = nullptr;
size_t _top = 0;
size_t _capacity = 0;
};
template<class T>
void Stack<T>::Push(const T& x)
{
if (_top == _capacity)
{
size_t newCapacity = _capacity == 0 ? 4 : _capacity * 2;
// 1、开辟新空间 2、拷贝数据 3、释放旧空间
T* tmp = new T[newCapacity];
if (_a)
{
memcpy(tmp, _a, sizeof(T)*_top);
delete[] _a;
}
_a = tmp;
_capacity = newCapacity;
}
_a[_top] = x;
++_top;
}
int main()
{
try
{
/*Stack<> st1;
Stack st2;*/
Stack<int> st3;
Stack<char> st4;
Stack<int> st5(10);
st3.Push(1);
st3.Push(2);
st3.Push(3);
st3.Push(4);
st3.Push(5);
/*st3.Top()++;
st3.Top() *= 2;*/
while (!st3.Empty())
{
cout << st3.Top() << " ";
st3.Pop();
}
cout << endl;
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}
3、讲解
- 上方代码是一个Stack的类模板,但它不是具体的类,而是编译器根据被实例化的类型而生成具体类的模板。
- 虽然模板不支持分离编译,即不支持声明放在.h头文件,定义放在.cpp源文件中。但当模板在同一个文件中时,是可以声明和定义分离的。即如上方代码中的Push成员函数。而当类模板中的成员函数放在类外进行定义时,需要加模板参数列表(template< class T>)。
- 类模板都需要显示实例化,虽然实例化出来的类都用了同一个类模板,但它们并不是同一个类型。
- 类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将欲实例化的类型放在<>中,如上方代码中的st3和st4。但需要注意的是类模板名字不是真正的类,而实例化后的结果才是真正的类,即Stack是类名,而Stack< int >才是类型。
- 当知道对象需要存储数据的数量时,可以如上方代码中的st5一样,在()中输入欲存储数据的数量,这样的操作能避免在插入数据时进行扩容的消耗。
- try和catch的作用是捕获异常,即当new开辟空间失败抛出异常时,会对被抛出的异常进行捕获。
- 类模板中的Top成员函数是用const修饰的,当调用Top函数时,不能对其进行修改,如要对其进行修改则需要去掉const。
4、运行结果
5、错误代码与编译器报错
五、STL
1、概念
- STL(Standard Template Library),即标准模板库,它是一个高效的C++程序库,包含了诸多常用的基本数据结构和基本算法。为广大C++程序员们提供了一个可扩展的应用框架,高度体现了软件的可复用性。
- 从逻辑层次来看,在STL中体现了泛型化程序设计的思想(generic programming)。在这种思想里,大部分基本算法被抽象,被泛化,独立于与之对应的数据结构,用于以相同或相近的方式处理各种不同的情形。
- 从实现层次来看,整个STL是以一种类型参数化(type parameterized)的方式实现的,即是基于模板(template)的。
2、六大组件
3、缺陷
- STL库的更新太慢了。它的上一个靠谱的版本是C++98,中间的C++03添加了一些修订。到C++11出来时已经过了13年,STL才得到了进一步的更新。
- STL现在都没有支持线程安全。并发环境下需要我们自己加锁,且锁的粒度比较大。
- STL极度地追求效率,导致内部比较复杂。比如,类型萃取,迭代器萃取。
- STL的使用会有代码膨胀的问题,比如使用vector/vector/vector这样会生成多份代码。当然,这是模板语法本身导致的。
本文到这里就结束了,如有错误或者不清楚的地方欢迎评论或者私信
创作不易,如果觉得博主写得不错,请务必点赞、收藏加关注💕💕💕