《STL源码剖析》学习笔记-第4章 序列式容器

1、vector

1、vector特性

(1)vector有自动扩容操作,每次扩容伴随着“配置新空间 / 移动旧数据 / 释放旧空间”的操作,因此有一定时间成本

(2)vector提供了reserve接口,如果能够对元素个数有大概了解,可以一开始就分配合适的空间。

(3)vector的内存空间是连续的,对插入元素的操作而言,在vector尾部插入才是合适的选择。维护的是一个连续线性空间,所以vector支持随机存取。

(4)vector动态增加大小时,并不是在原空间之后持续新空间(无法保证原空间之后尚有可供配置的空间),而是以原大小的2倍另外配置一块较大的空间,接着将原内容拷贝过来,然后才开始在原内容之后构造新元素,并释放原空间。因此,对vector的任何操作,一旦引起空间重新配置,指向原vector的所有迭代器就都失效了。

2、vector与array的区别

vector与array非常相似。两者的唯一区别在于空间运用的灵活性。array是静态空间,一旦配置了就不能改变;vector是动态空间,随着元素的加入,它的内部机制会自行扩充空间以容纳新元素。因此,vector的运用对于内存的合理利用与运用的灵活性有很大的帮助。

3、vector迭代器和数据结构

vector的迭代器是普通指针。因为vector迭代器需要的操作行为,如operator*,operator->,operator++,operator+,operator+=

等,普通指针天生具备;vector支持随机存取,普通指针正有这种能力。所以vector提供的是Random Access Iterators。

vector所采用的数据结构非常简单:线性连续空间。为了降低空间配置时的速度成本,vector实际配置的大小可能比客户端需求量更大一些,以备将来可能的扩充,这便是容量的观念。增加新元素时,如果容量不足,则扩充至2倍(若原大小为0,则配置为1),2倍容量仍不足,就扩充至足够大的容量。

部分操作示例:

#include <vector>
#include <algorithm>
#include <iostream>
using namespace std;
int main()
{
    vector<int> iv(2,9);
    cout << "size=" << iv.size()<< endl;  // size=2
    cout << "capacity=" << iv.capacity()<< endl;  // capacity=2
    iv.push_back(1);
    cout << "size=" << iv.size() << endl;  // size=3
    cout << "capacity=" << iv.capacity() << endl;  // capacity=4
    iv.push_back(2);
    cout << "size=" << iv.size() << endl;  // size=4
    cout << "capacity=" << iv.capacity() << endl;  // capacity=4
    //支持指定位置存取。下面示例随机存。
    vector<int>::iterator ite  = find(iv.begin(), iv.end(), 1);
    if (ite != iv.end())
        iv.insert(ite,3,7);//在指针ite处,加入3个7
    cout << "size=" << iv.size() << endl;  // size=7
    cout << "capacity=" << iv.capacity() << endl;  // capacity=8
    for(int i=0; i<iv.size(); ++i)
        cout << iv[i] << ‘ ‘;  // 9 9 7 7 7 1 2
    cout << endl;
    iv.clear();//清空
    cout << "size=" << iv.size() << endl;  // size=0
    cout << "capacity=" << iv.capacity() << endl;  // capacity=8
    return 0;
}

2、list

1、list特性

(1)相较于vector的连续线性空间,list就显得复杂许多。

(2)它的好处是每次插入或删除一个元素,就配置或释放一个元素空间。因此,list对于空间的运用有绝对的精准,一点也不浪费。

(3)对于任何位置的元素插入或元素移除,list永远是常数时间。

(4)list不仅是一个双向链表,而且还是一个环状双向链表。它只需要一个指针便可完整表现整个链表。

(5)插入操作和接合操作都不会造成原有的list迭代器失效,这在vector是不成立的。因为vector的插入操作可能造成记忆体重新配置,导致原有的迭代器全部失效。甚至list的元素删除操作(erase),也只有“指向被删除元素”的那个迭代器失效,其他迭代器不受任何影响。

(6)list不再能够像vector那样以普通指针作为迭代器,因为其节点不保证在储存空间中连续。list提供的是Bidirectional Iterators。

2、list的构造与内存管理

list缺省使用alloc作为空间配置器,并据此定义了一个list_node_allocator以节点大小为配置单位

#include <list>
#include <iostream>
#include <algorithm>
using namespace std;

