1 型程序设计
õ 面向过程->基于对象->面向对象->泛型
1. 将程序写得尽可能通用
2. 将算法从特定的数据结构中抽象出来,成为通用的
3. C++的模板为泛型程序设计奠定了关键的基础
4. STL是泛型程序设计的一个范例
2 STL简介
何为STL?
标准模板库(Standard Tempate Library)是ANSI/ISO C++ 最有特色,最实用的部分之一
STL 简史
STL抽象的是什么
标准模板库(STL)
STL主要由六大部分组成:
※迭代器(iterators)
※算法(algorithms)
※容器(containers)
※函数对象(function objects)
※内存分配器(allocators)
※适配器
STL 的六大组件全都是抽象出来的概念
3 Container(容器)
õ 容器的概念
用来管理一组元素。
标准库容器类 |
说明 |
顺序容器 voctor(矢量) list(列表) deque(双端队列) |
从后面快速插入与删除,直接访问任何元素 从任何地方快速插入与删除,双链表 从前或后面快速插入与删除,直接访问任何元素 |
关联容器 set(集合) multiset(多重集合) map(映射) multimap(多重映射) |
快速查找,不允许重复值 快速查找,允许重复值 一对一映射,基于关键字快速查找,不许重复值 一对多映射,基于关键字快速查找,允许重复值 |
容器适配器 stack(栈) queue(队列) priority_queue |
后进先出(LIFO) 先进先出(FIFO) 最高优先级元素总是第一个出列 |
容器的分类
õ 序列式容器(Sequence containers)
ô 每个元素都有固定位置--取决于插入时机和地点,和元素值无关。
ô 顺序容器,是因为元素的插入位置同元素的值无关。
ô vector、deque、list
õ 关联式容器(Associated containers)
ô 元素位置取决于特定的排序准则,和插入顺序无关
ô 关联式容器内的元素是排序的,插入任何元素,都按相应的排序准则来确定其位置。关联式容器的特点是在查找时具有非常好的性能。
ô set、multiset、map、multimap
容器的通用接口
õ 通用容器运算符
ô ==,!=,>,>=,<,<=,=
õ 方法(函数)
ô 迭代方法
ó begin(),end(),rbegin(),rend()
ô 访问方法
ó size(),max_size(),swap(),empty()
顺序容器
õ 顺序容器的接口
ô 插入方法
ó push_front(),push_back(),insert(),运算符“=”
ô 删除方法
ó pop() ,erase(),clear()
ô 迭代访问方法
ó 使用迭代器
ô 其它顺序容器访问方法(不修改访问方法)
ó front(),back(),下标[]运算符
顺序容器——向量
头文件 <vector>
实际上就是个动态数组。随机存取任何元素都能在常数时间完成。在尾端增删元素具有较佳的性能。
õ 数组尾部添加或移除元素非常快速。但是在中部或头部安插元素比较费时。
õ 向量属于顺序容器,用于容纳不定长线性序列(即线性群体),提供对序列的快速随机访问(也称直接访问)
õ 向量是动态结构,它的大小不固定,可以在程序运行时增加或减少。
例1 vector
#include <iostream>
#include <vector> //用vector前,必须包含此头文件
using namespace std;
int main()
{
vector<int> coll; //vector container for integer
// append elements with values 1 to 6
for(int i=1; i<=6; ++i) {
coll.push_back(i);
}
//print all elements followed by a space
for(int i=0; i<coll.size(); ++i) {
cout << coll[i] << ' ';
}
cout << endl;
}
顺序容器——双端队列
头文件 <deque>
也是个动态数组,随机存取任何元素都能在常数时间完成(但性能次于vector)。在两端增删元素具有较佳的性能。
ô deque,是“double-ended queue”的缩写。
ô 双端队列是一种放松了访问权限的队列。元素可以从队列的两端入队和出队,也支持通过下标操作符“[]”进行直接访问。
ô 可以随机存取元素(用索引直接存取)。
ô 数组头部和尾部添加或移除元素都非常快速。但是在中部或头部安插元素比较费时。
例2 deque
int main()
{
deque<float> coll; //deque container for floating-point
//insert element from 1.1 to 6.6 each at the front
for(int i=1; i<=6; ++i) {
coll.push_front(i*1.1); // insert at the front
}
//print all elements followed by a space
for(int i=0; i<coll.size(); ++i) {
cout << coll[i] << ' ';
}
cout << endl;
}
顺序容器——列表
list头文件 <list>
õ 双向链表,在任何位置增删元素都能在常数时间完成。不支持随机存取。
ô 列表主要用于存放双向链表,可以从任意一端开始遍历。列表还提供了拼接(splicing)操作,将一个序列中的元素从插入到另一个序列中。
ô 不提供随机存取(按顺序走到需存取的元素,O(n))。
ô 在任何位置上执行插入或删除动作都非常迅速,内部只需调整一下指针。
例3链表
int main()
{
list<char> coll; //list container for charactor
//append elements from 'a' to 'z'
for(char c='a'; c<='z'; ++c) {
coll.push_back(c);
}
while(!coll.empty()) {
cout << coll.front() << ' ';
coll.pop_front();
}
cout << endl;
}
关联式容器
õ Sets/Multisets
头文件 <set>
set 即集合。set中不允许相同元素,multiset中允许存在相同的元素。
ô 内部的元素依据其值自动排序
ô Set内的相同数值的元素只能出现一次,Multisets内可包含多个数值相同的元素。
ô 内部由二叉树实现,便于查找。
例4
int main()
{
//type of the collection
typedef std::set<int> IntSet;
IntSet coll; //set container for int values
coll.insert(1);
coll.insert(6);
coll.insert(2);
IntSet::const_iterator pos;
for(pos = coll.begin(); pos!= coll.end(); ++pos) {
std::cout <<*pos <<' ';
}
std::cout <<std::endl;
}
õ Maps/Multimaps
头文件 <map>
map与set的不同在于map中存放的是成对的key/value。
并根据key对元素进行排序,可快速地根据key来检索元素
map同multimap的不同在于是否允许多个元素有相同的key值。
ô Map的元素是成对的键值/实值,内部的元素依据其值自动排序。
ô Map内的相同数值的元素只能出现一次,Multimaps内可包含多个数值相同的元素。
ô 内部由二叉树实现,便于查找。
例5
#include <iostream>
#include <map>
#include <string>
using namespace std;
int main()
{
//type of the collection
typedef multimap<int, string> IntStringMMap;
IntStringMMap coll; //container for int/string
//notice make_pair 为便捷函数返回一个pair 对象
coll.insert(make_pair(6, "string"));
coll.insert(make_pair(1, "is"));
coll.insert(make_pair(3, "multimap"));
IntStringMMap::iterator pos;
for(pos = coll.begin(); pos!=coll.end; ++pos) {
cout << pos->second << ' ';
}
cout << endl;
}
容器适配器简介
1) stack :头文件 <stack>
栈。是项的有限序列,并满足序列中被删除、检索和修改的项只能是最近插入序列的项。即按照后进先出的原则
2) queue :头文件 <queue>
队列。插入只可以在尾部进行,删除、检索和修改只允许从头部进行。按照先进先出的原则。
3)priority_queue :头文件 <queue>
优先级队列。最高优先级元素总是第一个出列
容器的共有成员函数
1) 所有标准库容器共有的成员函数:
ô 相当于按词典顺序比较两个容器大小的运算符:
=, < , <= , > , >=, == , !=
ô empty : 判断容器中是否有元素
ô max_size: 容器中最多能装多少元素
ô size: 容器中元素个数
ô swap: 交换两个容器的内容
2) 只在第一类容器中的函数:
begin 返回指向容器中第一个元素的迭代器
end 返回指向容器中最后一个元素后面的位置的迭代器
rbegin 返回指向容器中最后一个元素的迭代器
rend 返回指向容器中第一个元素前面的位置的迭代器
erase 从容器中删除一个或几个元素
clear 从容器中删除所有元素
容器的共同能力
õ 所有容器提供的都是value语意,而非reference语意。容器执行插入元素的操作时,内部实施拷贝动作。所以STL容器内存储的元素必须能够被拷贝(必须提供拷贝构造函数)。
õ 每个容器都提供可返回迭代器的函数,运用返回的迭代器就可以访问元素。
õ 通常STL不会丢出异常。要求使用运行者对确保传入正确的参数。
容器的共同操作
õ 初始化--每个容器都提供了一个默认构造函数,一个拷贝构造函数
list<int> l;
...
vector<int> ivec( l.begin(), l.end() ); //以某个容器的元数为初值完成初始化
int array[] = {1, 2, 3, 4};
...
//以某个数组的元素为初值完成初始化
set<int> iset(array, array+sizeof(array)/sizeof(array[0]));
õ 与大小相关的操作函数
c.size() //返回容器中的元素数量
c.empty() //判断容器是否为空(相当于size()==0, 但可能更加的快)
c.max_size() //返回元素的最大可能数量
õ 返回迭代器的函数
c.begin() //返回一个迭代器,指向第一个元素
c.end() //返回一个迭代器,指向最后一个元素
c.rbegin() //返回一个逆向迭代器,指向逆向回访时的第一元素
c.rend() //返回一个逆向迭代器,指向逆向访问时最后元素的下一个位置
õ 比较操作
c1 == c2 //判断是否c1等于c2
c1 != c2 //判断是否c1不等于c2 相当于!(c1 == c2)
c1 < c2 //判断是否c1小于c2
c1 > c2 //判断是否c1大于c2 相当于!(c1 <= c2)
c1 <= c2 //判断是否c1小于等于c2 相当于!(c2 < c1)
c1 >= c2 //判断是否c1大于等于c2 相当于!(c1 < c2)
c1 = c2 //将c2的所有元素指派给c1
4 迭代器(Iterator)简述
õ 迭代器与容器
ô 通过迭代器,我们可以用相同的方式来访问、遍历容器(即为泛型的抽象)。
迭代器的概念
õ 迭代器是面向对象版本的指针
ô 指针可以指向内存中的一个地址
ô 迭代器可以指向容器中的一个位置
õ STL的每一个容器类模版中,都定义了一组对应的迭代器类。使用迭代器,算法函数可以访问容器中指定位置的元素,而无需关心元素的具体类型。
ô 迭代器是一个“可遍历STL容器内全部或部分元素”的对象。
ô 一个迭代器指出容器中的一个特定位置。
ô 具有遍历复杂数据结构的能力。
õ 迭代器的基本操作
用法和指针一样,其实指针就是一种迭代器
õ 迭代器的类型
ô 输入迭代器
õ 可以用来从序列中读取数据
ô 输出迭代器
õ 允许向序列中写入数据
ô 前向迭代器
õ 既是输入迭代器又是输出迭代器,并且可以对序列进行单向的遍历
ô 双向迭代器
õ 与前向迭代器相似,但是在两个方向上都可以对数据遍历
ô 随机访问迭代器
õ 也是双向迭代器,但能够在序列中的任意两个位置之间进行跳转。
标准库迭代器类型 |
说明 |
输入 InputIterator |
从容器中读取元素。输入迭代器只能一次一个元素地向前移动(即从容器开头到容器末尾)。 要重读必须从头开始。 |
输出 OutputIterator |
向容器写入元素 。 输出迭代器只能一次一个元素地向前移动。 输出迭代器要重写,必须从头开始 |
正向 ForwardIterator |
组合输入迭代器和输出迭代器的功能, 并保留在容器中的位置(作为状态信息), 所以重新读写不必从头开始。 |
双向 BidirectionalIterator |
组合正向迭代器功能与逆向移动功能 (即从容器序列末尾到容器序列开头) |
随机访问 RandomAccseeIterator |
组合双向迭代器的功能,并能直接访问容器中的任意元素,即可向前或向后调任意个元素。 |
不同迭代器所能进行的操作(功能)
õ 所有迭代器: ++p, p ++
õ 输入迭代器: * p, p = p1, p == p1 , p!= p1
õ 输出迭代器: * p, p = p1
õ 正向迭代器: 上面全部
õ 双向迭代器: 上面全部,--p, p --,
õ 随机访问迭代器: 上面全部,以及:
ô p+= i, p -= i,
ô p + i: 返回指向 p 后面的第i个元素的迭代器
ô p - i: 返回指向 p 前面的第i个元素的迭代器
ô p[i]: p 后面的第i个元素的引用
ô p < p1, p <= p1, p > p1, p>= p1
õ 迭代器示例
例6
int main()
{
list<char> coll; // list container for character elements
//append elements from 'a' to 'z'
for(char c='a'; c<='z'; ++c) {
coll.push_back(c);
}
list<char>::const_iterator pos;
for(pos = coll.begin(); pos != coll.end(); ++pos) {
cout << *pos <<' ';
}
cout << endl;
}
5 算法(algorithm)
õ STL中提供能在各种容器中通用的算法,比如插入,删除,查找,排序等。大约有70种标准算法。
õ 算法就是一个个函数模板。
õ 算法表现为一系列的函数模板,它们完整定义在STL头文件中。一般这些函数模板都使用迭代器作为它的参数和返回值,因此泛型算法不依赖于具体的容器。
õ STL中也定义了一些标准的函数对象,如果以功能划分,可以分为算术运算、关系运算、逻辑运算三大类。为了调用这些标准函数对象,需要包含头文件<functional>。
õ 泛型算法分类为
õ (1)不修改序列的操作
õ find( )、cout( )、equal( )、mismatch( )和search( )等。
õ (2)修改序列的操作
õ swap( )、copy( )、transform( )、replace( )、remove( )、
õ reverse( )、rotate( )和fill( )等。
õ (3)排序、合并和相关的操作
õ sort( )、binary_search( )、merge( )、min( )和max( )等。
每个泛型算法(generic algorithm)的实现都独立于单独的容器类型,它消除了算法的类型依赖性
算法示例:find()
template<class InIt, class T>
InIt find(InIt first, InIt last, const T& val);
õ first 和 last 这两个参数都是容器的迭代器,它们给出了容器中的查找区间起点和终点。
ô 这个区间是个左闭右开的区间,即区间的起点是位于查找范围之中的,而终点不是
õ val参数是要查找的元素的值
õ 函数返回值是一个迭代器。如果找到,则该迭代器指向被找到的元素。如果找不到,则该迭代器指向查找区间终点。
例7
#include <vector>
#include <algorithm>
#include <iostream>
using namespace std;
void main()
{
int array[10] = {10, 20, 30, 40};
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
vector<int>::iterator p;
p=find(v.begin(), v.end(), 3);
if(p != v.end()) {
cout<< *p <<endl;
}
p=find(v.begin(), v.end(), 9);
if(p == v.end()) {
cout << "not find" << endl;
}
p=find(v.begin()+1, v.end()-2, 1);
if(p != v.end()) {
cout<< *p << endl;
}
int *pp=find(array, array+4, 20);
cout << *pp << endl;
}
函数对象
õ 在C++中,为了使程序的安全性更好,采用“引用”来代替指针作为函数的参数或返回值。在C++的泛型算法中类似地采用了“函数对象”(function object)来代替函数指针。函数对象是一个类,它重载了函数调用操作符(operator())。该操作符封装了应该被实现为一个函数的操作。典型情况下,函数对象被作为实参传递给泛型算法。和“引用”一样,“函数对象”独立使用比较少。函数对象亦称拟函数对象(function_like object)和函子(functor)。下面给出一个求和函数对象的定义:
template<typename T>class Sum
{
T res;
public:
sum(T i=0):res(i)
{
}//构造函数,即sum(T i=0){res=i;}
void operator()(T x)
{
res+=x;-
}//累加
T result() const
{
return res;
}//
}
õ 对象与函数指针相比较有三个优点:第一,函数指针是间接引用,不能作为内联函数,而函数对象可以,这样速度更快;第二,函数对象可以拥有任意数量的额外数据,用这些数据可以缓冲当前数据和结果,当然多数情况下不一定使用,上例中res就是一个额外数据;第三,编译时对函数对象做类型检查。
泛型算法
õ 在C++标准库中给出了70余种算法,泛型算法函数名都加有后缀,这些后缀的意思如下:
õ _if 表示函数采用的操作是在元素上,而不是对元素的值本身进行操作。如find_if算法表示查找一些值满足函数指定条件的元素,而find查找特定的值。
õ _copy 表示算法不仅操作元素的值,而且还把修改的值复制到一个目标范围中。reverser算法颠倒范围中元素的排列顺序,而reverse_copy算法同时把结果复制到目标范围中。
õ 其它的后缀从英文意思上立即可以认出其意义
其次我们介绍泛型算法的构造与使用方法。所有泛型算法的前两个实参是一对iterator,通常称为first和last,它们标出要操作的容器或内置数组中的元素范围。元素的范围,包括first,但不包含last的左闭合区间。即:
[first,last)
当first==last成立,则范围为空。
对iterator的类则要求在每个算法声明中指出(5个基本类别),所声明的是最低要求。
泛型算法分以下几类:
- 查找算法:有13种查找算法用各种策略去判断容器中是否存在一个指定值。equal_range()、lower_bound()和upper_bound()提供对半查找形式。
- 排序和通用整序算法:共有14种排序(sorting)和通用整序(ordering)算法,为容器中元素的排序提供各种处理方法。所谓整序,是按一定规律分类,如分割(partition)算法把容器分为两组,一组由满足某条件的元素组成,另一组由不满足某条件的元素组成。
- 删除和代替算法:有15种删除和代替算法。
- 排列组合算法:有2种算法。排列组合是指全排列。如:三个字符{a,b,c}组成的序列有6种可能的全排列:abc,acb,bac,bca,cab,cba;并且六种全排列按以上顺序排列,认为abc最小,cba最大,因为abc是全顺序(从小到大)而cba是全逆序(从大到小)。
- 生成和改变算法:有6种,包含生成(generate),填充(fill)等等。
- 关系算法:有7种关系算法,为比较两个容器提供了各种策略,包括相等(equal()),最大(max()),最小(min())等等。
- 集合算法:4种集合(set)算法提供了对任何容器类型的通用集合操作。包括并(union),交(intersection),差(difference)和对称差(symmetric difference)。
小结
Ø 模板是C++类型参数化的多态工具。
C++提供函数模板和类模板
Ø 模板定义以模板说明开始。
类属参数必须在模板定义中至少出现一次
Ø 同一个类属参数可以用于多个模板
Ø 类属参数可用于函数的参数类型、返回类型和声明函数中的变量
Ø 模板由编译器根据实际数据类型实例化,生成可执行代码。实例化的函数模板称 为模板函数;实例化的类模板称为模板类
Ø 函数模板可以用多种方式重载
Ø 类模板可以在类层次中使用