vector


💡关联知识点:struct末尾char[1]动态扩容;array

阅读材料:【C/C++ Vector容量调整】理解C++ Vector:Reserve与Resize的区别与应用_c++ vector reserve-博客(写的很详细)

vector是动态数组,插入元素后如果内存不够,vector内部机制会自行扩充空间以容纳新的元素。

vector迭代器

vector支持随机存取,所以vector的迭代器类型是随机迭代器,所以使用原生指针(T *)就可以实现迭代器的所有功能。

如vector<int>中迭代器就是int *。

vector数据结构

vector采用线性连续空间,start和finish指向数组的头部和尾部(前开后闭),并以迭代器end_of_storage指向已分配空间的尾端。

vector大小是[start,finish),容量是[start,end_of_storage)。

STL源码剖析:序列型容器_迭代器失效

vector构造函数
vector():start(0),finish(0),end_of_storage(0){}
vector(size_type n, const T& value) {fill_initialize(n,value);}
vector(int n, const T& value){fill_initialize(n,value);}
vector(long n, const T& value){fill_initialize(n,value);}
void fill_initialize(size_type n, const T& value) {
     start = allocate_and_fill(n, value);
     finish = start + n;
     end_of_storage = finish;
}

所以vector初始化使用的是allocate_and_fill(n,value)。

iterator allocate_and_fill(size_type n, const T& x){
     iterator result = data_allocator::allocate(n);
     uninitialized_fill_n(result, n, x);
     return result;
 }

使用空间配置器分配内存,然后填充未初始化的内存。

在填充位初始化的内存的时候,使用类型萃取,如果vector中存的是POD类型(标量或者传统的C struct结构体),就使用STL 中的fill算法。对于非POD类型,遍历容器然后调用构造函数。uninitialized_fill_n见第二章。类型萃取后,对于POD类型,uninitialized_fill 可以STL中的fill函数来填充,非POD类型则需要逐一调用构造函数。

vector中insert
vector中迭代器失效

insert迭代器失效分为两类:

  • 扩容导致野指针
  • 迭代器指向的位置意义发生改变

阅读材料:【C++】vector的迭代器失效问题(什么是迭代器失效?那些操作会导致迭代器失效?如何避免迭代器失效?)_vector迭代器失效-博客

1. 插入元素

vector 插入元素时,可能会导致内部内存扩容,并释放原来的内存,只要是释放原来的内存就会。

重新分配会导致所有迭代器失效,包括对 vector 的所有元素的迭代器。

如果插入不导致扩容,会导致插入位置后面的迭代器指向位置的意义发生改变。

std::vector<int> vec = {1, 2, 3};
    cout<<vec.capacity();//3
    auto it = vec.begin(); //it指向原来内存的第一个整数
    vec.push_back(4);//插入后,vector内存扩容,it迭代器失效
    cout<<vec.capacity();//6
    std::cout << "First element: " << *it << std::endl; //迭代器失效(未知数)
    std::cout << "Last element: " << vec.back() << std::endl;

2. 删除元素

删除元素同样会影响迭代器:

  • erase的失效都是迭代器指向的位置意义发生改变,或者不在有效访问数据的有效范围内
  • erase一般不会使用缩容的方案,那么也就导致erase的失效一般不存在野指针失效

3. 调整大小和容量

  • resize():增加 vector 的大小可能会触发重新分配,从而使所有现有迭代器失效。减少 vector 的大小不会导致迭代器失效,但会丢弃多余的元素。
  • reserve():reserve如果容量大于当前容量,会分配一块新的内存,指向原有内存的迭代器全部失效。

4. 其他操作

  • clear():清空 vector 会使所有迭代器失效,因为所有元素都被移除,迭代器不再指向有效的元素。
常用操作
insert

调用insert_aux。如果空间够,就把插入位置的元素整体后移(copy_backward),然后插入元素。

如果空间不够使用空间配置器分配原来2倍大小的空间(allocate)。

然后移动数据,(见第二章) 如果经过类型萃取后是POD类型,就调用STL的copy算法,否则遍历调用构造函数。释放原空间,更新迭代器。

allocate->uninitialized_copy -> destroy(begin(), end());deallocate();->start = new_start; finish = new_finish

template <class T, class Alloc>
void 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,则配置原来大小的两倍,前半段用来放置原数据,后半段准备用来放置新数据
           iterator new_start = data_allocator::allocate(len);  // 实际配置
           iterator new_finish = new_start;
