【C++】C++中的容器解析

目录结构:ios

contents structure [-]

1 顺序容器

1.1 顺序容器的种类

 

类型 描述
vector 可变大小数组。支持快速随机访问。在尾部以外的位置插入或删除元素可能很慢。
deque 双端队列。支持快速随机访问。在头尾位置插入/删除速度很快。
list 双向链表。只支持双向随机访问。在list中的任何位置插入/删除操做速度都很快。
forward_list 单向链表。只支持单向顺序访问。在链表任何位置插入/删除操做速度都很快。
array 固定大小数组。支持快速随机访问。不能添加或删除元素。
string 与vector相似的容器,单专门用于保存字符。随机访问快。在尾部插入/删除速度快。

 


除了array是固定大小外(不能插入/删除元素),其它容器都提供了高效、灵活的内存管理。

string和vector将元素保存在连续的内存空间中。因为元素是连续存储的,由元素的下标来计算其地址是很是快速的。可是,在这两种容器的中间位置插入或删除元素就会很是耗时:在一次插入或删除操做后,须要移动插入/删除位置以后的全部元素,来保持连续存储。并且,添加一个元素有时可能还须要分配额外的存储空间。在这种状况下,每一个元素都必需移动到新的存储空间。

list和forward_list都是链表结构,链表在容器的任何位置添加或删除操做都很是快速。做为代价,链表就不支持快速随机访问元素:在链表中,为了访问一个元素,须要遍历整个容器。与vector、deque和array相比,这两个容器的内存开销也很大。

deque是一个双端队列结构(队列数据结构:FIFO,First In First Out)。与vector/string相似,deque支持快速随机访问元素,在deque的中间位置插入或删除元素的代价(可能)很高。可是,在deque的两端添加或删除元素都是很快的。数组

 

1.2 顺序容器的操做

容器能够保存元素类型的限制

顺序容器几乎能够保存任意类型的元素。特别是,咱们能够定义一个容器,其元素的类型是另一种容器。这种容器的定义与其余容器类型彻底同样:在尖括号中指定元素类型。数据结构

vector<vector<string>> lines;//vector的vector

此处的lines是一个vector,其元素的类型是string的vector。
注意:较旧的编译器可能须要在尖括号之间键入空格,例如,vector<vector<string> >。

虽然咱们能够在元素中保存几乎任何类型,但某些容器对元素类型有本身的特殊要求。咱们能够为不支持特定操做需求的类型定义元素,这种状况下就只能使用那些没有特殊要求的容器操做了。

例如,顺序容器构造函数的一个版本接受容器大小参数,它使用了元素类型的默认构造函数。但某些类没有默认构造函数。咱们能够定义一个保存这种类型对象的容器,但咱们在构造这种容器时不能只传递给它一个元素数目参数:app

//假定 noDefault 是一个没有默认构造函数的类型
vector<noDefault> v1(10,init); // 正确:提供了元素初始化器
vector<noDefault> v2(10); //错误:必须提供一个元素的初始化器

vector<int> ivec(10,-1); //正确:提供了元素初始化器,10个int元素,每一个都初始化为-1
vector<string> svec(10,"Hi"); //正确:提供了元素初始化器,10个string元素,每一个都初始化为"Hi"
vector<int> ivec(10); //正确:int有默认初始化器,10个int元素,每一个都使用默认初始化器初始化为0
vector<string> svec(10) //正确:string有默认初始化器,10个string元素,每一个都使用默认初始化器初始化为空string


容器的添加、删除、移动、访问元素

标准库中的容器提供了大量的方法,每种容器之间既有相同的操做方法,也有不一样的操做方法,主要是依据容器的特性而定。好比:array容器是固定大小的,因此array容器不能有添加、删除操做方法。forward_list是单向链表结构,因此forward_list不支持push_back和emplace_back操做。

除此以外,每种容器之间既有相同点,也有差别点。这里笔者就不一一列举这些异同点了。下面笔者以vector容器展现容器的增、删、查、改操做:函数

#include <iostream>
#include <vector>
 
