C++11 STL 序列式容器 vector、list、string、deque...

本文详细介绍了C++11 STL中的序列式容器,包括vector、list、string和deque的底层结构、优缺点、迭代器和应用场景。vector基于动态数组,提供O(1)的随机访问,但插入和删除效率低。list是一个双向链表,支持任意位置的插入和删除,但不支持随机访问。string作为字符串容器,涉及深拷贝和浅拷贝的问题。deque则在首尾插入和删除上更高效,同时支持随机访问。此外,文章还讨论了vector和list的比较以及STL容器适配器。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

STL 是 C++ 标准模板库,可以理解为一个包含数据结构和算法的软件框架,一共有六大组件

  • 容器
  • 算法
  • 迭代器
  • 配接器
  • 仿函数
  • 空间配置器

这篇文章主要总结序列式容器,下一篇总结关联式容器

常用的序列式容器有 vector、list、deque、string

vector

是一个可变大小的序列式容器,其底层结构是动态的数组,在堆中分配连续的内存空间来存储元素,它和数组不同之处就是,数组一般是静态的,大小是固定的,而 vector 的容量大小会随着有效元素的增加而变大

vector 同时也具有数组的特性,例如:

  • O(1) 时间的快速访问
  • 插入和删除的时间复杂度为O(N)

vector 的扩容规则 分为两种情况

  • 如果后面内存够用的话,直接在原来的位置扩容,在 VS 编译器是以 2 倍的扩容方式

  • 如果后面内存不够用,则操作系统会在内存的另一块区域寻找更大的内存空间,然后进行拷贝元素,最后释放原来的空间,所以 vector 在扩容的时候,会引发迭代器失效的问题,而这种扩容效率也比较低效

vector 迭代器

迭代器(iterator)就相当于是一个工具,当然也可以把它理解为指针,但又不完全是指针,迭代器是指针的泛指,因为迭代器有很多种,比如Java和 C# 都有他们自己的迭代器;C++ 中的迭代器是一种检查容器内元素并遍历元素的数据类型

使用案例

	int myints[] = {16,2,77,29 };
	int n = sizeof(myints) / sizeof(myints[0]);
	std::vector<int> fifth(myints, myints +n);
	// 写法一:定义迭代器,名字是 it
	std::vector<int>::iterator it = fifth.begin();
	// 写法二:auto it = fifth.begin()
	for (; it != fifth.end(); ++it) {
		std::cout << *it<<' ';
	}
	std::cout << '\n';

当然一个容器的迭代器有可能会有很多种类,比如,正向,反向,const 类型的迭代器

// 无法修改的 const 迭代器
std::vector<int>::const_iterator it = fifth.cbegin();	

逆向迭代器

std::vector<int>::reverse_iterator it = fifth.rbegin();

迭代器的失效问题(重点)

什么是迭代器失效,就是对容器的一些操作影响了元素的位置

(1) 删除 pos 位置的数据会导致 pos 迭代器失效,以及 pos 位置后面的迭代器也将失效

例如 1 2 3 4 5 6 7 8 9 ,假如此时迭代器指向6,那我们把 6 元素删除后,vector 中 6 后面的元素会依次向前走, 此时迭代器指向的时候 7 元素了,但是如果还对迭代器进行 ++ 操作,就会出错,因为被操作的迭代器是指向 6 的,它已经被删除了,正确做法是让 erase 重新赋值到指向 7 的迭代器,因为每删除一个元素,erase 自动返回指向下一个位置的迭代器

迭代器失效,错误案列

	int a[] = { 1, 2, 3, 4 };
	vector<int> v(a, a + sizeof(a) / sizeof(int));
	// 实现删除v中的所有偶数
	// 下面的程序会崩溃掉,如果是偶数,erase导致it失效
	// 对失效的迭代器进行++it,会导致程序崩溃
	vector<int>::iterator it = v.begin();
	while (it != v.end()){
		if (*it % 2 == 0) {
			// 错误案列,没有返回下一个迭代器
			v.erase(it);
		}
		++it;
	}

正确做法

	//erase会返回删除位置的下一个位置
	vector<int>::iterator it = v.begin();
	while (it != v.end()){
		if (*it % 2 == 0) {
			// 正确案列,这里要返回当前元素的下一个迭代器
			it = v.erase(it);
		}
		else {
			++it;
		}
	}

(2) 在 pos 位置插入元素会导致 pos 迭代器失效,如果插入的元素导致容器增容,那么整个迭代器就会失效,因为原来的空间很有可能就释放了,而迭代器依然指向的是原来的空间,这时候再去访问,就会发生内存错误

(3) 当插入一个元素时,end 操作方位的迭代器失效,因为之前的 end 迭代器指向的位置,和插入后 end 位置不一样,需要更新