int main()
{
    list<int> ilist;
    cout << "size=" << ilist.size() << endl;  // size=0
    ilist.push_back(0);
    ilist.push_back(1);
    ilist.push_back(2);
    ilist.push_back(3);
    ilist.push_back(4);
    cout << "size=" << ilist.size() << endl;  // size=5
    list<int>::iterator ite;//声明迭代器
    for(ite = ilist.begin(); ite != ilist.end(); ++ite)
        cout << *ite << ‘ ‘;  // 0 1 2 3 4
    cout << endl;
    //插入和接合操作不会造成原迭代器失效
    ite = find(ilist.begin(), ilist.end(), 3);
    if (ite != ilist.end())
        ilist.insert(ite, 99);//在以前3的位置处插入一个数99,
                              //插入完成后,新节点位于ite所指节点前方
    cout << "size=" << ilist.size() << endl;  // size=6
    cout << *ite << endl;  // 3 !!!!

    for(ite = ilist.begin(); ite != ilist.end(); ++ite)
        cout << *ite << ‘ ‘;  // 0 1 2 99 3 4
    cout << endl;
    //删除操作时,指向被删除元素的那个迭代器失效,其他不受影响
    ite = find(ilist.begin(), ilist.end(), 1);
    if (ite != ilist.end())
        cout << *(ilist.erase(ite)) << endl;// 2 !!!
    cout<<*ite<<endl;// 1 !!!
    cout<<*(ite++)<<endl;// 1 !!!
    for(ite = ilist.begin(); ite != ilist.end(); ++ite)
        cout << *ite << ‘ ‘;  // 0 2 99 3 4
    cout << endl;

    return 0;
}

3、list的元素操作

(1)push_front, push_back,
(2)erase(iterator position), //移除迭代器position所指节点
(3)pop_front, pop_back,
(4)clear,
(5)remove(const T& value), //将数值为value的所有元素移除
(6)unique, //移除数值相同的连续元素,只剩下一个。注意相同而连续。
(7)splice, //接合操作。将某连续范围的元素从一个list移动到另一个(或同一个)list的某个定点。
(8)merge, //将x合并到*this身上。两个lists内容必须先递增排序。
(9)reverse,
(10)sort()//list不能使用STL算法sort(),必须使用自己的sort()成员函数,因为STL算法sort()只接受RamdonAccessIterator。list中sort()函数采用quick sort。

splice()、reverse()、sort()操作示例:

int iv[5] = { 5,6,7,8,9 };
list<int> ilist2(iv, iv+5);
// 目前,ilist 的內容为0 2 99 3 4
ite = find(ilist.begin(), ilist.end(), 99);//在99前面插入
ilist.splice(ite,ilist2);  // 0 2 5 6 7 8 9 99 3 4
ilist.reverse(); // 4 3 99 9 8 7 6 5 2 0
ilist.sort(); // 0 2 3 4 5 6 7 8 9 99

3、deque

1、deque与vector的区别

(1)vector是单向开口的连续线性空间,用户只能在vector尾部进行插入删除操作(也允许在某个pos处插入,但由于vector的底层实现是数组,过多非队尾位置的插入会有性能上的消耗)。而deque是一种双向开口的连续线性空间,允许在头尾两端分别做插入和删除操作。

(2)deque允许在常数时间内对起头端进行元素的插入或移除操作。

(3)deque没有所谓容量概念。它是动态地用分段连续的空间组合而成,随时可以增加一段新的空间并连接起来。没有必要提供所谓的空间保留(reserve)功能。

(4)deque的最大任务,便是在这些分段的定量连续空间上,维护其整体连续的假象,并提供随机存取的接口。避开了“重新配置、复制、释放”的轮回,代价则是复杂的迭代器架构

(5)既是分段连续线性空间,就必须有中央控制,而为了维持整体连续的假象,数据结构的设计及迭代器前进后退等操作都颇为繁琐。deque的实现代码分量远比vector或list都多得多。所以,我们应尽可能选择vector而非deque。

2、deque的中控器

deque采用一块所谓的map(注意,不是STL的map容器)作为主控。这里所谓map是一小块连续空间,其中每个元素(此处称为一个节点,node)都是指针,指向另一段(较大的)连续线性空间,称为缓冲区。缓冲区才是deque的储存空间主体。SGI STL 允许我们指定缓冲区大小,默认值0表示将使用512 bytes 缓冲区。

