C++学习笔记——泛型程序设计与STL库

来源:互联网 发布:三星note8绘画软件 编辑:程序博客网 时间:2024/06/06 01:42

泛型程序设计与STL库

1. 泛型程序设计基本概念

  • 编写不依赖具体数据类型的程序
  • 将算法从特定的数据结构中抽象出来,成为通用的
  • C++的模板为泛型程序设计奠定了关键的基础
  • 泛型程序设计中的术语:
    • 概念:用来界定一定功能的数据类型,例如,将可以比较大小的所有数据类型这一概念记为Comparable,将具有公有的赋值构造函数并可以用'=' 赋值的数据类型这一概念记为Assignable,将可以比较大小、具有公有复制构造函数并可以用等号赋值的所有数据类型这一概念记作Sortable。
    • 子概念:对于两个不同的概念A和B,如果概念A所需求的所有功能也是概念B所需求的功能,则B是A的子概念,例如,上述Sortable既是Comparable的子概念,也是Assignable的子概念
    • 模型:符合一个概念的数据类型成为该概念的模型,例如int型是Comparable概念的模型
  • 用概念做模板参数名

2. STL简介

  • 标准模板库(Standard Template Library,简称STL)定义类一套概念体系,为泛型程序设计提供了逻辑基础
  • STL中的各个类模板、函数模板的参数都是用这个体系中的概念来规定的
  • 使用STL模板时,类型参数既可以是C++标准库中已有的类型,也可以是自定义的类型,只要这些类型是所要求概念的模型
  • STL的基本组件:容器、迭代器、函数对象、算法
    • 迭代器是算法和容器的桥梁,将迭代器作为算法的参数,通过迭代器来访问容器而不是把容器直接作为算法的参数
    • 将函数对象作为算法的参数而不是将函数所执行的运算作为算法的一部分
    • 使用STL中提供的或自定义的迭代器和函数对象,配合STL的算法可以组合出各种各样的功能

image1

  • STL基本容器:
    • 在基本容器类的基础上,加上容器适配器,实现了更为复杂的数据结构,例如,Stack(栈)、queue(队列)、qriority_queue(优先队列)
    • 使用容器和容器适配器时需要包含相应的头文件

image2

  • 迭代器——泛型的指针:

    • 提供了顺序访问容器中每个元素的方法
    运算符 功能 ++ 获得指向下一元素的迭代器 * 访问迭代器所指向的元素 -> 访问元素的一个成员 – 获得指向上一元素的迭代器
    • 使用独立于STL容器的迭代器,需要包含头文件

    • 指针本身就是一种迭代器

  • 函数对象——泛化的函数:

    • 是一个行为类似函数的对象,可以像调用函数一样调用
    • 任何普通的函数和任何重载了"()" 的运算符的类的对象都可以作为函数对象使用
    • 使用STL的函数对象,需要包含头文件
  • 算法:

    • 可以广泛用于不同的对象和内置的数据类型
    • STL包含70多个算法,如排序、消除、计数、比较、变换、置换、容器管理等
    • 使用STL的算法,需要包含头文件

实例:求相反数

image3

3. 迭代器

迭代器是算法和容器的桥梁:

  • 迭代器用作访问容器中的元素
  • 算法不直接操作容器中的数据,而是通过迭代器间接操作

算法和容器独立:

  • 增加算法,无需影响容器的实现
  • 增加容器,原有的算法仍然适用

① 迭代器的分类与功能

迭代器 进行的操作 输入迭代器 从序列中读取数据 输出迭代器 向序列中写入数据 前向迭代器 既是输入迭代器又是输出迭代器,并且可以对序列单向遍历 双向迭代器 与前向迭代器类似,但在两个方向上都可以对数据进行遍历 随机访问迭代器 也是双向迭代器,但能够在序列中的任意两个位置间进行跳转

② 输入流输出流迭代器

  • 输入流迭代器:istream_iterator<T> ——以输入流(如cin)为参数构造,可用*(p++) 获得下一个输入的元素
  • 输出流迭代器:ostream_iterator<T> ——构造时需要提供输出流(如cout),可用*(p++) = x 将x输出到输出流
  • 二者都属于适配器
    • 适配器是用来为已有对象提供新的接口的对象
    • 输入输出流适配器为流对象提供了迭代器的接口
#include <iterator>#include <iostream>#include <algorithm>using namespace std;// 求平方的函数double square(double x){  return x * x;}int main(){  /* transform算法的作用是读入标准输入流,直到流结束的位置(windows下为Ctrl+Z),  再将读入的结果用square函数计算,并将结果输出给输出流迭代器 */  transform(istream_iterator<double>(cin),istream_iterator<double>(),    ostream_iterator<double>(cout,"\t"),square);  cout << endl;  return 0;}

