C++ (Generic Programming)泛型编程与STL实现架构性讲解

C++ (Generic Programming)泛型编程与STL实现架构性讲解

特别说明:本文引用
[1]内容组织主要整理于 学堂在线的清华大学《C++语言程序设计进阶 (自主模式)》课程的 泛型编程章节 课程地址
[2]扩充概念主要参考 候捷的 《STL源码剖析》以及Matthew H.Austern.《Generic Programing and STL》
[3]部分参考百度百科

0. 泛型程序设计(GP)的基本概念

  • 编写不依赖于具体数据类型的程序
  • 将算法从特定的数据结构中抽象出来,使其通用
  • C++ 的模板为泛型程序设计奠定了关键基础
  • 与面向对象的区别:OOP关注的是编程的数据方面,而泛型编程关注的是算法

0.0 术语:概念

  • 用来界定具备一定功能的数据类型,列如
    1. “可比较大小的所有数据类型(有比较有比较运算符)”----Comparable 概念
    2. “具有共有的复制构造函数并可以用=复制的数据类型”----Assignable 概念
    3. “Comparable,且是Assignable”----Sortable 概念
  • 子概念:若A概念包含了B概念的含义,则称A是B的子概念,例如:
    1. Sortable 是 Comparable的子概念,也是Assignable的子概念

0.1 术语:模型(Model)

  • 模型:符合一个概念的数据类型 称为 该概念的模型,例如:
  • int型是Comparable概念的模型
  • 静态数组类型不是Assignable概念的模型(无法用=给整个静态数组赋值)

0.2 用概念做模板参数名(联系到STL)

  • 很多STL的实现代码就是使用概念来命名模板参数的
  • 为概念赋予一个名称,并使用改名称作为模板参数名
  • 例如,下述代码
template <class Sortable>
void insertionSort(Sortable a[], int n);
//插入排序算法函数,用Sortable作为模板参数名,意味要求,待排序的数组arr需要时Sortable概念的模型

1. STL简介(Standard Template Library)

1.1 STL 概要

  • 标准模板库(STL)定以了一套概念体系,为泛型程序设计(Generic Pragramming)提供了逻辑基础
  • STL中的各类模板,函数模板的参数都是用这个体系中的概念来规定的。
  • 使用STL的模板时,类型参数既可以是C++标准库中得已有类型,也可以是自定义的类型(要求自定的类型是所要求的概念的模型)

1.2 STL的六大基本组件及其关系

  • 容器(Container)
  • 迭代器(Iterators)
  • 函数对象(Function object/Functor):可以协助Algorithm完成策略变化
  • 算法(Algorithms):通过Iterator存取Container
  • 适配器(Adapters) :可以修饰或者套接Functor
  • 配置器(Allocators)
    STL的基本组件及其关系
  1. C++标准STL以源码形式供应,而非二进制可执行文件,且STL有不同的实现版本:HP, P.J.Plauger, Rouge Wave, STLport, SGI STL
    • 头文件安排:13个头文件:<algorithm.>、<deque.>、<functional.>、<iterator.>、<vector.>、<list.>、<map.>、<memory.h>、<numeric.>、<queue.>、<set.>、<stack.>和<utility.>
    • C++ 标准规定所有文件不再有拓展名,但是为了向下兼容,有些编译器同时提供<vector.h>和<vector.>两种形式

1.3 基本组件——容器(Container)

此处说明其框架结构关系,具体实现和特点参考数据结构在这里插入图片描述

  • 容纳、包含一组元素的对象。
  • 基本容器类模板
1.3.1 容器分类
容器类别 具体容器 功能
顺序容器 array(数组)(长度不可扩展)、vector(向量)、deque(双端队列)、forward_list(单链表)、list(列表) 看做长度可扩展的数组
(有序)关联容器 set(集合)、multiset(多重集合)、map(映射)、multimap(多重映射)
无序关联容器 unorderedset (无序集合)、unorderedmultiset(无序多重集合)unorderedmap(无序映射)、unordermultimap(无序多重映射)
容器适配器 stack(栈)、queue(队列)、priority_queue(优先队列)