总结:

(1)map是块连续空间,其内的每个元素都是一个指针,指向一块缓冲区。

(2)进一步发现,map其实是一个T**,也就是说它是一个指针,所指之物又是一个指针,指向型别为T的一块空间。

3、deque的迭代器

deque是分段连续空间。维持其”整体连续”假象的任务,落在了迭代器的operator++和operator–两个运算子身上。

deque的迭代器应该具备什么结构:

(1)它必须能够指出分段连续空间(亦即缓冲区)在哪里

(2)它必须能够判断自己是否已经处于其所在缓冲区的边缘,如果是,一旦前进或后退就必须跳跃至下一个或上一个缓冲区。为了能够正确跳跃,deque必须随时掌握管控中心(map)。

所以在迭代器中需要定义:当前元素的指针,当前元素所在缓冲区的起始指针,当前元素所在缓冲区的尾指针,指向map中指向所在缓冲区地址的指针。

deque在效率上不如vector,因此有时候在对deque进行sort的时候,需要先将元素移到vector再进行sort,然后移回来。

4、deque的构造与内存管理

由于deque的设计思想就是由一块块的缓存区连接起来的,因此它的内存管理会比较复杂。插入的时候要考虑是否要跳转缓存区、是否要新建map节点(和vector一样,其实是重新分配一块空间给map,删除原来空间)、插入后元素是前面元素向前移动还是后面元素向后面移动(谁小移动谁)。而在删除元素的时候,考虑是将前面元素后移覆盖需要移除元素的地方还是后面元素前移覆盖(谁小移动谁)。移动完以后要析构冗余的元素,释放冗余的缓存区。

#include <deque>
#include <iostream>
#include <algorithm>

using namespace std;
int main()
{
    deque<int,alloc,32> ideq(20,9);  // 注意,alloc 只適用於G++
    cout << "size=" << ideq.size() << endl;  // size=20
    // 现在,已经构造了一个deque,有20个int元素,初值皆为9
    //每个缓冲区大小为32 bytes
    for(int i=0; i<ideq.size(); ++i)
        ideq[i]= i;
    for(int i=0; i<ideq.size(); ++i)
        cout << ideq[i] << ‘ ‘;  // 0 1 2 3 4 5 6...19
    cout << endl;
    // 在最尾端增加3個元素,其值為0,1,2
    for(int i=0;i<3;i++)
        ideq.push_back(i);
    for(int i=0; i<ideq.size(); ++i)
        cout << ideq[i] << ‘ ‘;  // 0 1 2 3 ... 19 0 1 2
    cout << endl;
    cout << "size=" << ideq.size() << endl;  // size=23
    // 在最尾端增加1個元素,其值為3
    ideq.push_back(3);
    for(int i=0; i<ideq.size(); ++i)
        cout << ideq[i] << ‘ ‘;  // 0 1 2 3 ... 19 0 1 2 3
    cout << endl;
    cout << "size=" << ideq.size() << endl;  // size=24
    // 在最前端增加1個元素,其值為99
    ideq.push_front(99);
    for(int i=0; i<ideq.size(); ++i)
        cout << ideq[i] << ‘ ‘;  // 99 0 1 2 3...19 0 1 2 3
    cout << endl;
    cout << "size=" << ideq.size() << endl;  // size=25
    // 在最前端增加2個元素,其值分別為98,97
    ideq.push_front(98);
    ideq.push_front(97);
    for(int i=0; i<ideq.size(); ++i)
        cout << ideq[i] << ‘ ‘;  // 97 98 99 0 1 2 3...19 0 1 2 3
    cout << endl;
    cout << "size=" << ideq.size() << endl;  // size=27
    // 搜尋數值為99的元素,並列印出來。
    deque<int,alloc,32>::iterator itr;
    itr = find(ideq.begin(), ideq.end(), 99);
    cout << *itr << endl;  // 99
    cout << *(itr.cur) << endl;  // 99
    return 0;
}

另外,deque 的元素操作也包括pop_back, pop_front, clear, erase, insert等。

时间: 2024-10-07 18:28:24

《STL源码剖析》学习笔记-第4章 序列式容器的相关文章

stl源码剖析学习笔记(二)traits编程技法简明例程

