C++ 容器的基础知识总结

本文参考总结自(
作者:melonstreet
链接:https://www.cnblogs.com/QG-whz/p/5152963.html

0、容器概论

容器是指用以容纳物料并以壳体为主的基本装置 ,C++的容器就是可以用来储存对象的容器。

STL将定义的通用容器分三类:顺序容器,关联容器和容器适配器。

顺序容器:array(C++本身内置)、vector、list、forward_list、deque、stack、queue、priority-queue、string

关联容器:set、multiset、map、multimap

容器适配器:stack、queue、priority_queue

1、std::array

1.1底层数据结构

array的底层数据结构是固定数组,所以大小在定义后就不能再改变。由于是固定大小,所以不支持添加删除等改变容器大小的操作。

Defined in header <array>
template<
    class T,
    std::size_t N
> struct array;
1.2内存分配策略

编译器在哪里为array分配内存取决于array定义的位置和方式

  • 若定义为局部变量,则从栈上分配内存。
  • 若使用new操作符分配内存,则是在自由存储区上分配内存。
  • 若作为全局变量或局部静态变量,则是在全局/静态存储区上分配的内存。

例如,在函数定义的array局部对象在栈上分配内存,与此对比的是vector,它底层数据结构为动态数组,因此在自由存储区上分配内存:

#include <iostream>
#include <array>
using namespace std;
array<int, 5>b3;//全局变量array,在全局/静态储存区
int main()
{
	int stack_var;//栈上
	array<int, 5> b1;//临时变量,在栈上
	void *b2 = new array<int, 5>;//new申请的内存,在自由储存区
	cout << "stack area: 0x" << hex << &stack_var << endl;
	cout << "b1 address: 0x" << hex << &b1 << endl;
	cout << "b2 address: 0x" << hex << b2 << endl;
	cout << "b3 address: 0x" << hex << &b3 << endl;
	system("pause");
	return 0;
}
stack area: 0x00AFFB0C
b1 address: 0x00AFFAF0
b2 address: 0x0007EA18
b3 address: 0x0104C60C
请按任意键继续. . .
1.3array的优势
  • array比数组更安全。它提供了opeartor[]与at()成员函数,后者将进行数组越界检查。
  • 与其他容器相似,array也有自己的迭代器,因此array能够更好地与标准算法库结合起来。
  • 通过array::swap函数,可以实现线性时间内的两个数组内容的交换

另外,不像C-style数组,array容器类型的名称不会自动转换为指针。对于C++程序员来说,array要比C-style数组更好用。

2、forward_list
2.1底层数据结构

forward_list的底层就是单向链表,内部只维护了单向遍历的信息 ,如C++标准所讲,forward_list容器支持前向遍历元素序列,允许常数时间内在任意位置的插入或删除操作并进行自动的内存管理。 与list的主要区别是forward_list没有反方向的迭代器,不过也正因如此,forward_list的每个节点都节省了迭代器大小的开销,在元素众多的时候,将比list消耗少得多的内存。

2.2forward_list特殊之一:forward_list不提供返回其大小的操作

在所有已知的STL容器中,forward_list是唯一一个不提供size()的容器。

不提供的原因在于计算一个forward_list的长度需要线性的时间,库用户有时无法忍受这样的时间开销。其他容器提供的size()操作皆可以在常数时间内完成(在C++98时,list也是线性时间)。为了节省内存,forward_list甚至不跟踪序列的长度,要想获得某个forward_list对象的长度,用户需要通过distance()来计算。这带来了一些不便,但使得用户远离了size()带来的高消耗。每个容器类型都有三个与大小相关的操作:

max_size(),empty(),size(),而forward_list只提供了前两个。

#include <iostream>
#include <array>
#include <forward_list>
#include <string>
using namespace std;
template<typename Iter>
void  list_elements(Iter begin, Iter end)
{
	while (begin != end)
	{
		cout << *begin++<<" ";
	}
	cout << endl;
}


int main()
{
	forward_list<int> flist;
	string result = (flist.empty()) ? "true" : "false";//empty判断forward_list是否为空
	cout <<  result<< endl;

	for (int i = 0; i < 10; i++)
	{
		flist.push_front(i);//插入对象
	}
	list_elements(begin(flist), end(flist));//打印
	cout << flist.max_size() << endl;//可添加对象的最大值

	result = (flist.empty()) ? "true" : "false";//empty判断forward_list是否为空
	cout << result << endl;
    
   	cout << std::distance(flist.begin(), flist.end()) << endl;//forward_list的长度
	system("pause");
	return 0;
}
true
9 8 7 6 5 4 3 2 1 0
536870911
false
10
请按任意键继续. . .
2.3**forward_list特殊之二:****forward_list是唯一一个在给定位置之后插入新元素的容器

为此,forward_list提供了如下的插入接口:

接口描述
insert_after在给定位置之后插入新元素
emplace_after在给定位置之后构造新元素
erase_after删除给定位置之后的元素
splice_after将另一个forward_list的元素移动到本forward_list的指定位置之后

其他所有STL容器都是在指定位置之前插入元素(除了std::array,它不允许插入)。forward_list的这种特殊处理,还是出于效率的考虑。对于单链表我们应该很熟悉,为了在某个指定节点之前插入插入节点,我们必须改变插入位置的前一个节点的指向。换句话说,为了在指定节点之前插入新元素,我们必须要先获得插入位置前一个位置的节点,为了获取前面这个节点,需要线性的操作时间。

img

而如果我们是在指定位置之后插入新元素,则无需线性时间的查找操作,这样可实现常数时间的插入:

img

同样的,处于性能的考虑,forward_list没有提供在尾部进行操作的接口,包括push_back(),pop_back()和emplace_back(),这些操作对单列表来说都至少要花费O(n)来完成

2.4迭代器失效的问题

指向被删除元素的迭代器,在删除之后失效。

3List
3.1底层数据结构

ist同样是一个模板类,它底层数据结构为双向循环链表。因此,它支持任意位置常数时间的插入/删除操作,不支持快速随机访问。

3.2迭代器类型
接口(C++11新增)描述
emplace在指定位置之前插入新构造的元素
emplace_front在链表头插入新构造的元素
emplace_back在链表尾插入新构造的元素
3.3内存分配策略

双向链表,用多少申请多少

3.4迭代器失效

list的迭代器失效,只会出现在删除的时候,指向删除元素的那个迭代器在删除后失效。vector可能会因为插入导致空间重新配置,使得原来的迭代器全部失效

通常来说,list比forward_list灵活,接口多,但是forward_list更省内存。

4vector
4.1底层数据结构

vector的底层数据结构是动态数组 ,其大小可以不预先指定,并且自动扩展。在创建一个vector 后,它会自动在内存中分配一块连续的内存空间进行数据存储,初始的空间大小可以预先指定也可以由vector 默认指定,这个大小即capacity ()函数的返回值。当存储的数据超过分配的空间时vector 会重新分配一块内存块,但这样的分配是很耗时的,在重新分配空间时它会进行:申请更大内存——拷贝数据——销毁原内存中对象——释放原内存。

vector的实现技术关键就在于对其大小的控制以及重新配置时数据移动效率。

4.2迭代器类型

vector维护的是一个连续线性空间,与数组一样,所以无论其元素型别为何,普通指针都可以作为vector的迭代器而满足所有必要的条件。 因此,普通指针即可满足vector对迭代器的需求。所以,vector提供了Random Access Iterators。

4.3内存分配策略

标准库的实现者使用了这样的内存分配策略:以最小的代价连续存储元素。为了使vector容器实现快速的内存分配,其实际分配的容量要比当前所需的空间多一些(预留空间),vector容器预留了这些额外的存储区用于存放添加的新元素,于是不必为每个新元素进行一次内存分配。当继续向容器中加入元素导致备用空间被用光(超过了容量 capacity),此时再加入元素时vector的内存管理机制便会扩充容量至两倍,如果两倍容量仍不足,就扩张至足够大的容量。 按照《STL源码剖析》中提供的vector源码,vector的内存配置原则为:

  • 如果vector原大小为0,则配置1,也即一个元素的大小。
  • 如果原大小不为0,则配置原大小的两倍。

当然,vector的每种实现都可以自由地选择自己的内存分配策略,分配多少内存取决于其实现方式,不同的库采用不同的分配策略。

4.4迭代器失效问题

1、vector管理的是连续的内存空间,在容器中插入(或删除)元素时,插入(或删除)点后面的所有元素都需要向后(或向前)移动一个位置,指向发生移动的元素的迭代器都失效。

img

2、随着元素的插入,原来分配的连续内存空间已经不够且无法在原地拓展新的内存空间,整个容器会被copy到另外一块内存上,此时指向原来容器元素的所有迭代器通通失效。

img

3、删除元素后,指向被删除元素的迭代器失效,这是显而易见的。

5deque
5.1底层数据结构

deque则是一种双向开口的连续数据空间。所谓的双向开口,意思是可以在头尾两端分别做元素的插入和删除操作。

img

5.2内存分配策略

deque由一段一段的连续空间所链接而成,一旦需要在deque的前端或尾端增加新空间,便配置一段定量的连续空间,并将该空间串接在deque的头部或尾部。deque复杂的迭代器架构,构建出了所有分段连续空间”整体连续“的假象。

既然deque是由一段一段定长的连续空间所构成,就需要有结构来管理这些连续空间。deque采用一块map(非STL中的map)作为主控,map是一块小的连续空间,其中每个元素都是指针,指向一块较大的线性连续空间,称为缓冲区。而缓冲区才是存储deque元素的空间主体。示例图:

img

map本身也是一块固定大小的连续空间,当缓冲区数量增多,map容不下更多的指针时,deque会寻找一块新的空间来作为map。

5.3deque的迭代器

为了使得这些分段的连续空间看起来像是一个整体,deque的迭代器必须有这样的能力:它必须能够指出分段连续空间在哪里,判断自己所指的位置是否位于某一个缓冲区的边缘,如果位于边缘,则执行operator-- 或operator++时要能够自动跳到下一个缓冲区。因此,尽管deque的迭代器也是Ramdon Access Iterator 迭代器,但它的实现要比vector的复杂太多。SGI版本的STL deque实现思路可以看侯捷的《STL源码剖析》。

5.4迭代器失效
  • 在deque容器首部或者尾部插入元素不会使得任何迭代器失效。
  • 在其首部或尾部删除元素则只会使指向被删除元素的迭代器失效。
  • 在deque容器的任何其他位置的插入和删除操作将使指向该容器元素的所有迭代器失效。
6容器适配器

stack,也称为栈,是一种先进后出的数据结构。STL中的statck是一种容器适配器。所谓的容器适配器,是以某种容器作为底部容器,在底部容器之上修改接口,形成另一种风貌。stack默认以双端队列deque作为底部容器。stack没有提供迭代器,通过push/pop接口对栈顶元素进行操作。

queue,也称为队列,是一种先进先出的数据结构,它同样也是一种容器适配器。它的底部容器默认为deque。同样,queue也没有提供迭代器,通过push向队尾压入元素,pop从队首弹出元素。

priority-queue,优先队列,是一种拥有权值观念的队列,例如在以整数大小作为衡量的权值定义下,priority-queue总是弹出最大的数。priority-queue的底部数据结构默认是max-heap,大顶堆。

7总结
容器底层数据结构元素访问方式插入或删除元素效率迭代器失效情况
array固定大小的数组支持快速随机访问不能添加或删除元素通常不会发生迭代器失效,除非对象已经被销毁,则原来的迭代器全部失效
vector可动态增长的数组支持快速随机访问尾部可高效插入/删除元素若插入操作引起内存重新分配,则全部迭代器失效;否则插入点/删除点之后的迭代器失效;
list双向链表只支持元素的双向顺序访问在list的任何位置可高效插入/删除元素插入操作后指向容器的迭代器有效;删除操作指向其他位置的迭代器有效
deque双端队列支持快速随机访问首尾可高效插入/删除元素情况较多,见上面分析
forward_list单向链表只支持元素的单向顺序访问在链表的任何位置可高效插入/删除元素插入操作后指向容器的迭代器有效;删除操作指向其他位置的迭代器有效
string只存储字符元素的动态数组支持快速随机访问尾部可高效插入/删除元素若插入操作引起内存重新分配,则全部迭代器失效;否则插入点/删除点之后的迭代器失效;
stack默认deque先进后出,只能访问栈顶元素----没有迭代器
queue默认deque先进先出,只能访问队首元素----没有迭代器
priority-queue默认max-heap先进先出,只能访问队首元素----没有迭代器

注意:

  • “尾部可高效插入/删除元素”,意味着在除了尾部之外的其他位置插入/删除元素是较低效的。
  • “顺序访问”意味着要访问某一个元素,必须遍历其他元素。
    |
    | priority-queue | 默认max-heap | 先进先出,只能访问队首元素 | ---- | 没有迭代器 |

注意:

  • “尾部可高效插入/删除元素”,意味着在除了尾部之外的其他位置插入/删除元素是较低效的。
  • “顺序访问”意味着要访问某一个元素,必须遍历其他元素。
  • 迭代器失效意味着指针、引用在同样的情况下也会失效。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值