// 将内存重新配置
         try{
//uninitialized_copy()的第一个参数指向输入端的起始位置
//第二个参数指向输入端的结束位置(前闭后开的区间)
//第三个参数指向输出端(欲初始化空间)的起始处
//将原vector的安插点以前的内容拷贝到新vector
        new_finish = uninitialized_copy(start, position, new_start);
//为新元素设定初值 x
        construct(new_finish, x);
                // 调整已使用迭代器的位置
         ++new_finish;
                // 将安插点以后的原内容也拷贝过来
         new_finish = uninitialized_copy(position, finish, new_finish);
       }
       catch(...){
                // 回滚操作
           destroy(new_start, new_finish);
           data_allocator::deallocate(new_start, len);
           throw;
       }
            // 析构并释放原vector
            destroy(begin(), end());
            deallocate();
            // 调整迭代器,指向新vector
            start = new_start;
            finish = new_finish;
            end_of_storage = new_start + len;
        }
}

push_back的底层实现

push_back(x) -> insert_aux(end(),x);

emplace_back的底层实现

push_back() 向容器尾部添加元素时,首先会创建这个元素,然后再将这个元素拷贝或者移动到容器中(如果是拷贝的话,事后会自行销毁先前创建的这个元素);而 emplace_back() 在实现时,则是直接在容器尾部创建这个元素,省去了拷贝或移动元素的过程。(看不懂!)

reserve

reserve操作是用于预分配Vector的容量。在Vector中存储大量的元素时,可以使用Reserve来预先分配足够的内存。这样可以避免在添加元素时频繁地重新分配内存,从而提高程序的性能。Reserve操作只是预分配内存,并不会改变Vector的大小。

如果预分配的内存容量大于当前容量,会分配一块新的内存,然后把当前元素拷贝到新的内存,然后释放原有的内存,这样会使原有的迭代器失效。如果预分配的内存容量小于当前容量,不会做任何事情。

reserve不会改变vector的size(size是由finsh和start距离决定的)

resize

resize函数实质是改变vector中的元素个数。

如果大小比原有大小 大,添加元素(可能会触发扩容机制,导致迭代器失效)然后进行初始化(finish和start的距离变大)。

如果大小比原有大小 小, 删除末尾元素。

vector的动态扩容

(代码见insert_aux)

当备用空间不足时,vector做了以下的工作:

(1)重新分配空间:若原来的空间大小为0,则扩充空间为1,否则扩充为原来的两倍。

(2)移动数据,释放原空间,更新迭代器,当调用默认构造函数构造vector时,其空间大小为0;但当我们push_back一个元素到vector尾端时,vector就进行空间扩展,大小为1,以后每当备用空间用完了,就将空间大小扩展为原来的两倍。

STL源码剖析:序列型容器_c++_02

动态增加大小,并不是在原空间之后接续新空间,(因为无法保证原空间之后上有可供分配的空间),而是以原大小的两倍来另外分配一块较大空间,因此,一旦空间重新分配,指向原vector的所有迭代器就会失。

STL容器的线程安全问题及解决办法:

https://www.zhihu.com/question/29987589

List


📌 关联知识点:linux内核链表,可以用链表表示的对象(?),空闲链表/glibc,....


list的数据结构

STL中list是使用环状双向链表实现的。

结点结构定义

前一个节点的指针,后一个节点的指针和数据域(使用模板,所以是通用链表)。

template <classT>
struct__list_node {
    typedefvoid* void_pointer;
    void_pointer next;
    void_pointer prev;
    T data;
};

链表最后使用一个指针指向环形链表的空白节点,空白节点指向头节点,这样形成了一个环。

STL源码剖析:序列型容器_迭代器失效_03

template<classT,classAlloc = alloc>{  
 protected :  
      typedef __list_node<T> list_node ;  
 public  :  
      typedef list_node* link_type ;  
 protected :  
      link_type node ; //只要一个指针,便可以表示整个环状双向链表  
      ...
  };

node是指向list节点的一个指针,可以使用这个指针表示整个环状双向链表。

如果指针node指向置于尾端的一个空白节点,node就能符合stl对于前闭后开区间的要求,这样以下函数便能轻易完成。

iterator begin(){return (link_type)((*node).next)); }
iterator end(){return node;}
boolempty()const{ return node->next == node;}
size_type size()const{
    size_type result = 0;
    distance(begin(), end(), result);//SGI里面的distance函数作用就是遍历链表return result;
}
reference front(){ return *begin(); }
reference back(){ return *(--end()); }
list的迭代器

list是一个双向循环链表,元素在内存中不需要连续存放。

vector在内存中连续存放,所以可以使用原生指针作为迭代器。