int main()
{
    // 建立一个包含数字类型的vector容器
    std::vector<int> v = {7, 5, 16, 8};
 
    //添加数字到vector容器中
    v.push_back(25);
    v.push_back(13);

    //插入元素
    std::vector<int>::iterator it = v.begin();
    it = v.insert(it, 200);

    // 迭代和打印vector容器中的内容
    for(int n : v) {
        std::cout << n << '\n';
    }

    //清除vector中的全部元素
    v.clear();
}

vector与其它容器提供了其它丰富的容器操做方法,详情能够参见标准库文档。性能

 

1.3 容器操做可能使迭代器失效

向容器中添加元素和从容器中删除元素的操做可能会使容器元素的指针、引用或迭代器失效。一个失效的指针、引用或迭代器再也不表示任何元素。使用失效的指针、引用或迭代器是一种严重的程序设计错误,颇有可能引发与未初始化指针同样的问题。ui

 

向容器添加元素后:

若是容器是vector或string,且存储空间被从新分配,则指向容器的迭代器、引用和指针都会失效。若是存储空间未从新分配,则指向插入位置以前的迭代器、指针或引用仍然有效,但指向插入位置以后元素的迭代器、指针户引用将会失效。spa

对于deque容器,插入到首位置以外的任何位置都会致使迭代器、指针或引用失效。若是在首元素位置插入元素,迭代器会失效,但指向存在的元素的引用和指针不会失效。设计

对于list和forward_list容器,指向容器的迭代器(包括尾后迭代器和首前迭代器)、指针或引用仍然有效。

当咱们从一个容器中删除元素后,指向被删除元素的迭代器、指针和引用都会失效,这应该不会惊讶。毕竟,这些元素已经被销毁了。指针

向容器删除元素后:

对于list和forward_list,指向容器其它位置的迭代器、指针和引用仍然有效。

对于deque,若是在首尾以外的任何位置删除元素,那么指向被删除元素外其余元素的迭代器、引用或指针也会失效。若是删除的是deque的尾元素,则尾后迭代器会失效,但其它迭代器、引用和指针不受影响;若是删除首元素,其它的元素不会受影响。

对于vector和string,指向被删除元素以前元素的迭代器、指针和引用仍然有效,指向被删除元素以后元素的迭代器、指针和引用失效。

因为向迭代器添加元素和删除元素后可能会使迭代器失效,所以必须保证每次改变容器的操做后都正确的从新定位迭代器,尤为是vector,string和deque容器。

例如:

//删除偶数元素,复制每一个奇数元素
vector<int> vi = {0,1,2,3,4,5,6,7,8,9};
vector<int>::iterator iter = vi.begin();
while(iter != vi.end()){
    if(*iter % 2){//是奇数
        iter = vi.insert(iter,*iter);//复制元素后,从新定位迭代器
        iter += 2;//向前移动迭代器,跳过当前元素以及插入到它以前的元素
    }else//是偶数
        iter = vi.erase(iter);//删除偶数元素,从新定位迭代器
}

 

1.4 Vector容器的增加机制

为了支持快速随机访问,vector将元素连续存储-每一个元素紧挨着前一个元素。

假定容器中的元素是连续存储的,且容器的大小是可变的,考虑向vector或string中添加元素会发生什么:若是没有空间容纳新元素,容器不可能简单地将它添加到内存中的其余位置-由于元素必须是连续存储的。容器必须分配新的内存空间来保存已有元素和新元素,将已有元素从旧位置移动到新空间中,而后添加新元素,释放旧存储空间。若是咱们每添加一个元素,vector就执行一次这样的内存分配和释放操做,性能就会慢到不可接受。

为了不这种代价,标准库实现者采用了能够减小容器空间从新分配次数的策略。当不得不获取新的内存空间时,vector和string的实现一般会分配比新的空间需求更大的内存空间。容器预留这些空间做为备用,可用来保存更多的新元素。这样,就不须要每次添加新元素都从新分配容器的内存空间了。

这种分配策略比每次添加新元素时都从新分配容器内存空间的策略要高效的多。其实际性能表现得也足够好-虽然vector在每次从新分配内存空间时都要移动全部的元素,但使用此策略后,其扩张操做一般比list和deque还快。

