4.2 vector

4.2.3 vector 的迭代器

vector维护的是连续线性空间,而且普通指针支持vector随机存取特性,所以vector提供的是Random Access Iterators

  1. template<class T, class Alloc = alloc>
  2. class vector{
  3. typedef T value_type;
  4. typedef value_type* iterator; //vector的迭代器是普通指针
  5. };

4.2.4 vector 的数据结构

vector采用的数据结构是线性连续空间,用两个迭代器startfinish分别指向配置得来的连续空间中目前已被使用的范围,并用end_of_storage指向整块连续空间的尾端

  1. template<class T, class Alloc = alloc>
  2. class vector{
  3. ...
  4. protected:
  5. iterator start;
  6. iterator finish;
  7. iterator end_of_storage;
  8. };

为了降低空间配置时的速度成本,vector实际配置的大小可能比客户端需求量更大一些,以备将来可能的扩充。这就是capacity的概念。

4.2.5 vector 的构造和内存管理

vector默认使用alloc作为空间配置器,并据此另外定义了一个data_allocator,为了更方便以元素大小为配置单位

  1. template<class T, class Alloc = alloc>
  2. class vector{
  3. protected:
  4. typedef simple_alloc<value_type, Alloc> data_allocator;
  5. ...
  6. };

于是data_allocator::allocate(n)表示配置 n 个元素的空间。

vector提供许多构造函数,其中一个允许指定空间大小及初值

  1. vector(size_type n, const T& value){fill_initialize(n, value);}
  2. //填充并予以初始化
  3. void fill_initialize(size_type n, const T& value){
  4. start - allocate_and_fill(n, value);
  5. finish = start + n;
  6. end_of_storage = finish;
  7. }
  8. //配置而后填充
  9. iterator allocate_and_fill(size_type n, const T& x){
  10. iterator result = data_allocator::allocate(n); //配置n个元素
  11. uninitialized_fill_n(result, n, x);
  12. return result;
  13. }

uninitialized_fill_n()会根据第一参数的型别特性,决定使用算法fill_n()或反复调用construct()

当使用push_back()将新元素插入vector尾部时,该函数首先检查是否还有备用空间:

  • 如果尚还有备用空间就直接在备用空间上构造元素,并调整迭代器finish,使vector变大;
  • 如果没有备用空间了,就扩充空间(此过程包含:重新配置移动数据释放原空间): ```cpp void push_back(const T& x){ if(finish != end_of_storage){
    1. construct(finish, x);
    2. ++finish;
    }else
    1. insert_aux(end(), x);
    }

template void vector::insert_aux(iterator position, const T& x){ if(finish != end_of_storage){ //尚有备用空间,就在备用空间起始处构造一个元素,并以 vector 最后元素值为其初值 construct(finish, (finish - 1)); //调整水位 ++finish; T x_copy = x; copy_backward(position, finish - 2, finish - 1); position = x_copy; }else{ //已无备用空间 const size_type old_size = size(); const size_type len = old_size != 0 ? 2 * old_size : 1; //以上配置原则:如果原大小为 0, 则配置 1个大小; //如果原大小不为 0,则配置原大小的两倍 //前半段用来放置原数据,后半段准备用来放置新数据

  1. iterator new_start = data_allocator::allocate(len); //实际配置
  2. iterator new_finish = new_start;
  3. try{
  4. //将原 vector 的内容拷贝到新 vector
  5. new_finish = uninitialized_copy(start, position, new_start());
  6. //为新元素设定初值x
  7. construct(new_finish, x);
  8. //调整水位
  9. ++new_finish;
  10. //将原vector的备用空间中的内容也拷贝过来(不知缘何)
  11. new_finish = uninitialized_copy(position, finish, new_finish);
  12. }
  13. catch(...){
  14. //commit or rollback 语义,要么全做完、要么一个不做
  15. destroy(new_start, new_finish);
  16. data_allocator::deallocator(new_start, len);
  17. throw;
  18. }
  19. //析构并释放原 vector
  20. destroy(begin(), end());
  21. deallocate();
  22. //调整迭代器,指向新 vector
  23. start = new_start;
  24. finish = new_finish;
  25. end_of_storage = new_start + len;
  26. }

}

  1. 动态内存的增加,并不是在原空间之后接续新空间,因为**无法保证原空间之后尚有可配置空间**,而是**以原大小两倍另外配置一块较大的空间**,然后将原内容拷贝过来,然后才开始在原内容之后构造新元素,并释放原空间。<br />**因此,对**`**vector**`**的任何操作一旦引起空间的重新配置,指向原**`**vector**`**的所有迭代器就都失效了**。
  2. **vector 的一些成员函数**<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/26389447/1658475941220-ea37ddb0-99af-472a-a40f-3623987618e0.png#clientId=ue764dc87-8788-4&crop=0&crop=0.1215&crop=1&crop=0.8611&from=paste&height=1093&id=u996b2579&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1640&originWidth=2360&originalType=binary&ratio=1&rotation=0&showTitle=false&size=208382&status=done&style=none&taskId=u974f4f36-02b3-4f91-9382-5f688b3e4cc&title=&width=1573)
  3. ---
  4. <a name="iRlYC"></a>
  5. # 4.3 list
  6. <a name="uTtFr"></a>
  7. ## 4.3.2 list 的节点
  8. `list`本身和`list`的节点是不同的结构,需要分开设计。`list`节点结构如下:
  9. ```cpp
  10. template<class T>
  11. struct __list_node{
  12. typedef void* void_pointer;
  13. void_pointer prev;
  14. void_pointer next;
  15. T data;
  16. };