另外还有些注意事项,如下

	vector<int>::iterator pos = find(v.begin(), v.end(), 3);
	v.erase(pos);
	cout << *pos << endl; // 此处会导致非法访问

	// 在pos位置插入数据,导致pos迭代器失效。
	// insert会导致迭代器失效,是因为insert可
	// 能会导致增容,增容后pos还指向原来的空间,而原来的空间已经释放了。
	pos = find(v.begin(), v.end(), 3);
	v.insert(pos, 30);
	cout << *pos << endl; // 此处会导致非法访问

总结一下迭代器失效解决办法

当遍历时插入元素时,//想要指向下一个元素,要跳过当前和被添加的元素
iter = v.insert(iter,\*iter);
iter+=2;

遍历时删除元素,erase 函数返回的就是删除之后的元素的迭代器
iter=iter.erase(iter);


vector 基础使用
常见的四种构造函数

	// 无参数构造
	std::vector<int> first; 
	first.bush_back(100);first.bush_back(100);first.bush_back(100);first.bush_back(100);

	// 构造并初始化 4 个 100
	std::vector<int> second(4, 100); 

	// 使用迭代器进行构造
	std::vector<int> third(second.begin(), second.end()); 

	// 拷贝构造
	std::vector<int> fourth(third); 

尾插: push_back(),尾删:pop_back()
查找: find(), 插入:insert(), 删除:erase(), 交换:swap()

// 使用 find 查找 3 所在位置的 iterator
vector<int>::iterator pos = find(v.begin(), v.end(), 3);

// 在 pos 位置之前插入 30
v.insert(pos, 30);	std::vector<int>::iterator it = fifth.begin();

// 在 pos 位置删除元素
v.erase(pos);

vector 优点

时间效率 :由于底层是一块连续的内存空间,所以访问元素的时间复杂度是常数阶的

空间效率:相比于数组来说,更加节省空间,因为它可以动态的申请空间,而不像数组一样,在运行之前就必须指定大小,但在很多情况下我们都不知道需要的空间有多大,所以使用 vector 可以避免因为申请的空间过于大而造成浪费…

vector 缺点

和 list 相比,在进行插入删除操作的时候,效率低

而且 vector 只能在尾部进行插入和删除,不能任意位置进行插入删除


list 底层结构是一个带头结点的双向循环链表,所以在常数范围内可以任意位置进行插入和删除的,并且该容器可以前后双向迭代,因为一个节点有两个指针,分为前指针和后指针

下面总结了它的使用方法
构造函数

	std::list<int> l1; 
	l1.push_back(100);
	l1.push_back(100);
	l1.push_back(100);
	l1.push_back(100);

	// l2中放4个值为100的元素
	std::list<int> l2(4, 100); 

	// 用l2的[begin(), end())左闭右开的区间构造l3
	std::list<int> l3(l2.begin(), l2.end()); 

	// 用l3拷贝构造l4
	std::list<int> l4(l3); 

常用函数

	std::list<int> l5(array, array + n);
	// 返回第一个结点的值
	l5.front();
	// 返回最后一个节点的值
	l5.back();
	// 在第一个节点前面插入
	l5.push_front(0);
	// 在最后一个节点后面插入
	l5.push_back(100);
	// 删除第一个节点
	l5.pop_front();
	//删除最后一个节点
	l5.pop_back();
	// 在 pos 位置插入 val,默认插入一个val,在pos参数后面可以指定插入的个数
	l5.insert(pos,val);// pos 是迭代器,而不是一个 int 值
	// 在 pos 位置删除元素
	l5.erase(pos);
	l5.erase(first,last); //删除区间为 [first,last)的值
	// 增加有效元素,默认值为 0,指定值为 val
	// n 是最终的元素个数,所以n 必须要大于原来容器值的个数
	l5.resize(n,val);
	l5.swap(list&l4);//交换两个链表的内容
	l5.clear()//清空l5

这里稍微说明一下,push_back 和 push_front 的缺点

既然有缺点就会有对比,和 emplace_back , emplace_front 相比起来,push_back 和 push_front 稍微有点低效率.如果是内置类型,那么push_back 和 emplace 效率基本差不多.

如果是自定以类型,其低效率主要表现在,在调用 push_back 函数之前要先构造好对象,然后把对象传入到 push_back 参数中,传进行之后,push_back 还得调用一次拷贝构造函数,而对于 emplace_back 只需要调用一次构造函数即可,没有调用拷贝构造函数

Date d(2019, 12, 20);
l2.push_back(d); // push_back 是无法直接传入数值的
	
l2.emplace_back(2019,12,21);

list 迭代器

list 迭代器用法和 vector 迭代器几乎差不多

定义迭代器
list<int>::iterator it = L.begin();
list<int>::reverse_iterator it = L.rbegin();
list<int>::const_iterator it = L.cbegin();

