HaveFunWithEmbeddedSystem/Chapter2_C_与_C++/2.11_容器和模板.md

11 KiB
Raw Blame History

2.11 容器和模板

C++ 提供了一些用于泛型编程的工具,包括各种标准容器、迭代器,以及用于实现容器的模板类等。所谓泛型编程,就是指所开发的算法不依赖于特定的数据类型。泛型编程使得程序代码更具有通用性,例如之前针对 short 类型的查找算法,不需要修改就可以应用到 int 类型的数据上。
可以通过 C++ 模板来自行实现一些泛型算法,另外一些标准的泛型算法如排序、查找等都已经被集成在标准模板库中,可以直接使用。

2.11.1 C++ 标准模板库 STL

C++ 标准模板库,提供了很多常用的容器和算法。以往在 C 语言中需要自己实现的排序算法,在 C++ 中只要使用 vector 容器和 sort() 方法即可轻松实现。

就像算法常常以数据结构为基础一样STL 中的算法,主要围绕容器来实现。而容器则是以模板为基础实现的。最常用的有 10 大标准容器,分为 3 大类:

  1. 线性容器

    向量(vector)
    列表(list)
    双端队列(deque)

  2. 适配器容器

    堆栈(stack)
    队列(queue)
    优先队列(priority_queue)

  3. 关联容器

    映射(map)
    多重映射(mutimap)
    集合(set)
    多重集合(mutiset)

下面以 Vector、List、Map 和 Set 为例,对标准容器进行介绍。

2.11.1.1 Vector

是一个线性顺序结构。相当于数组,但其大小可以不预先指定,并且自动扩展。它可以像数组一样被操作,由于它的特性我们完全可以将 vector 看作动态数组。

在创建一个 vector 后,它会自动在内存中分配一块连续的内存空间进行数据存储,初始的空间大小可以预先指定也可以由 vector 默认指定,这个大小即 capacity() 函数的返回值。当存储的数据超过分配的空间时 vector 会重新分配一块内存块,但这样的分配是很耗时的,在重新分配空间时它会做这样的动作:

  1. 首先vector 会申请一块更大的内存块;
  2. 然后,将原来的数据拷贝到新的内存块中;
  3. 其次,销毁掉原内存块中的对象(调用对象的析构函数);
  4. 最后,将原来的内存空间释放掉。

如果 vector 保存的数据量很大时,这样的操作一定会导致糟糕的性能(这也是 vector 被设计成比较容易拷贝的值类型的原因)。所以说 vector 不是在什么情况下性能都好,只有在预先知道它大小的情况下 vector 的性能才是最优的。

vector 的特点:

  1. 指定一块如同数组一样的连续存储,但空间可以动态扩展。即它可以像数组一样操作,并且可以进行动态操作。通常体现在 push_back() pop_back();
  2. 随机访问方便,它像数组一样被访问,即支持操作符和 vector.at();
  3. 节省空间,因为它是连续存储,在存储数据的区域都是没有被浪费的,但是要明确一点 vector 大多情况下并不是满存的,在未存储的区域实际是浪费的;
  4. 在内部进行插入、删除操作效率非常低,在有频繁插入/删除操作的情况下,往往不使用 vector;
  5. 只能在 vector 的最后进行 push 和 pop ,不能在 vector 的头进行 push 和pop ;
  6. 当动态添加的数据超过 vector 默认分配的大小时要进行内存的重新分配、拷贝与释放,这个操作非常消耗性能。 所以要 vector 达到最优的性能,最好在创建 vector 时就指定其空间大小。

2.11.1.2 List

是一个线性链表结构,它的数据由若干个节点构成,每一个节点都包括一个信息块(即实际存储的数据)、一个前驱指针和一个后驱指针。它无需分配指定的内存大小且可以任意伸缩,这是因为它存储在非连续的内存空间中,并且由指针将有序的元素链接起来。

由于其结构的原因list 随机检索的性能非常的不好,因为它不像 vector 那样直接找到元素的地址,而是要从头一个一个的顺序查找,这样目标元素越靠后,它的检索时间就越长。检索时间与目标元素的位置成正比。

虽然随机检索的速度不够快,但是它可以迅速地在任何节点进行插入和删除操作。因为 list 的每个节点保存着它在链表中的位置,插入或删除一个元素仅对最多三个元素有所影响,不像 vector 会对操作点之后的所有元素的存储地址都有所影响,这一点是 vector 不可比拟的。

list 的特点:

  1. 不使用连续的内存空间这样可以随意地进行动态操作;
  2. 可以在内部任何位置快速地插入或删除,当然也可以在两端进行 push 和pop
  3. 不能进行内部的随机访问,即不支持操作符和 list.at()
  4. 相对于 verctor 占用更多的内存。

2.11.1.3 Map

Map 属于关联容器,是一种非线性的树结构,具体的说采用的是一种比较高效的特殊的平衡检索二叉树。

Map 提供一种“键- 值”关系的一对一的数据存储能力。其“键”在容器中不可重复,且按一定顺序排列。

关联容器的特点是明显的,相对于顺序容器,有以下几个主要特点:

  1. 其内部实现是采用非线性的二叉树结构,具体的说是红黑树的结构原理实现的;
  2. set 和 map 保证了元素的唯一性mulset 和 mulmap 扩展了这一属性,可以允许元素不唯一;
  3. 元素是有序的集合,默认在插入的时候按升序排列。

2.11.2 迭代器

迭代器是一个对象,可以循环访问 STL 容器中的元素,并提供对各个元素的访问。 STL 容器全都提供迭代器,以便算法可以采用标准方式访问其元素,而不必考虑用于存储元素的容器类型。