4.3.3 list 的迭代器

list不再能像vector一样以普通指针作为迭代器,因此其节点不保证处于连续空间。list迭代器必须有能力指向 list 的节点,并有能力进行正确的递增、递减、取值、成员存取等操作。又由于list是一个双向链表,迭代器必须具备前移、后移的能力,所以list的迭代器是Bidirectional Iterator

插入与接续都不会造成原有的**list**迭代器失效。而**list**的元素删除操作只有指向被删除元素的那个迭代器失效,其他迭代器不受影响。

4.3.4 list 的数据结构

SGI list 不仅是一个双向链表,还是一个环状双向链表,所以只需要一个指针就可以完整表现整个链表。

  1. template<class T, class Alloc = alloc>
  2. class list{
  3. protected:
  4. typedef __list_node<T> list_node;
  5. public:
  6. typedef list_node* link_type;
  7. protected:
  8. link_type node; //只要一个指针,便可表示整个环状双向链表
  9. };

如果让指针node指向刻意置于尾端的一个空白节点,node便能符合 STL 对于前闭后开区间的要求,成为last迭代器
image.png
于是以下几个函数都可以轻易完成:

  1. iterator begin(){return (link_type)((*node).next);}
  2. iterator end(){return node;}
  3. bool empty()const{return node->next == node;}
  4. size_type size()const{
  5. size_type result = 0;
  6. distance(begin(), end(), result);
  7. return result;
  8. }
  9. reference front(){return *begin();}
  10. reference back(){return *(--end());}

4.3.5 list 的构造和内存管理

看书,不记笔记了。。

4.4 deque

vector是单向开口的连续线性空间,deque是一种双向开口的连续线性空间。
image.png
dequevector的差异:

  • deque允许在常数时间内对头端进行元素的插入或移除操作
  • deque没有容量观念,因为是动态地以分段连续空间组合而成,随时可以增加一段新的空间并链接起来。不像vector需要重新配置一块更大的空间,然后复制元素再释放旧空间。所以deque也没有必要提供空间保留功能(reserve)。

deque也提供Random Access Iterator,但是它的迭代器并不是普通指针,其复杂度与vector不可媲美。因此应尽可能使用vector而不是deque

4.4.2 deque 的中控器