vector和string类型提供了一些成员函数,容许咱们与它的实现内存部分互动。

操做 描述
c.shrink_to_fit() 将capacity()减小为与size()相同大小
c.capacity() 不从新分配内存空间的话,c能够保存多少个元素
c.reserve(n) 分配至少能容纳n个元素的内存空间

shrink_to_fit只适用于vector,string,deque。capacity和reserve只适用与vector和string。reserve并不改变容器中元素的数量,它仅影响vector预先分配多大的内存空间。


capacity和size的区别,size表示它已经保存的元素的数目,capacity表示在不分配内存空间的前提下它最多能够保存多少个元素。
例如:

vector<int> ivec;
//size 应该为0,capacity依赖于具体的实现
cout << " ivec: size : " << ivec.size() << " capacity : " << ivec.capacity() << endl;

//向ivec添加24个元素。
for(vector<int>::size_type ix = 0; ix != 24 ;  ix ++)
    ivec.push_back(ix);

//size应该为24;capacity应该大于或等于24,具体值依赖标准库实现
cout << " ivec: size : " << ivec.size() << " capacity : " << ivec.capacity() << endl;
输出结果为:
ivec: size : 0 capacity : 0
ivec: size : 24 capacity : 32


咱们能够看出ivec的当前状态应该以下图所示:


如今能够预分配一些额外空间:

ivec.reserve(50); // 将capacity至少设置为50,可能会更大
//size应为为24;capacity应该大于等于50,具体值依赖标准库的实现
cout << " ivec: size : " << ivec.size() << " capacity : " << ivec.capacity() << endl

输出结果为

ivec: size : 24 capacity : 50


添加元素用光预留空间:

//添加元素用光多余容量
while(ivec.size() != ivec.capacity())
    ivec.push_back(0);
//capacity应该未改变,size和capacity相等
cout << " ivec: size : " << ivec.size() << " capacity : " << ivec.capacity() << endl;

输出结果为:

ivec: size : 50 capacity : 50

因为咱们用完了预留空间,所以不必为vector从新分配新的空间。实际上,只要没有超出vector容量,vector就不会添加新的元素。

若是咱们再添加一个元素,vector就不得不从新分配空间:

ivec.push_back(0);//再添加一个元素
//size应该为51,capacity应该大于等于51,具体值依赖标准库实现
cout << " ivec: size : " << ivec.size() << " capacity : " << ivec.capacity() << endl;

输出结果为:

ivec: size : 51 capacity : 100

这代表vector的实现采用的策略彷佛是在每次分配新的内存空间时将当前容量翻倍。

能够调用shrink_to_fit来要求vector将超出当前大小的多余内存退回给系统:

ivec.shrink_to_fit(); //要求归还内存
//size应该未改变,capacity的值依赖具体的标准库实现
cout << " ivec: size : " << ivec.size() << " capacity : " << ivec.capacity() << endl;

调用shrink_to_fit只是一个请求,标准库不保证退换内存。

 

1.5 容器适配器

除了顺序容器外,标准库还定义了三个顺序容器适配器:stack、queue 和 priority_queue。适配器是标准库的一个通用概念。容器、迭代器和函数都有适配器。本质上,适配器是一种机制,能使某种事物的行为看起来像另一种事物。一个容器适配器接受一种已有的容器类型,使其行为看起来像一种不一样的类型。例如:stack适配器接受一个顺序容器(除array或forward_list外),并使操做看起来像stack同样。

 

栈适配器

stack类型定义在stack头文件中。下面展现了如何使用stack:

stack<int> intStack; // 空栈
//填满栈
for(size_t ix = 0; ix != 10; ++ix)
    intStack.push(ix); // intStack 保存0到9十个数
while(!intStack.empty()){// intStack中有值就继续循环
    int value = intStack.top();
    //使用栈顶值的代码
    intStack.pop();//弹出栈顶元素,继续循环
}

其中声明语句

stack<int> intStack;//空栈

定义了一个保存整形元素的栈intStack,初始时为空。for循环将10个元素添加到栈中,这些元素被初始化从0开始连续的整数。while循环遍历整个stack,获取top值,将其从栈中弹出,直至栈空。

 

