您现在的位置是:首页 >技术教程 >从C语言到C++⑩(第四章_模板初阶+STL简介)如何学习STL网站首页技术教程
从C语言到C++⑩(第四章_模板初阶+STL简介)如何学习STL
目录
1. 泛型编程
1.1 函数重载弊端
如何实现一个通用的交换函数呢?我们学了C++还是比C语言方便的(引用+函数重载):
#include<iostream>
using namespace std;
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;
double c = 1.1, d = 2.2;
char e = 'e', f = 'f';
Swap(a, b);
Swap(c, d);
Swap(e, f);
return 0;
}
1.2 泛型编程概念
2. 函数模板
2.1 函数模板的概念
2.2 函数模板格式
① template 是定义模板的关键字,后面跟的是尖括号 < >
也可以使用class(切记:不能使用struct代替class)
② typename 是用来定义模板参数的关键字
③ T1, T2, ..., Tn 表示的是函数名,可以理解为模板的名字,名字你可以自己取。
#include<iostream>
using namespace std;
template<typename T>
void Swap(T& left, T& right)
{
T temp = left;
left = right;
right = temp;
}
int main()
{
int a = 0, b = 1;
double c = 1.1, d = 2.2;
char e = 'e', f = 'f';
Swap(a, b);
Swap(c, d);
Swap(e, f);
cout << a << " " << b << endl;
return 0;
}
成功输出 1 0
如果是自定义类型,函数里面就要是拷贝构造,实现好就行。
因为 T 没有规定是什么类型,所以任意类型都是可以的,内置类型和自定义类型都可以的。
2.3 函数模板原理
思考:这三个交换函数调用的是同一个函数吗?
#include<iostream>
using namespace std;
template<typename T>
void Swap(T& left, T& right)
{
T temp = left;
left = right;
right = temp;
}
int main()
{
int a = 0, b = 1;
double c = 1.1, d = 2.2;
char e = 'e', f = 'f';
Swap(a, b);
Swap(c, d);
Swap(e, f);
return 0;
}
不是同一个函数。这三个函数执行的指令是不一样的,你可以这么想,
它们都需要建立栈帧,栈帧里面是要开空间的,你就要给一个类型开空间,
(大小可能是1字节/4字节/8字节......)
类型都不一样(char int double )。所以当然调用的不是同一个函数了。
所以这里调用的当然不是模板,而是这个模板造出来的东西。
而函数模板造出 "实际要调用的" 的过程,叫做模板实例化。
编译器在调用之前会干一件事情 —— 模板实例化。我们就来探讨一下模板实例化。
2.4 函数模板实例化
这些不同类型的Swap函数是怎么来的:
int a = 0, b = 1;
Swap(a, b);
编译器在调用 Swap(a, b) 的时候,发现 a b 是整型的,编译器就开始找,
虽然没有找到整型对应的 Swap,但是这里有一份模板:
template<typename T>
void Swap(T& left, T& right)
{
T temp = left;
left = right;
right = temp;
}
这里要的是整型,编译器就通过这个模板,推出一个 T 是 int 类型的函数。
这时编译器就把这个模板里的 T 都替换成 int,生成出一份 T 是 int 的函数。
一样的,如果要调用 Swap(e, f) ,e f 是字符型,编译器就会去实例化出一个 char 的。
你调的函数还是那些函数,只是你写一份模板出来,让编译器去用模板生成那些函数。
前面注意事项那里我们说过,函数模板本身不是函数。
它是是编译器使用方式产生特定具体类型函数的模具,在编译器编译阶段,
对于模板函数的使用,编译器需要根据传入的实参类型来推演,生成对应类型的函数以供调用。
比如:当用 double 类型使用函数模板时,编译器通过对实参类型的推演,
将 T 确定为 double 类型,然后产生一份专门处理 double 类型的代码,对于字符类型也是如此。
用不同类型的参数使用模板参数时,称为函数模板的实例化。
模板参数实例化分为:隐式实例化 和 显式实例化
2.4.1 隐式实例化
定义:让编译器根据实参,推演模板函数的实际类型。
我们刚才讲的 Swap 其实都是隐式实例化,就是让编译器自己去推。
现在我们再举一个 Add 函数模板做参考:
#include <iostream>
using namespace std;
template<class T>
T Add(const T& x, const T& y)
{
return x + y;
}
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.1, d2 = 20.2;
cout << Add(a1, a2) << endl;
cout << Add(d1, d2) << endl;
return 0;
}
现在思考一个问题,如果出现 a1 + d1 即int+double这种情况呢?实例化能成功吗?
这必然是失败的, 因为会出现冲突。编译器怎么知道T是什么呢?
解决方式
① 传参之前先进行强制类型转换,非常霸道的解决方式:
② 写两个参数,那么返回的参数类型就会起决定性作用:
#include <iostream>
using namespace std;
template<class T1, class T2>
T1 Add(const T1& x, const T2& y) // 那么T1就是int,T2就是double
{
return x + y; // 范围小的会像范围大的提升,int会像double "妥协"
} // 最后表达式会是一个double,但是最后返回值又是T1,是int,又会转
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.1, d2 = 20.2;
cout << Add(a1, d1) << endl;
return 0;
}
当然,这种问题严格意义上来说是不会用多个参数来解决的,
这里只是想从语法上演示一下,我们还有更好地解决方式:
③ 我们还可以使用 "显式实例化" 来解决:
Add<int>(a1, d2); // 指定实例化成int
Add<double>(a1, d2) // 指定实例化成double
2.4.2 显式实例化
定义:在函数名后的 < > 里指定模板参数的实际类型。
简单来说,显式实例化就是在中间加一个尖括号 < > 去指定你要实例化的类型。
(在函数名和参数列表中间加尖括号)
函数名 <类型> (参数列表);
#include <iostream>
using namespace std;
template<class T>
T Add(const T& x, const T& y)
{
return x + y;
}
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.1, d2 = 20.2;
cout << Add(a1, a2) << endl;
cout << Add(d1, d2) << endl;
cout << Add<int>(a1, d2) << endl; // 指定T用int类型
cout << Add<double>(a1, d2) << endl; // 指定T用double类型
return 0;
}
第一个 Add<int>(a1, a2) ,a2 是 double,它就要转换成 int 。
第二个 Add<double>(a1, a2),a1 是 int,它就要转换成 double。
这种地方就是类型不匹配的情况,编译器会尝试进行隐式类型转换。
像 double 和 int 这种相近的类型,是完全可以通过隐式类型转换的。
如果无法成功转换,编译器将会报错。
总结:
函数模板你可以让它自己去推,但是推的时候不能自相矛盾。
你也可以选择去显式实例化,去指定具体的类型。
2.5 模板参数的匹配原则
#include <iostream>
using namespace std;
template<class T>
T Add(const T& x, const T& y)
{
cout << "T" << endl;
return x + y;
}
int Add(int x, int y)
{
cout << "int" << endl;
return x + y;
}
int main()
{
int a1 = 10, a2 = 20;
cout << Add(a1, a2) << endl;
return 0;
}
如果你是编译器,当 Add(a1, a2) 时你会选择用哪一个?
是用函数模板印一个 int 类型的 Add 函数,还是用这现成的 Add 函数呢?
匹配原则:
① 一个非模板函数可以和一个同名的模板函数同时存在,
而且该函数模板还可以被实例化为这个非模板函数:
#include <iostream>
using namespace std;
template<class T>
T Add(const T& x, const T& y)
{
cout << "T" << endl;
return x + y;
}
int Add(int x, int y)
{
cout << "int" << endl;
return x + y;
}
int main()
{
int a1 = 10, a2 = 20;
cout << Add(a1, a2) << endl; // 默认用现成的,专门处理int的Add函数
cout << Add<int>(a1, a2) << endl; // 指定让编译器用模板,印一个int类型的Add函数
return 0;
}
② 对于非模板函数和同名函数模板,如果其他条件都相同,
在调用时会优先调用非模板函数,而不会从该模板生成一个实例。
如果模板可以产生一个具有更好匹配的函数,那么将选择模板。
#include <iostream>
using namespace std;
template<class T1, class T2>
T1 Add(const T1& x, const T2& y)
{
cout << "T" << endl;
return x + y;
}
int Add(int x, int y)
{
cout << "int" << endl;
return x + y;
}
int main()
{
cout << Add(1, 2) << endl; // 用现成的
//(与非函数模板类型完全匹配,不需要函数模板实例化)
cout << Add(1, 2.0) << endl; // 可以,但不是很合适,自己印更好
//(模板参数可以生成更加匹配的版本,编译器根据实参生产更加匹配的Add函数)
return 0;
}
3. 类模板
C语言在讲数据结构的时候,要转化存的类型,是用 typedef 来解决的。
但是要设置两个存不同类型的栈呢?CV?所以还是得用模板解决。
int main()
{
Stack st1; // 存int数据
Stack st2; // 存double数据
return 0;
}
3.1 类模板的定义
定义:和函数模板的定义方式是一样的,template 后面跟的是尖括号 < > :
template<class T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员定义
};
类内定义类模板和函数模板没什么不一样,看看类外定义类模板参数:
template<class T>
class Stack
{
public:
Stack(T capacity = 4)
: _top(0)
, _capacity(capacity)
{
_arr = new T[capacity];
}
~Stack();// 让析构函数放在类外定义
private:
T* _arr;
int _top;
int _capacity;
};
// 类模板中函数放在类外进行定义时,需要加模板参数列表
template <class T>
Stack<T>::~Stack()
{
delete[] _arr;
_arr = nullptr;
_capacity = _top = 0;
}
3.2 类模板实例化
我们试试用C++写一部分数据结构的栈:
#include <iostream>
using namespace std;
template<class T>
class Stack
{
public:
Stack(T capacity = 4)
: _top(0)
, _capacity(capacity)
{
_arr = new T[capacity];
}
~Stack()
{
delete[] _arr;
_arr = nullptr;
_capacity = _top = 0;
}
private:
T* _arr;
int _top;
int _capacity;
};
int main()
{
Stack<int> st1; // 存储int
Stack<double> st2; // 存储double
return 0;
}
函数模板之所以能推,是因为有实参传形参这么一个 "契机" ,让编译器能帮你推。
你定义一个类,编译器能推吗?
注意事项:
① Stack 不是具体的类,是编译器根据被实例化的类型生成具体类的模具。
template<class T>
class Stack {...};
② Stack 是类名,Stack<int> 才是类型:
Stack<int> s1;
Stack<double> s2;
4. 模板初阶笔试选择题
4.1 下面有关C++中为什么用模板类的原因,描述错误的是? ( )
A.可用来创建动态增长和减小的数据结构
B.它是类型无关的,因此具有很高的可复用性
C.它运行时检查数据类型,保证了类型安全
D.它是平台无关的,可移植性
4.2 在下列对fun的调用中,错误的是( )
template <class T>
T fun(T x,T y)
{
return x*x+y*y;
}
A.fun(1, 2)
B.fun(1.0, 2)
C.fun(2.0, 1.0)
D.fun<float>(1, 2.0)
4.3 下列关于模板的说法正确的是( )
A.模板的实参在任何时候都可以省略
B.类模板与模板类所指的是同一概念
C.类模板的参数必须是虚拟类型的
D.类模板中的成员函数全是模板函数
4.4 下列的模板声明中,其中几个是正确的( )
1)template
2)template<T1,T2>
3)template<class T1,T2>
4)template<class T1,class T2>
5)template<typename T1,T2>
6)template<typename T1,typename T2>
7)template<class T1,typename T2>
8)<typename T1,class T2>
A.2
B.3
C.4
D.5
4.5 下列描述错误的是( )
A.编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础
B.函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具
C.模板分为函数模板和类模板
D. 模板类跟普通类以一样的,编译器对它的处理时一样的
答案
4.1 C
A.模板可以具有非类型参数,用于指定大小,可以根据指定的大小创建动态结构
B.模板最重要的一点就是类型无关,提高了代码复用性
C.模板运行时不检查数据类型,也不保证类型安全,相当于类型的宏替换,故错误
D.只要支持模板语法,模板的代码就是可移植的
4.2 B
A.通过参数推导,T为int,不存在二义性,调用正确
B.由于参数类型不一样,模板不支持类型转换,推导参数会产生二义性,编译错误
C.通过参数推导,T为float,不存在二义性,调用正确
D.通过类型实例化函数,调用正确
4.3 D
A.不一定,参数类型不同时有时需要显示指定类型参数
B.类模板是一个类家族,模板类是通过类模板实例化的具体类
C.C++中类模板的声明格式为template<模板形参表声明><类声明>,
并且类模板的成员函数都是模板函数
D.正确,定义时都必须通过完整的模板语法进行定义
4.4 B
A.1.模板语法错误,2.没有关键字class或typename指定类型,3.T2缺少class或typename
B.正确, 4,6,7为正确声明
C.5.T2缺少class或typename
D.8.缺少template
4.5 D
A.模板是代码复用的重要手段
B.函数模板不是一个具体函数,而是一个函数家族
C.目前涉及到的模板就两类,函数模板与类模板
D.模板类是一个家族,编译器的处理会分别进行两次编译,其处理过程跟普通类不一样
5. STL简介(了解)
5.1 什么是STL
STL —— Standard Template Libary——标准模板库,是 C++ 标准库的重要组成部分,
它不仅是一个可重复的组件库,还是个包罗数据结构与算法的软件框架。
百度百科:
标准模板库(Standard Template Library,STL)是惠普实验室开发的一系列软件的统称。它是由Alexander Stepanov、Meng Lee和David R Musser在惠普实验室工作时所开发出来的。虽说它主要表出现到C++中,但在被引入C++之前该技术就已经存在了很长时间。STL的代码从广义上讲分为三类:algorithm(算法)、container(容器)和iterator(迭代器),几乎所有的代码都采用了模板类和模板函数的方式,这相比于传统的由函数和类组成的库来说提供了更好的代码重用机会。
标准模板库是一个C++软件库,大量影响了C++标准程序库但并非是其的一部分。其中包含4个组件,分别为算法、容器、函数、迭代器。
模板是C++程序设计语言中的一个重要特征,而标准模板库正是基于此特征。标准模板库使得C++编程语言在有了同Java一样强大的类库的同时,保有了更大的可扩展性。
在C++标准中,STL被组织为下面的13个头文件:<algorithm>、<deque>、<functional>、<iterator>、<vector>、<list>、<map>、<memory>、<numeric>、<queue>、<set>、<stack>和<utility>。
5.2 STL的版本
原始版本(HP 版本)
Alexander Stepanov、Meng Lee 在惠普实验室完成的原始版本,本着开源精神,他们声明允许任何人任意运用、拷贝、修改、传播、商业使用这些代码,无需付费。唯一的条件就是也需要向原始版本一样做开源使用。 HP 版本--所有STL实现版本的始祖。
P. J. 版本
由P. J. Plauger开发,继承自HP版本,被Windows Visual C++采用,不能公开或修改,
缺陷:可读性比较低,符号命名比较怪异。
RW版本
由Rouge Wage公司开发,继承自HP版本,被C+ + Builder 采用,不能公开或修改,可读性一般。
SGI版本
由Silicon Graphics Computer Systems,Inc公司开发,继承自HP版 本。
被GCC(Linux)采用,可移植性好,
可公开、修改甚至贩卖,从命名风格和编程 风格上看,阅读性非常高。
我们后面学习STL要阅读部分源代码,主要参考的就是这个版本。
5.3 STL的六大组件
STL提供六大组件,它们之间可以相互组合使用。
以下文字看一眼就行,后面还会详细讲解。
① 容器(containers)
容器用来存放数据,包括各种数据结构,如vector,list,deque,set,map等。
从实现的角度来看,STL容器是一种class template。
② 算法(algorithms)
算法包括各种常用的sort,search,copy,erase, find等等。
从实现的角度来看,STL算法是一种function template。
③ 迭代器(iterators)
迭代器作为“泛型指针”,扮演容器和算法之间的粘合剂,用来连接容器和算法。
从实现角度来看,迭代器是一种将operator*,operator->,operator++,operator--
等指针相关操作进行重载的class template。
所有的STL容器都带有自己专属的迭代器。原生指针也是一种迭代器。
所谓的原生指针就是我们定义的最普通的指针,
形如 类型名 *指针名,类型名可以是基础类型int,double等,也可以是一个类。
当一个类将*和->操作符进行重载时,虽然也可进行类似指针的操作,但是它已不是原生指针。
④ 仿函数(functors)
仿函数是让一个类看起来像一个函数。
其实就是一种重载了operator()的class或者class template。
⑤ 配接器(adapters)
一种用来修饰容器,仿函数或者迭代器的接口的东西。
配接器修改类的接口,使原来不相互匹配的两个类可以相互匹配,进行合作。
⑥ 配置器(allocators)
配置器主要负责空间的配置和管理。从实现角度来看,
配置器是一个实现了动态空间配置、空间管理、空间释放的class template。
5.4 如何学习STL
建议后期阅读:
《Effctive C++》《高质量C++》《STL源码剖析》
看看大佬是怎么说的:
本专栏后面就马上更新STL的内容了,学完消耗了应该就到第二境界了。
第三境界就靠各位的探索了。
本章完。
此章节(以及前面的章节)都是为了后面STL学习做的铺垫。
后面的章节比较散,所以就不分章节了,按序号看就好了。后几篇:string的详解。