但是,list不连续存储,所以不能使用普通指针作为迭代器,因为它需要特殊的迭代器,list的迭代器封装了List节点指针(list_node *),它是一个双向迭代器,允许前移和后移操作。

list迭代器失效(被删除节点的迭代器)

list删除操作,只有指向被删除元素的迭代器会失效,其他迭代器仍然有效。插入不会使任何的迭代器失效。

template<classT,classRef,classPtr>
  struct_list_iterator{
      typedef _list_iterator<T,T&,T*> iterator;
      typedef _list_iterator<T,T&,T*> iterator;
      typedef bidirectional_iterator_tag iterator_category;
      typedef T value_type;
      typedef Ptr pointer;
      typedef Ref reference;
      typedef _list_node<T>* link_type;
      typedefsize_t size_type;
      typedefptrdiff_t difference_type;
      link_type node; // list的迭代器封装了List节点指针(list_node *).
      _list_iterator(link_type x):node(x){}
      _list_iterator(){}
      _list_iterator(const iterator& x):node(x.node){}
      booloperator==(const self& x) const {return node==x.node;}
      booloperator!=(const self& x) const {return node!=x.node;}
      reference operator*() const {return (*node).data;}
      reference operator->() const {return &(operator*());}      
      self& operator++(){
          node=(link_type)((*node).next);
          return *this;
      }
      self operator++(int){
          self tmp=*this;
          ++*this;
          return tmp;
      }
      self& operator--(){
          node=(link_type)((*node).prev);
          return *this;
      }
      self operator--(int){
          self tmp=*this;
          --*this;
          return tmp;
      }
  }
list节点的构造和释放

使用std::allocator构造节点和释放节点的内存,然后初始化节点的指针和值。

然后对节点进行操作。

list操作

insert:类似双向链表的插入。

terator insert(iterator position, const T& x){
    link_type tmp = create_node(x);   // 产生一个节点,调整双向指针,使tmp插入.
    tmp->next = position.node;
    tmp->prev = position.node->prev;
    (link_type(position.node->prev))->next = tmp;
    position.node->prev = tmp;
    return tmp;
}

erase:类似双向链表的删除。

iterator erase(iterator position){  
      link_type next_node=link_type(position.node->next);  
      link_type prev_node=link_type(position.node->prev_nodext);  
      prev_node->next=next_node;  
      next_node->prev=prev_node;  
      destroy_node(position.node);  
      returniterator(next_node);  
  }

push_front(),push_back(),pop_front(),pop_back() 在 insert 和 erase 的基础上实现。

deque


🔔关联知识点:二级指针,空基类优化


阅读材料:https://zhuanlan.zhihu.com/p/644990261

deque在常数时间在头尾两端分别做元素的插入和删除。虽然vector也可以在首端进行元素的插入和删除(利用insert和erase),但效率差(涉及到整个数组的移动),无法被接受。

deque是由一段一段连续内存空间组成。与vector 容器采用连续的线性空间不同,deque容器存储数据的空间是由一段一段等长的连续空间构成,各段空间之间不一定连续。

使用一个指针数组(中控器)map指向一段一段连续的线性空间(缓冲区,默认大小512字节)。

如果当前段空间用完了,就添加一个新的空间并将它链接在map的头部或尾部。

如果map的空间用完了,就配置一段空间给新map使用,并把原来的map元素拷贝过去。

STL源码剖析:序列型容器_初始化_04

在初始化 Map 内存时,根据所需的节点数量计算出映射表的大小,并为其分配内存,预留额外的空间,以便在未来插入元素时不需要重新分配映射表。

deque数据结构

STL源码剖析:序列型容器_迭代器失效_05

deque迭代器

由于 deque 容器底层将序列中的元素分别存储到了不同段的连续空间中,迭代器在遍历 deque 容器时,必须能够确认各个连续空间在数组中的位置,必须能够判断自己是否已经处于空间的边缘位置,前进后退需要考虑是否要跳跃到上一个或下一个连续空间中。

deque的迭代器内部包含 4 个指针;cur:指向当前正在遍历的元素。

first:指向当前连续空间的首地址;last:指向当前连续空间的末尾地址。

node:二级指针用于指向数组中存储的指向当前连续空间的指针。

STL源码剖析:序列型容器_c++_06

虽然deque容器的迭代器也支持随机访问,但是访问元素的速度要低于vector。

指针数组 (map): 是一个指针数组,维护指向缓冲区的指针。map 左右两边预留有空间,允许前后扩展。迭代器 (start, finish):分别指向 deque 中第一个元素和最后一个元素之后的位置,通过迭代器可以快速访问或修改 deque 的元素。