队列适配器

queue和priority_queue适配器定义在queue头文件中。
标准库queue是一种先进先出的存储和访问策略。进入队列的对象被安置到队尾,而离开队列的对象则从队首删除。饭店按照客人的到达顺序来为他们安排座位,就一个先进先出的案例。

 

下面展现了如何使用queue:

#include <queue>
#include <deque>
#include <iostream>

int main()
{
    std::queue<int> q;//空队列
    //填满队列
    for(size_t ix = 0; ix != 10; ++ix){
        q.push(ix);
    }
    while(!q.empty()){
        int value = q.front();
        //队列首位置的值
        std::cout << value << " ";
        q.pop();//弹出首位置的值
    }
    std::cout << std::endl;
}

输出结果:

0 1 2 3 4 5 6 7 8 9


priority_queue运行咱们为队列中的元素创建优先级。新加入的元素会安排在全部优先级低于它已有元素以前。饭店按照客人的预订时间而不是到达时间的迟早来为他们安排座位,就是一个队列优先的例子。priority_queue老是会优先输出较大元素,固然咱们也能够指定自定义的大小比较器。

下面展现了如何使用priority_queue:

#include <functional>
#include <queue>
#include <vector>
#include <iostream>

template<typename T> void print_queue(T& q) {
    while(!q.empty()) {
        std::cout << q.top() << " ";
        q.pop();
    }
    std::cout << '\n';
}

int main() {
    std::priority_queue<int> q;

    for(int n : {1,8,5,6,3,4,0,9,7,2})
        q.push(n);

    print_queue(q);

    std::priority_queue<int, std::vector<int>, std::greater<int> > q2;

    for(int n : {1,8,5,6,3,4,0,9,7,2})
        q2.push(n);

    print_queue(q2);

    // 使用lambda表达式比较元素
    auto cmp = [](int left, int right) {return (left ^ 1) < (right ^ 1);};
    std::priority_queue<int, std::vector<int>, decltype(cmp)> q3(cmp);

    for(int n : {0,1,2,3,4,5,6,7,8,9})
        q3.push(n);

    print_queue(q3);
}

输出结果:

9 8 7 6 5 4 3 2 1 0
0 1 2 3 4 5 6 7 8 9
8 9 6 7 4 5 2 3 0 1

 

2.关联容器

在上面咱们介绍了顺序容器,这一节介绍关联容器。关联容器和顺序容器有着更本的不一样:关联容器中的元素是按关键字来保存和访问的。与之相对,顺序容器中的元素是按它们在容器中的位置来保存和访问的。

2.1 关联容器的分类

按关键字有序保存元素(有序容器)

类型 描述
map 关联数组:保存关键字-值对
set 关键字即值,即只保存关键字的容器
multimap 关键字可重复出现的map
multiset 关键字可重复出现的set


按哈希值无序保存元素(无序容器)

类型 描述
unordered_map 用哈希函数组织的map
unordered_set 用哈希函数组织的set
unordered_multimap 哈希组织的map; 关键字能够重复
unordered_multiset 哈希组织的set; 关键字能够重复


类型map和multimap定义在头文件map中,set和multiset定义在头文件set中。按哈希值存储的无序容器则是定义在头文件unordered_map、unordered_set中。

2.2 关联容器操做

2.2.1 关联容器对关键字的要求

关联容器对其关键字类型有一些限制。对于无序容器关键字的要求将会在2.3.1阐述;这里就先说一下有序容器对关键字的要求。有序容器-map,multimap,set以及multiset,关键字类型必须定义元素比较的方法。在默认状况下,标准库使用关键字类型的<运算符来比较两个关键字。

例如:
fruit.h

#include <string>
//定义Fruit类
struct Fruit{
    std::string name;
    Fruit(std::string nm):name(nm){}
};
//定义比较操做符<
bool operator<(const Fruit &f1,const Fruit &f2){
    return f1.name < f2.name;
}

Test.cpp

#include <string>
#include "fruit.h"
#include <set>
using namespace std;
int main(int argc,char* argv[]){
    Fruit apple("apple");
    Fruit orange("orange");
    set<Fruit> fts;//使用set模板
    fts.insert(apple);
    fts.insert(orange);
    return 0;
}