在这里插入图片描述
在这里插入图片描述

1.3.2 容器功能
  • 容器的通用功能
    • 用默认构造函数构造空容器
    • 支持关系运算符:==、!=、<、<=、>、>=
    • begin()、end():获得容器首、尾迭代器
    • clear():将容器清空
    • empty():判断容器是否为空
    • size():得到容器元素个数
    • s1.swap(s2):将s1和s2两容器内容交换
  • 相关数据类型(S表示容器类型)
    • S::iterator:指向容器元素的迭代器类型
    • S::const_iterator:常迭代器类型
1.3.3 可逆容器的访问
  • STL为每个可逆容器都提供了逆向迭代器,逆向迭代器可以通过下面的成员函数得到:
    • rbegin() :指向容器尾的逆向迭代器
    • rend():指向容器首的逆向迭代器
  • 逆向迭代器的类型名的表示方式如下:
    • S::reverse_iterator:逆向迭代器类型
    • S::constreverseiterator:逆向常迭代器类型
1.3.4 随机容器的访问
  • 随机访问容器支持对容器的元素进行随机访问
    • s[n]:获得容器s的第n个元素
1.3.5 顺序容器功能接口
  • 构造函数
  • 赋值函数
    • assign
  • 插入函数
    • insert, pushfront(只对list和deque), pushback,emplace,emplace_front
  • 删除函数
    • erase,clear,popfront(只对list和deque) ,popback,emplace_back
  • 首尾元素的直接访问
    • front,back
  • 改变大小
    • resize

STL所提供的顺序容器各有所长也各有所短,我们在编写程序时应当根据我们对容器所需要执行的操作来决定选择哪一种容器。
如果需要执行大量的随机访问操作,而且当扩展容器时只需要向容器尾部加入新的元素,就应当选择向量容器vector;
如果需要少量的随机访问操作,需要在容器两端插入或删除元素,则应当选择双端队列容器deque;
如果不需要对容器进行随机访问,但是需要在中间位置插入或者删除元素,就应当选择列表容器list或forward_list;
如果需要数组,array相对于内置数组类型而言,是一种更安全、更容易使用的数组类型。

1.3.5 关联容器功能接口
  • 关联容器的特点
    • 每个关联容器都有一个键(key)
    • 可以根据键高效地查找元素
  • 接口
    • 插入:insert
    • 删除:erase
    • 查找:find
    • 定界:lowerbound、upperbound、equal_range
    • 计数:count
      在这里插入图片描述

四种关联容器

单重关联容器(set和map)
键值是唯一的,一个键值只能对应一个元素

多重关联容器(multiset和multimap)
键值是不唯一的,一个键值可以对应多个元素

简单关联容器(set和multiset)
容器只有一个类型参数,如set、multiset,表示键类型
容器的元素就是键本身

二元关联容器(map和multimap)
容器有两个类型参数,如map<K,V>、multimap<K,V>,分别表示键和附加数据的类型
容器的元素类型是pair<K,V>,即由键类型和元素类型复合而成的二元组
无序关联容器

C++11新标准中定义了4个无序关联容器

unorderedset、unorderedmap、unorderedmultiset、unorderedmultimap
不是使用比较运算符来组织元素的,而是通过一个哈希函数和键类型的==运算符。
提供了与有序容器相同的操作
可以直接定义关键字是内置类型的无序容器。
不能直接定义关键字类型为自定义类的无序容器,如果需要,必须提供我们自己的hash模板

1.4 基本组件——迭代器(Iterator)