可以通过使用成员和全局函数(如 begin() 和 end())以及运算符(如 ++ 和 -- )向前或向后移动,来显式使用迭代器。 还可以通过范围 for 循环或(对于某些迭代器类型)下标运算符 [],来隐式使用迭代器。

在 STL 中,序列或范围的开头是第一个元素。 序列或范围的末尾始终定义为最后一个元素的下一个位置。

2.11.3 STL 容器示例

vector、list、map 以及迭代器的示例如下:

/**
 * @file main.cpp
 */
#include <vector>
#include <list>
#include <map>
#include <iostream>

using namespace std;

void ExpOfVector(void)
{
    vector<double> MyVec;

    MyVec.push_back(3.14*2);
    MyVec.resize(0);
    MyVec.push_back(3.14);
    MyVec.push_back(3.14/2);
    MyVec.push_back(3.14/4);
    MyVec.push_back(3.14/8);
    MyVec.insert(MyVec.begin()+1, 5.0);             // 在 MyVec 的第1个元素从第0个算起的位置插入数值为 5.0 的元素.

    MyVec.pop_back();
    cout<<"Vector Size="<<MyVec.size()<<endl;       // 输出 4.
    cout<<"Vector First="<<MyVec.front()<<endl;     // 返回 MyVec 的第一个元素 , 即 3.14.
    cout<<"Vector Last="<<MyVec.back()<<endl;       // 返回 MyVec 的最后一个元素, 即 3.14/4.
    cout<<"Vector At 1="<<MyVec.at(1)<<endl;        // 返回 MyVec 的第二个元素, 即 5.0.

    // 依次输出 MyVec 中的每个元素值.
    for(vector<double>::iterator itr=MyVec.begin();
        itr!=MyVec.end();
        itr++)
    {
        cout<<*itr<<endl;
    }

    MyVec.clear();
    cout<<"Vector Size="<<MyVec.size()<<endl;
    cout<<"---------------------------------------"<<endl;
}

void ExpOfList(void)
{
    list<double> MyList(5);

    MyList.clear();

    MyList.push_front(3.14*2);
    MyList.push_front(3.14*3);
    MyList.push_front(3.14*4);
    MyList.push_back(3.14*1);

    // 依次输出 MyList 中的每个元素值: 3.14*4、3.14*3、3.14*2、3.14*1.
    for(list<double>::iterator itr=MyList.begin();
        itr!=MyList.end();
        itr++)
    {
        cout<<*itr<<endl;
    }

    MyList.pop_back();
    MyList.pop_front();

    // 依次输出 MyList 中的每个元素值: 3.14*3、3.14*2.
    for(list<double>::iterator itr=MyList.begin();
        itr!=MyList.end();
        itr++)
    {
        cout<<*itr<<endl;
    }

    MyList.insert(MyList.begin()++, 5.0);

    // 依次输出 MyList 中的每个元素值: 5.0、3.14*3、3.14*2.
    for(list<double>::iterator itr=MyList.begin();
        itr!=MyList.end();
        itr++)
    {
        cout<<*itr<<endl;
    }

    cout<<"Vector Size="<<MyList.size()<<endl;

    cout<<"---------------------------------------"<<endl;
}

void ExpOfMap(void)
{
    map<int, string> MyMap;

    MyMap.insert(map<int, string>::value_type(1, "Student1"));
    MyMap.insert(map<int, string>::value_type(2, "Student2"));
    MyMap[3] = "Student3";

    MyMap.erase(2);     // 删除 Key 值为 2 的元素.

    map<int, string>::iterator itr;

    itr = MyMap.find(3);
    if(itr != MyMap.end())
    {
        cout<<"The Key="<<itr->first<<endl;
        cout<<"The Value="<<itr->second<<endl;
    }
    else
        cout<<"Do not Find"<<endl;

    itr = MyMap.find(2);
    if(itr != MyMap.end())
    {
        cout<<"The Key="<<itr->first<<endl;
        cout<<"The Value="<<itr->second<<endl;
    }
    else
        cout<<"Do not Find"<<endl;
    cout<<"---------------------------------------"<<endl;
}

int main(void)
{
    ExpOfVector();
    ExpOfList();
    ExpOfMap();
    return 0;
}

2.11.4 模板类

使用模板的目的就是能够让程序员编写与类型无关的代码。比如编写了一个交换两个整型 int 类型的 swap 函数,这个函数就只能实现 int 型,对 double字符这些类型无法实现要实现这些类型的交换就要重新编写另一个 swap 函数。使用模板的目的就是要让这程序的实现与类型无关,比如一个 swap 模板函数,即可以实现 int 型,又可以实现 double 型的交换。

模板的声明或定义只能在全局,命名空间或类范围内进行。

函数模板的格式为:

template<class 形参名, class 形参名, ...> 返回类型 函数名(参数列表)
{
    ...
}

类模板的格式为:

template<class 形参名, class 形参名, ...> class 类名
{
    ...
};

一个简单的示例如下:

/**
 * @file    ATemplate.h
 */
#pragma once        // 只被编译一次.

template<class T> class A{
public:
    A();
    T Add(T a,T b);
};

#endif
/**
 * @file    ATemplate.cpp
 */
#include "ATemplate.h"
#include <iostream.h>

template<class T> A<T>::A()
{
}

template<class T> T A<T>::Add(T a,T b)
{
    return a+b;
}
/**
 * @file    main.cpp
 */
#include "ATemplate.h"

void main(){
    A<int> a;
    cout<<a.Add(2, 3)<<endl;
}

练习

  1. 基于模板实现堆栈操作。
  2. 使用 Vector 进行从小到大排序。