迭代器失效的问题(重要)

list 迭代器只有在删除的时候才会失效,而且仅仅是当前被删除元素的迭代器,其它的迭代器不受影响。

	int array[] = { 16,2,77,29 };
	int n = sizeof(array) / sizeof(array[0]);
	std::list<int> l5(array, array + n);
	// C++11 另一种迭代器的定义
	auto it = l5.begin();
	while (it != l5.end()) {
		l5.erase(it);
		// 错误案列:it 迭代器已经失效,所以不能 ++ 了
		++it;
	}
	auto it = l5.begin();
	while (it != l5.end()) {
		// 正确案列:删除后会自动返回下一个 it
		it = l5.erase(it);
	}

list 与 forward_list 非常相似:最主要的不同在于 forward_list 是单向链表,只能朝前迭代,而 list 是双链表,可以前后迭代

list 的应用以及优缺点

与其他的序列式容器相比(array,vector,deque),list 通常在任意位置进行插入、移除元素的执行效率更好

与其他序列式容器 array vector ,相比 list 和 forward_list 最大的缺陷是不支持任意位置的随机访问,因为底层并不是连续的内存空间,元素之间是通过指针的指向来访问;所以只能从头部或者尾部迭代,在查找时需要更多的时间开销,

另外,list 还需要一些额外的空间,以保存每个节点的相关联信息(对于存储类型较小元素的大 list 来说这可能是一个重要的因素)

vector 和 list 比较

vectorlist
底层结构动态顺序表带头节点的双向链表
随机访问支持随机访问不支持随机访问
插入和删除任意位置插入和删除效率低效率高,不搬移元素
空间利用率底层为连续空间,利用率高底层节点动态开辟
迭代器原生态指针对原生指针封装了
迭代器失效插入和删除都可能导致失效只有删除的时候才会失效
使用场景高效存储,不关心插入和删除大量的插入和删除

string 序列式容器

在 C++ 中,string 也可以理解为一种字符串的数据类型

在 string 中会有一个深拷贝和浅拷贝的问题,是关于指针和内存的问题,我单独写了文章,因为放在一起的话字数会很多

参考文章https://blog.csdn.net/qq_43763344/article/details/90575615

deque 序列式容器

双端队列是动态大小的序列式容器,其两端可以伸缩,它的底层结构还是比较复杂的, 是 中央控制器和多个缓冲区,其复杂之处主要体现在 首尾可以快速增删,以及支持任何位置的随机访问

因为 deque 和 vector 功能特别相似,但 deque 在头部和尾部进行数据插入和删除操作更加高效,与vector不同的是,deque不能保证所有的元素存储在连续的空间中,在 deque 中通过指针加偏移量方式访问元素可能会导致非法的操作,因为其底层并不是连续的内存空间,它的元素可能分散在不同的存储块

由于在 deque 中保存信息,依然可以常数阶访问任何一个位置,这些额外的信息,也是 deque 相比于 vector 的优势,特别是在扩容的时候,不需要进行拷贝元素,但是 vector 有个库函数弥补了这个缺陷,这个库函数就是 reserve ,功能是提前预留好空间,这样 vector 在插入的时候,就不会进行拷贝元素了

deque 迭代器

deque 迭代器的工作是比较复杂的,需要在不同区块间跳转,所以它非一般指针,deque 看起来像是连续的内存空间,其实都是迭代器在背后实现的

deque 的内存区块不再被使用时,会自动被释放。deque的内存大小是可自动缩减的

参考文章:https://blog.csdn.net/xiaoxianerqq/article/details/

底层示意图

在这里插入图片描述

常用接口

构造函数

	deque<int>d1{100,100,100,100};
	deque<int>d2(4, 100);
	deque<int>d3(d2.begin(), d2.end());
	deque<int>d4(d2);

deque 基本操作函数以及迭代器操作和 list 特别相似

front() 返回 deque 首元素的引用
back() 返回 deque 尾元素的引用
push_back(val) 在尾部插入元素
push_front(val) 在头部插入元素
push_pop() 在尾部删除元素
pop_front()在头部删除元素

//在deque的position位置插入值为val的元素
iterator insert (iterator position, const value_type& val)

//删除deque中position位置的元素,并返回该位置的下一个位置
iterator erase (iterator position)

void swap (deque& x) //交换
void clear() 清除元素

// 利用标准库中的算法对deque中的元素进行升序排序
sort(d.begin(), d.end());

deque 应用场景

deque 最大的应用就是作为标准库中 stack 和 queue 的底层结构,因为存储元素或者查找删除元素的序列式容器 vector 和 list 比 deque 高效

stack 和 queue 属于适配器

STL 容器适配器

参考文章https://blog.csdn.net/qq_43763344/article/details/100053612

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序猿的温柔香

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值