map_size 表示 map 中指针的数量,即数据块的总数。

map 的左右两边留有剩余空间是 deque 设计一个点。

这些剩余空间允许快速地在 deque 的前端或后端添加新的分段,而无需重新分配整个 map。

这样,即使是在 deque 的两端插入或删除元素,操作的时间复杂度也能保持在常数时间内。

erase 和 insert 函数实现了元素的删除和插入操作,它们采用了不同的策略来最小化元素移动的开销。具体策略取决于操作位置相对于 deque 中间位置的不同,以减少需要移动的元素数量。

当 deque 需要扩展其存储空间时,它会分配一个新的 map,这个新 map 有更多的指针,可以指向更多的缓冲区。然后,deque 会将现有的缓冲区指针从旧 map 复制到新 map 中,并适当地调整 start 和 finish 迭代器,以反映新的布局。这个过程使得 deque 能够在维持高效插入和删除操作的同时,提供对元素的快速随机访问。

当 deque 的一个分段被填满时,它会动态地添加一个新的分段来存储更多的元素。同样地,如果 deque 的大小减少,一些分段可能会被释放以节约空间。deque 动态调整其分段的能力,使得它在存储大量数据时非常灵活和高效。

EBO(Empty Base Optimization)空基类优化

_Deque_impl 结构体中,使用了 EBO 技术。空基类优化是指在派生类中,基类如果没有数据成员(即是空基类),编译器通常会将其优化掉,从而减少额外的内存开销。

struct_Deque_impl

: public _Tp_alloc_type, public _Deque_impl_data

{ /* ... */ };

_Deque_impl 中,_Tp_alloc_type(内存分配器)可能没有数据成员,因此其继承的实际内存开销为零,这样可以避免因为空基类而增加不必要的内存消耗。

(需要整理)

阅读材料:11. C++空基类优化_空子类-博客

stack

💡



关联知识点:适配器模式,函数栈帧。


stack是后进先出的数据结构,有压栈和出栈的操作。

STL中stack是一个容器适配器,它提供了接口,这些接口通过封装另一个底层容器(如 deque或 list)的功能实现的,这使用的是适配器模式。

stack默认使用deque底层容器,但用户可以通过模板参数选择其他容器,如 vector 或 list。

(这些容器支持 back(), push_back(), 和 pop_back() 操作)

这种封装提供了灵活性,因为底层容器可以很容易地被替换,而不影响 stack 类的公共接口。

queue


🔔



关联知识点:适配器模式,任务调度,消息队列,FIFO和FILO的区别


queue是先进先出的数据结构,有压栈和出栈的操作。

STL中queue是一个容器适配器,它提供了接口,这些接口通过封装另一个底层容器(如 deque或 list)的功能实现的,使用的是适配器模式。

默认情况下,queue 使用 deque 作为其底层容器,因为 deque 支持高效的在两端插入和删除操作,提供的功能比queue多,所以可以使用deque实现queue。然而通过模板参数,用户可以指定其他类型的容器(如 list),只要这个容器支持front(),back(),push_back()和 pop_front()操作。

heap


🔔



关联知识点:排序


heap堆是一个完全二叉树,可以用数组实现,STL中堆使用vector表示数组,这样插入的时候可以对数组进行动态扩容,操作见堆排序。

自定义比较函数(...)

priority_queue

优先队列是在正常队列的基础上加了优先级,保证每次的队首元素都是优先级最大的。每次从优先队列中取出的元素都是队列中优先级最大的一个,它的底层是通过来实现的。

forward_list

forward_list(c++ 11)是单向非循环链表,每个元素只包含指向下一个元素的指针。

forward_list的迭代器是前向迭代器,只能从前向后遍历。

list的迭代器是双向迭代器,所以slist的功能会受限,比如不能反向遍历链表,但是slist省去了一个指针的内存空间,更加轻量级。

使用forwad_list的场景:

(1)内存空间有限:当内存使用紧张时,可以使用forward_list。由于只存储单个指针,相比双向链表占用的空间更小。

(2)不需要双向遍历:如果应用场景不需要双向遍历元素,那么 std::forward_list 比 std::list 更加高效。

为什么会有双向遍历?

查找/删除/插入,如果链表是双向链表(如list),可以从当前节点向前或向后查找,以便更快地找到目标元素;

浏览器的历史记录通常需要双向操作,因为用户可以向前和向后浏览页面:

  • 前进和后退: 浏览器维护一个历史记录链表来支持用户在页面间导航。用户可以前进或后退,双向链表使得这些操作非常高效。