1.4.1 概念特性
  1. 迭代器是泛化的指针,提供了顺序访问容器中每个元素的方法
    • 提供了顺序访问容器中每个元素的方法;
    • 可以使用“++”运算符来获得指向下一个元素的迭代器;
    • 可以使用“*”运算符访问一个迭代器所指向的元素,如果元素类型是类或结构体,还可以使用“->”运算符直接访问该元素的一个成员;
    • 有些迭代器还支持通过“–”运算符获得指向上一个元素的迭代器;
    • 迭代器是泛化的指针:指针也具有同样的特性,因此指针本身就是一种迭代器;
    • 使用独立于STL容器的迭代器,需要包含头文件
  2. 迭代器是算法和容器的桥梁
    • 迭代器用作访问容器中的元素
    • 算法不直接操作容器中的数据,而是通过迭代器间接操作
  3. 算法和容器独立
    • 增加新的算法,无需影响容器的实现
    • 增加新的容器,原有算法也能适用
1.4.2 迭代器分类

在这里插入图片描述

1.4.3 迭代器支持的操作
  • 迭代器是泛化的指针,提供了类似指针的操作(诸如++、*、->运算符)
  • 输入迭代器
    • 可以用来从序列中读取数据,如输入流迭代器
  • 输出迭代器
    • 允许向序列中写入数据,如输出流迭代器
  • 前向迭代器
    • 既是输入迭代器又是输出迭代器,并且可以对序列进行单向的遍历
  • 双向迭代器
    • 与前向迭代器相似,但是在两个方向上都可以对数据遍历
  • 随机访问迭代器
    • 也是双向迭代器,但能够在序列中的任意两个位置之间进行跳转,如指针、使用vector的begin()、end()函数得到的迭代器
1.4.5 迭代器的区间
  • 两个迭代器表示一个区间:***[p1, p2)***
  • STL算法常以迭代器的区间作为输入,传递输入数据
  • 合法的区间
    • p1经过n次(n > 0)自增(++)操作后满足p1 == p2
  • 区间包含p1,但不包含p2
1.4.5 迭代器的辅助函数
  • advance(p, n)
    对p执行n次自增操作
  • distance(first, last)
    计算两个迭代器first和last的距离,即对first执行多少次“++”操作后能够使得first == last

1.5 基本组件——函数对象(Function Object)

  • 一个行为类似函数的对象,对它可以像调用函数一样调用。
  • 函数对象是泛化的函数:任何普通的函数和任何重载了“()” 运算符的类的对象都可以作为函数对象使用
  • 可以没有参数,也可以带有若干参数
    • 普通函数就是函数对象
    • 重载了“()”运算符的类的实例是函数对象
  • 使用STL的函数对象,需要包含头文件

在这里插入图片描述

1.5.1 STL提供的函数对象

用于算术运算的函数对象:

一元函数对象(一个参数) :negate
二元函数对象(两个参数) :plus、minus、multiplies、divides、modulus

用于关系运算、逻辑运算的函数对象(要求返回值为bool)

一元谓词(一个参数):logical_not
二元谓词(两个参数):equalto、notequalto、greater、less、greaterequal、lessequal、logicaland、logical_o

利用STL标准函数对象

#include <iostream>   
#include <numeric>   //包含数值算法头文件
#include <functional>  //包含标准函数对象头文件
using namespace std;    
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, multiplies<int>())
         << endl; //将标准函数对象传递给通用算法
    return 0;
}

利用STL中的二元谓词函数对象

#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);
    cout << "before sorting:" << endl;
    copy(a.begin(),a.end(),ostream_iterator<int>(cout,"\t"));
    cout << endl;

    sort(a.begin(), a.end(), greater<int>());

    cout << "after sorting:" << endl;
    copy(a.begin(),a.end(),ostream_iterator<int>(cout,"\t"));
    cout << endl;
    return 0;
}

1.6 基本组件——适配器

1.6.1 绑定适配器:bind1st、bind2nd

将n元函数对象的指定参数绑定为一个常数,得到n-1元函数对象

  • binder2nd的实例构造通常比较冗长,bind2nd函数用于辅助构造binder2nd,产生它的一个实例。
  • binder1st和bind1st,将一个具体值绑定到二元函数的第一个参数。
#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(greater<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;
}