除了像上面那样显示的定义<比较操做符,咱们也能够在构造set的时候传入一个比较函数。例如:
car.h

#pragma once
#include <string>
class Car{
    public:
    std::string name;
    Car(std::string nm):name(nm){}
};

test.cpp

#include "car.h"
#include <string>
#include <set>
using namespace std;
//compareCar用于比较两个car
bool compareCar(const Car& a,const Car& b){
    return a.name < b.name;
}
int main(int argc,char* argv[]){
    Car car1("BMW");
    Car car2("Benz");
    set<Car,decltype(compareCar)*> cars(compareCar);
//    也能够用以下的形式定义:
//    using f = bool(const Car&,const Car&);
//    set<Car,f*> cars(compareCar);

    cars.insert(car1);
    cars.insert(car2);
    return 0;
}

在咱们定义set时,使用了set<Car,decltype(compareCar)*>的模板类型,其中咱们使用了自定义的函数比较类型(应该是一种函数指针类型)。这次,咱们使用decltype来指出自定义操做的类型。记住,当用decltype来得到一个函数指针类型时,必须加上一个*来指出咱们要使用一个给定的函数类型的指针。

2.2.2 pair类型

pair类型在map,multimap,ordered_map,unordered_map容器中应用很是普遍,关于pair在它们中扮演的角色读者能够自行查阅文档。这里笔者介绍一下pair的使用语法。

pair标准库类型保存在容器utility中。一个pair保存两个数据成员,相似容器,pair是一个用来生成特定类型的模板。当建立一个pair时,咱们必须提供两个类型名,pair的数据成员将具备对应的类型。

pair<string,string> a;//保存两个string
pair<string,size_t> b;//保存一个string和一个size_t


pair的默认构造函数对数据成员进行值初始化,咱们也能够为每一个成员提供初始化器,pair的数据成员是public的,两个成员名分别是first和second:

pair<string,string> auther{"james","joyes"};
cout << auther.first << "\n";
cout << auther.second << endl;


下面在函数中返回一个pair:

pair<string,int> process(vector<string> &v){
    if(!v.empty())
        return {v.back(),v.back().size()};  // 列表初始化
    else
        return pair<string,int>();  //隐式构造返回值
}

 

2.2.3 关联容器迭代器

当解引用一个关联容器迭代器时,咱们会获得一个类型为容器的value_type的值的引用。对于map而言,value_type是一个pair类型,其first成员保存const的关键字,second成员保存值。

//words是map<string,size_t>类型
auto map_it = words.begin();
//map_it是map<string,size_t>::iterator类型
//*map_it是一个指向pair<const string,size_t>对象的引用
cout << map_it->first;
cout << map_it->second;
//map_it->first = "newKey";//错误,关键字是const,不能更改
++map_it->second;//正确


set的迭代器是const的,虽然set类型同时定义了iterator和const_iterator类型,但两种类型都只容许读取set中的元素。与不能改变map元素关键字同样,一个set中的关键字也是const的。能够用一个set迭代器来读取元素的值。

2.2.4 元素的访问、修改、添加和删除

标准库中提供了大量的操做方法,读者能够自行查阅文档,下面笔者简单列举两三个方法。

方法 描述
insert 向容器添加一个元素或一个元素范围
erase 删除一个元素或是元素范围
find 访问指定关键字处的元素

 

