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)。
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,以后每当备用空间用完了,就将空间大小扩展为原来的两倍。
动态增加大小,并不是在原空间之后接续新空间,(因为无法保证原空间之后上有可供分配的空间),而是以原大小的两倍来另外分配一块较大空间,因此,一旦空间重新分配,指向原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;
};
链表最后使用一个指针指向环形链表的空白节点,空白节点指向头节点,这样形成了一个环。
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元素拷贝过去。
在初始化 Map 内存时,根据所需的节点数量计算出映射表的大小,并为其分配内存,预留额外的空间,以便在未来插入元素时不需要重新分配映射表。
deque数据结构
deque迭代器
由于 deque 容器底层将序列中的元素分别存储到了不同段的连续空间中,迭代器在遍历 deque 容器时,必须能够确认各个连续空间在数组中的位置,必须能够判断自己是否已经处于空间的边缘位置,前进后退需要考虑是否要跳跃到上一个或下一个连续空间中。
deque的迭代器内部包含 4 个指针;cur:指向当前正在遍历的元素。
first:指向当前连续空间的首地址;last:指向当前连续空间的末尾地址。
node:二级指针用于指向数组中存储的指向当前连续空间的指针。
虽然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
),可以从当前节点向前或向后查找,以便更快地找到目标元素;
浏览器的历史记录通常需要双向操作,因为用户可以向前和向后浏览页面:
- 前进和后退: 浏览器维护一个历史记录链表来支持用户在页面间导航。用户可以前进或后退,双向链表使得这些操作非常高效。