③ 迭代器的区间

  • 两个迭代器表示一个区间:[p1,p2)
  • STL算法常以迭代器的区间作为输入,传递输入数据
  • 区间包含p1但不包含p2
template <class T, class InputIterator, class OutputIterator>void mySort(InputIterator first, InputIterator last, OutputIterator result){  // 通过输入迭代器将输入数据存入向量  vector<T> s;  for (;first != last; ++first)    s.push_back(*first);  // 对s进行排序,sort函数必须是随机访问迭代器  sort(s.begin(),s.end());  // 将s通过输出迭代器输出  copy(s.begin(),s.end(),result);}

④ 迭代器的辅助函数

  • advance(p,n) :对p执行n次自增操作
  • distance(first, last) :计算两个迭代器first和last的距离,即经过多少次 “++” 使first == last

4. 容器的基本功能与分类

① 容器的分类

  • 按元素的组织方式:顺序、关联
  • 按所关联的迭代器类型:可逆、随机访问

image4

  • 常用的容器:
容器类型 常用容器 顺序容器 array(数组)、vector(向量)、deque(双端队列)、forward_list(单链表)、list(列表) 有序关联容器 set(集合)、multiset(多重集合)、map(映射)、multimap(多重映射) 无序关联容器 unordered_set(无序集合)、unordered_multiset(无序多重集合)、unordered_map(无序映射)、unordered_multimap(无序多重映射)

② 容器的通用功能

  • 用默认的构造函数构造空容器
  • 支持关系运算符
  • begin()、end():获得容器首、尾迭代器
  • clear():将容器清空
  • empty():判断容器是否为空
  • size():得到容器元素的个数
  • s1.swap(s2):将s1和s2两容器互换
  • 对可逆容器的访问:
    • rbegin():指向容器尾的逆向迭代器
    • rend():指向容器首的逆向迭代器
    • s[n] :获得随机访问容器中指定元素

③ 相关数据类型(S表示容器类型)

  • S::iterator :指向容器元素的迭代器类型
  • S::const_iterator :常迭代器类型
  • S::reverse_iterator :逆向迭代器类型
  • S::const_reverse_iterator :逆向常迭代器类型

④ 顺序容器

顺序容器的性质与接口:

  • ①中表格中除数组外其它的顺序容器均可看做是一个长度可扩展的数组
  • 元素线性排列,可以随时在指定位置插入元素和删除元素
  • 必须符合Assignable这一概念
  • 数组对象的大小固定,单向链表有特殊的添加和删除操作
  • 接口:构造函数、赋值函数、插入函数、删除函数、首尾元素的直接访问、改变大小(上述接口总结不包含array和单向链表)

典型顺序容器的特点:

  • 向量:
    • 一个可扩展的动态数组,随机访问、在尾部插入或删除元素快,在头部或中间插入删除元素慢
    • 向量的容量是指实际分配空间的大小,s.capacity() 返回当前容量,s.reserve(n) 对s进行扩展 使其容量至少为n
  • 双端队列:
    • 在两端插入删除元素快,在中间插入删除慢元素,随机访问较快,但比向量容器慢
  • 列表:
    • 在任意位置插入和删除元素很快,不支持随机访问
    • 接合操作s1.splice(p,s2,q1,q2) 表示将s2中的 [q1,q2)移动到s1中p所指向的元素之前
  • 单向链表:
    • 单向链表每个结点只有指向下一个结点的指针,没有简单的方法来获取一个结点的前驱
    • 其插入和删除并不是对p1所指向的元素进行操作,二是对p1所指元素之后的结点进行操作,包括insert_after、emplace_after、erase_after 操作
    • 不支持size操作
  • 数组:
    • array是对内置数组的封装,提供了更安全、更方便的使用数组的方式
    • array的对象的大小时固定的,定义时需要制定元素类型和容器的大小
    • 不能动态改变容器的大小

顺序容器的插入迭代器和适配器:

  • 插入迭代器用于向容器的头部、尾部或中间制定位置插入元素的迭代器,包括前插、后插和任意位置插入迭代器
list<int> s;back_inserter iter(s);*(iter++) = 5;  // 通过iter把5插入s末尾
  • 适配器:以顺序容器为基础构建常用的数据结构,是对顺序容器的封装,可以构建栈、队列、优先级队列(最“大” 的元素最先被弹出)
    • 栈可以使用任意一种顺序容器,队列只允许用前插顺序容器(双端队列或列表),优先级队列基础容器必须是支持随机访问的容器
    • 栈和队列不支持迭代器,优先级队列不支持比较操作

⑤ 关联容器

关联容器的性质与接口:

  • 每个关联容器都有一个键,根据键高效地查找元素
  • 接口:插入、删除、查找、定界、计数

image5

  • C++11增加了无序关联容器,通过哈希函数和键类型的==运算符来组织元素,提供了与有序容器相同的操作,可以直接定义关键字是内置类型的无序容器,不可直接定义关键字的是自定义类的无序容器,如果需要则必须提供自定义的hash模板

典型关联容器的特点:

  • 集合(set):

    • 用来存储一组无重复的元素,集合元素本身是有序的,可以高效查找指定元素 ,也可以方便得到指定大小范围的元素在容器中所处的区间
    #include <set>#include <iterator>#include <utility>#include <iostream>using namespace std;int main(){set <double> s;while(true){  double v;  cin >> v;  // 输入0表示结束  if (v == 0) break;  pair<set<double>::iterator, bool> r = s.insert(v);  // 若v存在,给出提示  if (r.second)    cout << v << "is duplicated" << endl;}// 得到第一个元素的迭代器set <double>::iterator iter1 = s.begin();// 得到末尾的迭代器set <double>::iterator iter2 = s.end();// 求最大和最小元素的中值double medium = (*iter + *(--iter2))/2// 输出结果cout << "<=medium:";copy(s.begin(),s.upper_bound(medium),ostream _iterator<double>(cout," "));cout << endl;cout << ">=medium:";copy(s.begin(),s.lower_bound(medium),ostream _iterator<double>(cout," "));cout << endl;return 0;}
  • 映射:

    • 映射和集合同属单重关联容器(键唯一性),集合元素类型是键本身,而映射元素类型是由键和附加数据构成的二元组
    #include <iostream>#include <map>#include <cctype>using namespace std;int main(){map <char, int> s;char c;do {  cin >> c;  // 判断是否为字母  if (isalpha(c)){    c = tolower(c);  // 如果存在返回引用,不存在则插入    s[c]++;  } while(c != '.');  // 输出每个字母的出现次数  for (map<char, int>::iterator iter = s.begin(); iter != s.end(); ++iter)    cout << iter->first << " " << iter->second << " ";  cout << endl;  return 0;}}
  • 多重集合与多重映射:

    • 多重集合允许有重复元素的集合
    • 多重映射是允许一个键对应多个附加数据的映射

5. 函数对象

函数对象:行为类似函数的对象,可以没有参数,也可以带有若干参数,其功能是获取一个值或者改变操作状态

  • 普通函数就是函对象
  • 重载了() 运算符的类的实例也是函数对象

image6

#include <iostream>#include <numeric> // 包含数值算法头文件using namespace std;class MultClass{  public:    // 重载操作符()    int operator() (int x, int y) const {return x * y};}int main(){  int a[] = {1,2,3,4,5};  const int N = sizeof(a)/sizeof(int);  cout << "The result by multipling all elements in a is"    << accumulate(a, a+N, 1, MultClass()) // 将类传递给通用算法    << endl;  return 0;}
  • STL也提供了用于算术运算、关系运算和逻辑运算的函数对象

函数适配器:

  • 绑定适配器:bind1st、bind2nd,将n元函数对象的指定参数绑定为一个常数,得到n-1元函数对象
  • 组合适配器:not1、not2,将指定谓词结果取反
  • 函数指针适配器:对一般函数指针转化为函数对象,使之能够作为其他函数适配器的输入,在进行参数绑定或其他转换时,通常需要函数对象类型信息(函数指针形式无法获取)
  • 成员函数适配器:对成员函数指针使用,把n元成员函数适配为n+1元函数对象,该函数对象的第一个参数为调用该成员函数时的目的对象
#include <functional>#include <iostream>#include <vector>#include <algorithm>using namespace std;int main(){  int intArr[] = {30,90,10,40,70,50,20,80};  const int N = sizeof(intArr)/sizeof(int);  vector<int> a(intArr,intArr+N);  vector<int>::iterator p = find_if(a.begin(),a.end(),bind2nd(great<int>(),40));  if (p == a.end())    cout << "no element greater than 40" << endl;  else    cout << "first element greater than 40 is:" << *p << endl;  return 0;}

6.算法

STL算法:是一种函数模板,通过迭代器获得输入数据,通过函数对象对数据进行处理,通过迭代器将结果输出,是通用的,独立于具体的数据类型、容器类型

算法分类:

  • 不可变序列算法:不可直接修改所操作容器内容的算法,用于查找指定元素、比较两个序列是否相等、对元素进行计数等
  • 可变序列算法:可以修改所操作的容器对象,包括对序列进行复制、删除、替换、倒序、旋转、交换、分割、去重、填充、洗牌及生成一个序列的算法
  • 排序和搜索算法:对序列进行排序、对两有序序列进行合并、对有序序列进行搜索、有序序列的集合操作等
  • 数值算法:求序列元素和、部分和、相邻元素的差或量序列的内积,这些运算均可使用函数对象来指定,返回的迭代器指向输出序列最后一个元素的下一个元素