deque是连续空间,是由一段一段的定量连续空间构成一旦需要在**deque**的前端或尾端增加新空间,便配置一段定量的连续空间,串接在**deque**的头端或尾端deque只是在这些分段的定量连续空间上维护了其整体连续的假象,并提供了随机存取的接口,避免了重新分配、赋值、释放的低性能,代价是复杂的迭代器架构

deque采用一块**map**作为主控。此处的**map**是指一小块连续空间,其中的每个元素都是一个指针,指向另一段比较大的连续线性空间——缓冲区。缓冲区才是deque存储空间主体。SGI STL 允许指定缓冲区大小,默认值 0 表示将使用 512 bytes 缓冲区。

  1. template<class T, class Alloc = alloc, size_t BufSiz = 0>
  2. class deque{
  3. public:
  4. typedef T value_type;
  5. typedef value_type* pointer;
  6. ...
  7. protected:
  8. //元素的指针的指针
  9. typedef pointer* map_pointer;
  10. protected:
  11. map_pointer map; //指向map,map是块连续空间,其中每个元素都指向一块缓冲区
  12. size_type map_size; //map内可容纳多少指针
  13. };

其实map是一个T**,即它是一个指针的指针,第二个指针指向型别为T的一块空间:
image.png

4.4.5 deque 的迭代器

deque是分段连续空间,维持其整体连续假象的任务落在了迭代器的operator++operator--两个云算子上。
deque的迭代器需要:

  • 能够指出缓冲区在哪里
  • 能够判断自己是否已经出于其所在缓冲区的边缘,如果是,一旦前进或后退就必须跳跃至下一个或上一个缓冲区。为了正确跳跃,deque必须随时掌握中控器map。 ```cpp template struct deque_iterator{ //没有继承std::iterator typedef deque_iterator iterator; typedef __deque_iterator const_iterator; static size_t buffer_size(){

    1. return __deque_buf_size(BufSiz, sizeof(T));

    }

    //由于未继承 std::iterator,所以必须自行撰写五个必要的迭代器相应型别 typedef random_access_iterator_tag iterator_category; typedef T value_type; typedef Ptr pointer; typedef Ref reference; typedef size_t size_type; typedef ptrdiff_t difference_type; typedef T** map_pointer;

    typedef __deque_iterator self;

    //保持与容器的联结 T cur; //此迭代器所指是缓冲区中现行元素 T first; //此迭代器所指是缓冲区的头 T* last; //此迭代器所指是缓洪区的尾(含备用空间) map_pointer node; //指向管控中心的某个节点 … };

//buf_size()用于决定缓冲区大小,调用全局函数deque_buf_size() inline size_t deque_buf_size(size_t n, size_t sz){ return n != 0 ? n : (sz < 512 ? size_t(512 / sz) : size_t(1)); } //如果 n 不为0,传回n,表示 buffer_size 由用户自定义 //如果 n 为0,表示 buffer_size 使用默认值,那么 //如果 sz(元素大小,sizeof(value_type))小于512,传回512/sz //如果 sz不小于 512,传回1

  1. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/26389447/1658562178371-21125fc1-ebf4-4db6-b062-a08f79725288.png#clientId=uea2aa39c-7c47-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=402&id=u754b5efc&margin=%5Bobject%20Object%5D&name=image.png&originHeight=502&originWidth=803&originalType=binary&ratio=1&rotation=0&showTitle=false&size=62490&status=done&style=none&taskId=u03ba0bd1-9a64-40d1-8ee7-a12672e1749&title=&width=642.4)<br />例如有一个`deque<int>`,其`start`和`end`迭代器布局如下:<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/26389447/1658562725252-16ce4fc2-0a56-4bcc-9add-a62141c5a933.png#clientId=uea2aa39c-7c47-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=490&id=u7399ce9c&margin=%5Bobject%20Object%5D&name=image.png&originHeight=613&originWidth=909&originalType=binary&ratio=1&rotation=0&showTitle=false&size=100964&status=done&style=none&taskId=u6690e948-bf64-4fe7-83a5-729315302d8&title=&width=727.2)
  2. ---
  3. <a name="N8ja3"></a>
  4. ## 4.4.4 deque 的数据结构
  5. `deque`除了维护一个指向`map`的指针外,也维护`start``finish`两个迭代器,分别指向第一缓冲区的第一个元素和最后缓冲区的最后一个元素(的下一个位置),此外,也必须**记住目前**`**map**`**的大小**,因为一旦`map`所提供的节点不足,就必须配置更大的一块`map`
  6. <a name="Kb4sO"></a>
  7. ##
  8. <a name="TDXAD"></a>
  9. ## 4.4.5 deque 的构造与内存管理
  10. `deque`的**缓冲区扩充操作非常复杂**。以下逐步介绍:<br />`deque<int, alloc, 32> ideq(20, 9);`声明了一个`deque`,其缓冲区大小为 32bytes,并令其保留 20 个元素空间,每个元素初值为 9。为了指定`deque`的第三个模板参数(**缓冲区大小**),必须将前两个参数都指明出来,因此必须明确指定`alloc`为空间配置器。
  11. `deque`自定了两个专属的空间配置器:
  12. ```cpp
  13. protected:
  14. //专属空间配置器,每次配置一个元素大小
  15. typedef simple_alloc<value_type, Alloc> data_allocator;
  16. //专属空间配置器,每次配置一个指针大小
  17. typedef simple_alloc<pointer, Alloc> map_allocator;
  18. //并提供一个构造函数
  19. deque(int n, const value_type& value)
  20. :start(), finish(), map(0), map_size(0){
  21. fill_initialize(n, value);
  22. }
  23. //fill_initialize()负责产生并安排deque的结构,并设定好初值
  24. template<class T, class Alloc, size_t BufSize>
  25. void deque<T, Alloc, BufSize>::fill_initialize(size_type n, const value_type& value){
  26. create_map_and_nodes(n);
  27. map_pointerr cur;
  28. __STL_TRY{
  29. //为每个节点的缓冲区设定初值
  30. for(cur = start.node; cur < finish.node; ++cur)
  31. uninitialized_fill(*cur, *cur + buffer_size(), value);
  32. //最后一个节点设定稍有不同(因为尾端可能有备用空间,不用设定初值)
  33. uinitialized_fill(finish.first, finish.cur, value);
  34. }
  35. catch(...){
  36. ...
  37. }
  38. }

