【C++】STL和模板初阶
前言:
这节学的是STL,不懂STL,就不要说懂C++!有了STL,许多的数据结构以及算法都不需要自己重新造轮子。
一、STL
简介
1.1 什么是STL
概念:STL(standard template libaray 标准模板库),是C++标准模板库的重要组成部分,不仅是一个可复用的组件库,而且是一个包罗数据结构与算法的软件框架。
1.2 STL
的版本
- HP STL:最初由惠普公司开发,是STL的早期版本,为C++标准模板库的发展奠定了基础,是开源的,是所有STL实现版本的始祖。
- P.J.STL:由P. J. Plauger开发,继承自HP版本,被Windows Visual C++采用,不能公开或修改。
- RW STL:由Rouge Wage公司开发,继承自HP版本,被C++ Builder采用,不能公开或修改,可读性一般。
- SGI STL:由Silicon Graphics Computer Systems,Inc公司开发,继承自HP版本。被GCC采用,可移植性好,可公开、修改甚至贩卖,从命名风格和编程风格来看,阅读性非常高。
1.3 STL
的重要性
- 泛型编程支持:利用模板实现高度抽象与复用,能编写与数据类型无关的通用代码,提高灵活性和扩展性。
- 高效的数据结构与算法:有 vector 、map 等多种数据结构,还包含大量优化过的经典算法,可优化性能,具备良好的时间和空间复杂度。
- 统一规范与接口:为容器和算法制定统一接口与操作规范,使代码风格统一,利于团队协作开发。
1.4 STL
的六大组件
二、模板初阶
2.1 泛型编程
概念:编写与类型无关的通用代码,是代码服用的一种手段。模板是泛型编程的基础。
示例:当我们写一个交换函数(swap),要如何实现呢?
首先想到的是用函数重载,函数重载的概念是,在同一作用域内,可以有多个同名函数,但在这些函数的参数列表(参数个数,参数类型或参数顺序)必须不同。
代码:
void swap(int& i, int& j)
{
int tmp = i;
i = j;
j = tmp;
}
void swap(double& i, double& j)
{
int tmp = i;
i = j;
j = tmp;
}
以上的函数重载虽然可以实现,但是代码很像,后期使用这样的代码的时候,是否要写这么多个,而且维护起来很难。那么要用这样的代码的时候,我们告诉编译器一个模板就好了。
模板分类:
2.2 函数模板
- 概念:函数模板代表了一个函数家族,函数模板就是一个公式,可用来生成针对特定类型的函数版本。
- 函数模板格式:
template<typename T1, typename T2,......,typename Tn>
返回值类型 函数名(参数列表){}
;
代码例子:
template<typename T>
void Swap( T& left, T& right)
{
T temp = left;
left = right;
right = temp;
}
注意:typename是用来定义模板参数关键字,也可以使用class(但不能用struct代替class)
2.3 实例化函数模板
概念:用不同类型的参数使用函数模板时,称为函数模板实例化。实例化分为隐式实例化和显式实例化。
- 隐式实例化:让编译器根据实参推演模板参数的实际类型
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.0, d2 = 20.0;
Add(a1, a2);
Add(d1, d2);
cout << Add(a1, a2) << endl;
return 0;
}
- 显式实例化:在函数名后的<>中指定模板参数的实际类型
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
int main(void)
{
int a = 10;
double b = 20.0;
// 显式实例化
Add<int>(a, b);
return 0;
}
要是遇到两个参数类型不同,有两种解决方法,一是程序员自己强转一下类型,二是显式实例化。
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.0, d2 = 20.0;
Add(a1, a2);
Add(d1, d2);
Add(a1, (int)d1);
Add<int>(a1, d2);
cout << Add(a1, a2) << endl;
cout << Add(a1, (int)d1) << endl;
cout << Add<int>(a1, d2) << endl;
return 0;
}
2.3.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); // 与非模板函数匹配,编译器不需要特化
Add<int>(1, 2); // 调用编译器特化的Add版本
}
-
对于非模板函数和同名函数模板,若其他条件都相同,在调动时会优先调用非模板函数。若模板可以产生一个具有更好匹配的函数,那么将选择模板。
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); // 与非函数模板类型完全匹配,不需要函数模板实例化 Add(1, 2.0); // 模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的 Add函数 }
2.4 类模板
- 概念:类模板是用来生成蓝图的。与函数模板不同的是,编译器不能为类模板推断模板参数类型。
- 类模板的定义格式:
template<class T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员定义
};
代码示例:
// 类模版
template<typename T>
class Stack
{
public:
Stack(size_t capacity = 4)
{
_array = new T[capacity];
_capacity = capacity;
_size = 0;
}
void Push(const T& data);
private:
T* _array;
size_t _capacity;
size_t _size;
};
注意
:类模板不建议声明和定义分离到两个文件.h和.cpp中,会出现链接错误,具体原因如下:
- 编译实例化困难:类模板的实例化依赖于编译器同时看到声明和定义,分离后链接时编译器难以找到模板的完整定义,导致无法正确实例化模板。
- 链接错误:在链接阶段,链接器可能找不到模板实例化所需的符号,从而产生链接错误,尤其在有多个源文件的情况下。
2.5 类模板的实例化
类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。
// Stack是类名,Stack<int>才是类型
Stack<int> st1; // int
Stack<double> st2; // double