注:
find_if算法在STL中的原型声明为:
template<class InputIterator, class UnaryPredicate>
InputIterator find_if(InputIterator first, InputIterator last, UnaryPredicate pred);
它的功能是查找数组[first, last)区间中第一个pred(x)为真的元素。

1.6.2 组合适配器:not1、not2

将指定谓词的结果取反

  • 对于一般的逻辑运算,有时可能还需要对结果求一次逻辑反。
  • unarynegate和binarynegate实现了这一适配功能。STL还提供了not1和not2辅助生成相应的函数对象实例,分别用于一元谓词和二元谓词的逻辑取反。
#include <functional>
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;

bool g(int x, int y) {
    return x > y;
}

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;
    p = find_if(a.begin(), a.end(), bind2nd(ptr_fun(g), 40));
    if (p == a.end())
        cout << "no element greater than 40" << endl;
    else
        cout << "first element greater than 40 is: " << *p << endl;
    p = find_if(a.begin(), a.end(), not1(bind2nd(greater<int>(), 15)));
    if (p == a.end())
        cout << "no element is not greater than 15" << endl;
    else
        cout << "first element that is not greater than 15 is: " << *p << endl;

    p = find_if(a.begin(), a.end(), bind2nd(not2(greater<int>()), 15));
    if (p == a.end())
        cout << "no element is not greater than 15" << endl;
    else
        cout << "first element that is not greater than 15 is: " << *p << endl;
    return 0;
}
1.6.3 函数指针适配器:ptr_fun

将一般函数指针转换为函数对象,使之能够作为其它函数适配器的输入。

在进行参数绑定或其他转换的时候,通常需要函数对象的类型信息,例如bind1st和bind2nd要求函数对象必须继承于binary_function类型。但如果传入的是函数指针形式的函数对象,则无法获得函数对象的类型信息。

1.6.4 成员函数适配器:ptrfun、ptrfun_ref

对成员函数指针使用,把n元成员函数适配为n + 1元函数对象,该函数对象的第一个参数为调用该成员函数时的目的对象
也就是需要将“object->method()”转为“method(object)”形式。将“object->method(arg1)”转为二元函数“method(object, arg1)”。

#include <functional>
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

struct Car {
    int id;
    Car(int id) { this->id = id; }
    void display() const { cout << "car " << id << endl; }
};

int main() {
    vector<Car *> pcars;
    vector<Car> cars;
    for (int i = 0; i < 5; i++)
        pcars.push_back(new Car(i));
    for (int i = 5; i < 10; i++)
        cars.push_back(Car(i));
    cout << "elements in pcars: " << endl;
    for_each(pcars.begin(), pcars.end(), std::mem_fun(&Car::display));
    cout << endl;

    cout << "elements in cars: " << endl;
    for_each(cars.begin(), cars.end(), std::mem_fun_ref(&Car::display));
    cout << endl;

    for (size_t i = 0; i < pcars.size(); ++i)
        delete pcars[i];

    return 0;
}

1.7 基本组件——算法(Algorithm)

  • STL包括70多个算法
    例如:排序算法,消除算法,计数算法,比较算法,变换算法,置换算法和容器管理等
  • 可以广泛用于不同的对象和内置的数据类型。
  • 使用STL的算法,需要包含头文件。
1.7.1 STL 算法特点
  • STL算法本身是一种函数模版
    • 通过迭代器获得输入数据
    • 通过函数对象对数据进行处理
    • 通过迭代器将结果输出
  • STL算法是通用的,独立于具体的数据类型、容器类型

1.8 例10-1 从标准输入读入几个整数,存入向量容器,输出它们的相反数

template <class InputIterator, class OutputIterator, class UnaryFunction>
OutputIterator transform(InputIterator first, InputIterator last, OutputIterator result, UnaryFunction op) {
    for (;first != last; ++first, ++result)
        *result = op(*first);
    return result;
}

代码数据流图

InputIterator
OutputIterator
容器
transform 算法 用for 遍历两个迭代器指向的元素
容器
发布了17 篇原创文章 · 获赞 1 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/TowerOs/article/details/103957395