其中create_map_and_nodes()负责产生并安排好deque的结构:

  1. template<class T, class Alloc, size_t BufSize>
  2. void deque<T, alloc, BufSize>::create_map_and_nodes(size_type num_elements){
  3. //需要节点数 = (元素个数/每个缓冲区可容纳的元素个数) + 1
  4. //如果刚好整除,会多配一个节点
  5. size_type num_nodes = num_elements / buffer_size() + 1;
  6. //一个map要管理几个节点,最少8个,最多是“所需节点数 + 2”
  7. //前后各预留一个,扩充时可用
  8. map_size = max(initial_map_size(), num_nodes + 2);
  9. map = map_allocator::allocate(map_size);
  10. //以上配置出一个“具有map_size个节点”的map
  11. //以下令nstart和nfinish指向map所拥有的全部节点的最中央区段
  12. //保持在最中央,可使头尾两端的扩充能量一样大。每个节点可对应一个缓冲区
  13. map_pointer nstart = map + (map_size - num_nodes) / 2;
  14. map_pointer nfinish = nstart + num_nodes - 1;
  15. map_pointer cur;
  16. __STL_TRY{
  17. //为map内的每个现用节点配置缓冲区,所有缓冲区加起来就是deque的可用空间
  18. for(cur = nstart; cur <= nfinish; ++cur)
  19. *cur = allocate_node();
  20. }
  21. catch(...){}
  22. //为deque内的两个迭代器start和end设定正确内容
  23. start.set_node(nstart);
  24. finish.set_node(nfinish);
  25. start.cur - start.first;
  26. finish.cur = finish.first + num_elements % buffer_size();
  27. }