解释说明 traits侯捷老师的翻译是萃取.其目的就是在编译期进行模板调用的类型识别,从而做一些事情. 最突出的例子,我觉得不是<STL源码剖析>中"迭代器概念与traits编程技法"这一章的说明,而是stl算法中copy的实现.代码在stl源码的stl_algobase.h中. copy的最终实现,大致分为两类,一类是直接整块内存的memmove操作,另一类是一个个对象赋值.其中涉及has_trivial_assignment_operator的类型推断. 如果has_t

c++ stl源码剖析学习笔记(一)

template <class InputIterator, class ForwardIterator>inline ForwardIterator uninitialized_copy(InputIterator first, InputIterator last,ForwardIterator result) 函数使用示例 #include <algorithm> #include <iostream> #include <memory> #inclu

STL源码剖析 学习笔记

目录: 第二章 空间适配器 第三章 迭代器 第四章 序列式容器(vector,list,deque,stack,heap,priority_queue,slist) 第五章 关联式容器(树的算法 + RB_tree ,set,map,hashtable) 第六章 算法 第七章 仿函数 第八章 适配器(adapet) 第二章 空间适配器 具有次配置力的SGI空间适配器,STL allocator将对象的内存分配和初始化分离开,内存配置由alloc:allocate 负责,内存释放由dealloca

重温《STL源码剖析》笔记 第三章

第三章:迭代器概念与traits编程技法 迭代器是一种smart pointer auto_Ptr 是一个用来包装原生指针(native pointer)的对象,声明狼藉的内存泄漏问题可藉此获得解决. auto_ptr用法如下,和原生指针一模一样: void func() { auto_ptr<string> ps(new string("jjhou")); cout << *ps << endl; //输出:jjhou cout <<

重温《STL源码剖析》笔记 第四章

源码之前,了无秘密  ——侯杰 第四章:序列式容器 C++语言本身提供了一个序列式容器array array:分配静态空间,一旦配置了就不能改变. vector: 分配动态空间.维护一个连续线性空间,迭代器类型为:Random Access Iterators 空间配置 typedef simple_alloc<value_type, Alloc> data_allocator 所谓动态增加空间大小,并不是在原空间之后接续新空间,而是以原大小的两倍另外配置一块较大 的空间,然后将原内容拷贝过来

c++ stl源码剖析学习笔记(二)iterator auto_ptr(老版本书籍示例 新版本C++中已经废除此概念)

ITERATOR template<class InputIterator,class T> InputIterator find(InputIterator first,InputIterator last,const T& value) { while(first != last && *first != value) ++first; return first; } 代码示例 1 #include <iostream> 2 #include <v

STL 源码剖析读书笔记三:序列式容器之 vector、list

1. STL 中的容器 容器,置物之所也.STL 容器即是将运用最广的一些数据结构实现出来.如下图所示: 上图以内缩方式来表达基层和衍生层的关系.所谓衍生,并非派生关系,而是内含关系.例如 heap 内含一个 vector,priority-queue 内含一个 heap.stack.queue都内含一个 deque,set/map/multimap/multiset 都内含一个 RB-tree,hash_x 都内含一个 hashtable. 2. 序列式容器之 vector 所谓序列式容器,其

STL 源码剖析读书笔记四:序列式容器之 deque、stack、queue

1. 序列式容器 deque 1.1 deque 概述 vector是单向开口的连续线性空间,用户只能在vector尾部进行插入删除操作,而 deque 是一种双向开口的连续线性空间,允许我们在头尾两端操作. deque 和 vector 的最大差异在于: deque 允许常数时间对头端元素进行插入和移除操作 deque 没有所谓容量(capacity)概念,因为它是动态地以分段连续的空间组合而成,随时可以增加一段新的空间并链接起来 deque提供的迭代器也是 RandomAccess Iter

《STL源码剖析》学习笔记-第5章 关联式容器(二)

1.set和multiset set的特性: (1)所有元素都会根据元素的键值自动被排序. (2)set是集合,它的元素的键值就是实值,实值就是键值,不允许两个元素有相同的值. (3)不可以通过set的iterator来改变元素的值,因为set的元素值就是键值,改变键值会违反元素排列的规则. (4)在客户端对set进行插入或删除操作后,之前的迭代器依然有效.当然,被删除的元素的迭代器是个例外. (5)它的底层机制是RB-tree.几乎所有的操作都只是转调用RB-tree的操作行为而已. mult