#include <set>/*set,multiset*/
#include <map>/*map,multimap*/
#include <string>/*string*/
#include <iostream>/*cout,cin*/
using namespace std;
int main(int argc,char* argv[]){
        cout << "set examples : " << endl;
        set<int> set_a;
        set_a.insert(10);
        set<int>::const_iterator set_a_iter = set_a.find(10);//set不支持at和[]操做
        cout << *set_a_iter << endl;
        set_a.erase(10);//删除元素

        cout << "multiset examples : " << endl;
        multiset<int> set_b;//能够存放相同的值
        set_b.insert(15);
        set_b.insert(15);
        set_b.insert(16);
        pair<multiset<int>::const_iterator,multiset<int>::const_iterator> set_b_pair = set_b.equal_range(15);
        while(set_b_pair.first != set_b_pair.second){
                cout << *set_b_pair.first << endl;
                ++set_b_pair.first;
        }

        cout << "map example : " << endl;
        map<string,size_t> map_a;
        map_a.insert(make_pair<string,size_t>("a",1));
        size_t map_a_val =  map_a["a"];
        map<string,size_t>::iterator map_a_iter = map_a.begin();
        cout << map_a_iter->first << ":" << map_a_iter->second << endl;
        pair<string,size_t> map_a_pair = *map_a_iter;

        cout << "multimap example : " << endl;
        multimap<string,size_t> map_b;//能够存放重复关键字的元素
        map_b.insert(pair<string,size_t>("b",2));
        map_b.insert(pair<string,size_t>("b",3));
        map_b.insert(make_pair<string,size_t>("c",4));
        pair<multimap<string,size_t>::iterator,multimap<string,size_t>::iterator> map_b_pair = map_b.equal_range("b");
        while(map_b_pair.first != map_b_pair.second){
                cout << map_b_pair.first->first << ":" << map_b_pair.first->second << endl;
                ++map_b_pair.first;
        }
    return 0;
}

 

2.3 无序容器

C++11中定义了4个无序关联容器(unordered associative container)。这些容器不是使用比较运算符来组织元素,而是使用一个哈希函数(hash function)和关键字类型的==运算符。在关键字类型的元素没有明显的序的状况下,无序容器很是有用的。

2.3.1 无序容器对关键字类型的要求

无序容器使用关键字类型的==运算符来比较元素,它们还使用一个hash<key_type>类型的对象来生成每一个元素的哈希值。标准库为内置类型(包括指针)提供了hash模板,还为一些标准库类型,包括string和智能指针类型提供了hash,咱们能够直接使用这些类型。

可是,若是是咱们自定义类型的无序容器,则咱们必需定义本身的hash模板或是提供默认的比较方法,不然不能做为无序容器的key。
例如:

#include <string>
#include <unordered_set>
using namespace std;
struct Foo{
        int val;
};
size_t hasher(const Foo& foo){
        return hash<int>()(foo.val);
}
bool eqOp(const Foo& fooa,const Foo& foob){
        return fooa.val == foob.val;
}
int main(int argc,char* argv[]){
        using Foo_multiset = unordered_multiset<Foo,decltype(hasher)*,decltype(eqOp)*>;
        Foo_multiset fm(10,hasher,eqOp);//10是容器的初始化桶的大小
    return 0;
}

若是为Foo提供了==操做符,那么就只须要提供hasher函数就能够了。

unordered_set<Foo,decltype(hasher)*> fooSet(10,hasher);

 

2.3.2 无序容器桶的管理

无序容器在存储上组织为一组桶(buckets),每一个桶保存零个或多个元素。无序容器使用一个哈希函数将元素映射到桶。为了访问一个元素,容器首先计算元素的哈希值,指出应该搜索那个桶。容器将具备一个特定哈希值的全部元素都保持在相同的桶中。若是容器容许重复关键字,全部具备相同关键字的元素也都会在同一个桶中。所以无序容器的性能依赖哈希函数的质量和桶的数量和大小。

无序容器提供了一组管理桶的函数,这些成员函数运行咱们查询容器的状态以及在必要时强制进行重组。

桶接口  
c.bucket_count() 当前建立桶的数量(并非每一个桶都有元素)
c.max_bucket_count() 容器能容纳桶的最大数量
c.bucket_size(n) 第n个桶有多少个元素
c.bucket(k) 关键字k在那个桶中
桶迭代  
local_iterator 访问桶中元素的迭代器类型
const_local_iterator 桶迭代器的const版本
c.begin(n),c.end(n) 桶n的首元素迭代器和尾后迭代器
c.cbegin(n),c.cend(n) 桶n的首元素const迭代器和尾后const迭代器
哈希策略  
c.load_factor() 每一个桶的平均数量
c.max_load_factor() c试图维护的桶的平均大小
c.rehash(n) 重组存储
相关文章
相关标签/搜索