当尾端插入元素时,缓冲区的大小仍然充足,那么就不会引起缓冲区的再配置,此时的deque状态:
image.png
以下是push_back()函数内容:

  1. public:
  2. void push_back(const value_type& t){
  3. if(finish.cur != finish.last - 1){
  4. construct(finish.cur, t);
  5. ++finish.cur;
  6. }else
  7. push_back_aux(t);
  8. }

如果在缓冲区容量不足的情况下,再添加元素到尾端,那么push_back()会调用push_back_aux(),先配置一整块新的缓冲区,再设置新元素内容,然后更改迭代器finish的状态:

  1. //只有当finihs.cur == finish.last - 1时才会调用
  2. //即,只有当最后一个缓冲区只剩一个备用元素空间时才会被调用
  3. template<class T, class Alloc, size_t BufSize>
  4. void deque<T, Alloc, BufSize>::push_back_aux(const value_type& t){
  5. value_type t_type = t;
  6. reserve_map_at_back(); //若符合某种条件则必须重换一个map
  7. *(finish.node + 1) = allocate_node(); //配置一个新节点(缓冲区)
  8. __STL_TRY{
  9. construct(finish.cur, t_copy); //针对当前元素设值
  10. finish.set_node(finish.node + 1); //使finish指向新节点
  11. finish.cur = finish.first; //设定finish的内部属性
  12. }
  13. STL_UNWIND(deallocate_node(*(finish.node + 1)));
  14. }

image.png


deque前端插入新元素的情况与在后端插入类似。


有两个函数用于判断何时map需要重新调整:

  • reserve_map_at_back()
  • reserve_map_at_front()

实际操作则由reallocate_map()执行:

  1. void reserve_map_at_back(size_type nodes_to_add = 1){
  2. if(nodes_to_add + 1 > map_size - (finish.node - map))
  3. //如果map尾端的节点备用空间不足
  4. //符合以上条件必须重换一个map(配置过程与vector扩容过程类似)
  5. reallocate_map(nodes_to_add, false);
  6. }
  7. void reserve_map_at_front(size_type node_to_add = 1){
  8. //如果map前端的节点备用空间不足
  9. //符合以上条件则必须重换一个map
  10. reallocate_map(nodes_to_add, true);
  11. }
  12. template<class T, class Alloc, size_t BufSize>
  13. void deque<T, Alloc, BufSize>::reallocate_map(size_type nodes_to_add,
  14. bool add_at_front){
  15. size_type old_num_nodes = finish.node - start.node + 1;
  16. size_type new_num_nodes = old_num_nodes + nodes_to_add;
  17. map_pointer new_nstart;
  18. if(map_size > 2 * new_num_nodes){
  19. new_nstart = map + (map_size - new_num_nodes) / 2
  20. +(add_at_front ? nodes_to_add : 0);
  21. if(new_nstart < start.node)
  22. copy(start.node, finish.node + 1, new_nstart);
  23. else
  24. copy_backward(start.node, finish.node + 1, new_nstart + old_num_node);
  25. }
  26. else{
  27. size_type new_map_size = map_size + max(map_size, nodes_to_add) + 2;
  28. //配置一块空间,准备给新map使用
  29. map_pointer new_map = map_allocator::allocate(new_map_size);
  30. new_nstart = new_map + (new_map_size - new_num_nodes) / 2
  31. +(add_at_front ? node_to_add : 0);
  32. //把原map内容拷贝过来
  33. copy(start.node, finish.node + 1, new_nstart);
  34. //释放原map
  35. map_allocator::deallocate(map, map_size);
  36. //设定新map的起始地址和大小
  37. map = new_map;
  38. map_size = new_map_size;
  39. }
  40. //重新设定迭代器start和finish
  41. start.set_node(new_nstart);
  42. finish.set_node(new_nstart + old_num_nodes - 1);
  43. }

4.4.6 deque 的元素操作

不记笔记了 看书。。