C++ STL—vector,map,set,list,deque等

news2025/1/18 6:11:34

STL是什么

STL是标准模板库,包括算法、容器和迭代器。

  • 算法:包括排序、复制等常用算法
  • 容器:数据存放形式,包括序列式容器和关联式容器,序列式容器就是list,vector,关联式容器就是set,map等
  • 迭代器是在不暴露容器内部结构的情况下对容器遍历

迭代器++it和it++哪个好

前者返回一个引用,后者返回一个对象;前者不会产生临时对象,后者产生临时对象,导致效率降低

// ++i实现代码为:
int& operator++()
{

  *this += 1;
  return *this;

} 
//i++实现代码为:                 
int operator++(int)                 
{
int temp = *this;                   

   ++*this;                       

   return temp;                  
} 


迭代器五种成员类型

  • value type:表示迭代器所指向的元素类型。
  • difference type:表示两个迭代器之间的距离,通常是一个带符号整数。
  • pointer:表示迭代器所指向的元素类型的指针类型。
  • reference:表示迭代器所指向的元素类型的引用类型。
  • iterator category:表示迭代器的类型,包括输入迭代器、输出迭代器、前向迭代器、双向迭代器和随机访问迭代器。

STL中的hashtable实现

STL中使用的是开链法解决hash冲突问题。
hashtable中的bucket所维护的list既不是list也不是slist,而是其自己定义的由hashtable_node数据结构组成的linked-list,而bucket聚合体本身使用vector(因为vector自己有动态扩容的能力)进行存储。hashtable迭代器只提供前进操作,不提供后退操作。

  • 在hashtable设计bucket的数量上,内置了28个质数[53,97,193…429496729],创建hashtable的时候根据存入的元素个数选择大于等于元素的个数的质数作为hashtable的容量。

如果插入的hashtable的元素个数超过了bucket容量,就要重建table,也就是找出下一个质数,创建新的buckets vector,重新计算元素在新hashtable的位置。

开链法具体实现

当插入一个元素的时候,先将元素的哈希值对哈希表大小取余得到一个索引值,然后将元素插入到该索引对应的链表中。如果该链表中已经存在一个与该元素关键字相同的元素则将新元素插入到链表头部,成为新的链表头;否则将新的元素插入到链表头部。

  • 当查找一个元素时,先根据元素关键字计算哈希值,然后在对应的链表中查找,找到元素则返回元素的地址,否则返回一个空指针。

  • 当删除一个元素时,先在对应的链表中查找该元素,如果找到则将该元素从链表中删除。如果删除后链表为空,则将该哈希槽置为空(即指向空指针)。

RAII是什么

资源获取即初始化,也就是在构造函数中申请分配资源,析构函数中释放资源。智能指针就是RAII最具有代表的实现,不用担心忘记delete造成的内存泄漏

STL的两级空间配置器

为什么需要二级空间配置器?

动态开辟内存的时候,需要在堆上申请。但是频繁申请的时候容易造成很多外部碎片,降低效率。于是设置了二级空间配置器:开辟内存小于等于128字节时,开辟小内存

二级空间配置器实现过程

  1. 维护16条将链表,从8字节到128字节,传入一个字节参数,然后系统选择相应位置的链表,如果链表不为空,则拔出来。
  2. 如果链表为空,查看内存池是否为空,如果不为空,如果剩余空间有20个节点大小(所需内存*20),则从内存池中拿出20个节点大小空间,将其中一个分配给用户使用,另外19个挂在free_list下面;如果不够20个,但是够1个,则拿出一个分配给用户,剩下的空间挂在free_list下面;如果一个节点大小都不满足,则把剩余空间挂在free_list中,然后申请内存
  3. 如果内存池为空,使用malloc()从heap上面申请内存【一次所申请的内存大小为2 * 所需节点内存大小(提升后)* 20 + 一段额外空间】,一半拿来使用,一般放在内存池
  4. malloc失败,二级空间配置器会从比所需节点空间大的free_list中搜索,拔一个节点来使用,如果还没有找到,那么要使用一级适配器。

释放时调用deallocate函数,如果释放空间大于128,则调用一级空间配置器,否则就直接将内存块挂上自由链表的合适位置。

二级空间配置器缺点

  • 造成内部碎片
  • 它的实现中所有成员全都是静态的,所以它申请的所有内存只有在进程结束的时候才会释放内存,还给操作系统。如果不断开辟小内存,最后整个堆上的空间都被挂在自由链表上,这个时候如果想开辟大块内存就会失败;若自由链表上挂很多内存块没有被使用当前进程又占着内存不释放,这个时候别的进程在堆上申请不到空间,也不可以使用当前进程的空闲内存,就会引发很多问题。

底层实现

容器内删除元素

顺序容器

erase迭代器不仅使所指向被删除的迭代器失效,而且使被删元素之后的所有迭代器失效(list除外),所以不能使用erase(it++)的方式,但是erase的返回值是下一个有效迭代器;

因为元素在内存中是连续的,删除一个元素会导致这个元素后面所有元素都向前移动一个位置,这样就会导致原本指向后面元素的迭代器指向错误位置。
对于list来说,元素是分散存储的,删除一个元素是不会影响其它元素的位置。

It = c.erase(it);

关联容器

erase迭代器只是被删除元素的迭代器失效,但是返回值是void,所以要采用erase(it++)的方式删除迭代器;
c.erase(it++)

vector

分配空间和底层实现

  1. 操作方式与array比较相似,但是vector是维护一块连续的线性空间,空间不足的时候,可以自动扩展空间。
  2. 在扩充空间的时候,需要:重新配置空间,移动数据,释放空间。在Windows是扩充为原来的1.5倍,Linux+GCC下是两倍
  3. 为什么扩容两倍或者成倍增长?实验得出采用成倍方式扩容,可以保证常数时间复杂度,而增加指定大小容量只能达到O(n)时间复杂度。

释放空间

所有内存空间是在vector析构的时候才被内存回收。如果需要空间动态缩小,可以考虑使用deque。如果想要清空内存,可以使用swap。

clear函数只是清空内容,不能改变容量大小;remove一般也不会改变容器大小。而pop_back()与erase()等成员函数会改变容器大小。如果要改变容量大小可以使用deque

swap

#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec1 = {1, 2, 3};
    std::cout << "vec1 size: " << vec1.size() << ", capacity: " << vec1.capacity() << std::endl;

    std::vector<int> vec2 = {4, 5, 6, 7};
    std::cout << "vec2 size: " << vec2.size() << ", capacity: " << vec2.capacity() << std::endl;

    vec1.swap(vec2);

    std::cout << "After swap:" << std::endl;
    std::cout << "vec1 size: " << vec1.size() << ", capacity: " << vec1.capacity() << std::endl;
    std::cout << "vec2 size: " << vec2.size() << ", capacity: " << vec2.capacity() << std::endl;

    return 0;
}

删除元素

动态增长

size和capacity相同的时候,说明vector目前的空间已经被用完了,如果再添加新的元素则会引起vec空间的动态增长。
由于动态增长会引起重新分配空间、拷贝原空间、这些过程会降低效率。所以可以使用reserve(n)预先分配一块较大的指定大小的内存空间,只有当n>capacity的时候,调用reserve(n)才会改变vector容量;当指定大小的内存空间没有使用完的时候,不会重新分配空间。

void resize(size_type __new_size, const _Tp& __x) {
      if (__new_size < size()) 
            erase(begin() + __new_size, end());
      else
            insert(end(), __new_size - size(), __x);
      }


resize和erserver对比
#include <iostream>
#include <vector>

int main() {
  std::vector<int> my_vector;

  // 在插入元素之前,预留 10 个 int 类型的空间
  my_vector.reserve(10);

  for (int i = 0; i < 10; ++i) {
    my_vector.push_back(i);
  }

  std::cout << "Vector size: " << my_vector.size() << '\n';//10
  std::cout << "Vector capacity: " << my_vector.capacity() << '\n';//10

  return 0;
}
  • 空间大小不足的时候,新分配空间大小为原来空间大小的2倍
  • 使用reserve预先分配一块内存之后,空间未满情况下不会引起重新分配
  • reserve分配的空间比原空间小的时候,不会重新分配
  • resize只改变容器数目,不改变容器大小
  • 用reserve(size_type)只是扩大capacity值,这些内存空间可能还是“野”的,如果此时使用“[ ]”来访问,则可能会越界。而resize(size_type new_size)会真正使容器具有new_size个对象。

emplace_back

作用是在容器末尾插入一个新的元素,这个元素的值由给定的参数直接在容器内构造而成。
可以直接在容器中构造元素,不需要先创建一个对象然后将其复制到或者移动到容器中,避免移动和复制开销

需要注意的是,emplace_back() 在构造元素时可能会抛出异常,这会导致容器的状态变为未定义,因此在使用 emplace_back() 时需要格外小心,确保提供的参数能够成功构造出一个新的元素。

map和set

底层是红黑树实现的,插入删除操作都是在O(logn)时间之内完成,并且是自动排序的,按照中序遍历会是有序遍历。map是key+value,set是value。

map和set的区别

set只提供一种数据类型的接口,但是会将这一个元素分配到key和value上,而且它的compare_function用的是 identity()函数,这个函数是输入什么输出什么,这样就实现了set机制,set的key和value其实是一样的了。其实他保存的是两份元素,而不是只保存一份元素

map则提供两种数据类型的接口,分别放在key和value的位置上,他的比较function采用的是红黑树的comparefunction(),保存的确实是两份元素。

map实现原理

一旦map的key确定了,那么是无法修改的,但是可以修改这个key对应的value,因此map的迭代器既不是constant iterator,也不是mutable iterator。
map底层也是红黑树,构造的时候默认采用递增排序key,也是用alloc配置器配置空间大小。需要注意的是在插入元素时,调用的是红黑树中的insert_unique()方法,而非insert_euqal()(multimap使用)

下标操作

需要注意的是subscript(下标)操作既可以作为左值运用(修改内容)也可以作为右值运用(获取实值)。
首先根据键值和实值做出一个元素,这个元素的实值未知,因此产生一个与实值型别相同的临时对象替代,再将这个对象插入到map中,并返回一个pair,pair第一个元素是迭代器,指向当前插入的新元素,如果插入成功返回true,此时对应左值运用,根据键值插入实值。插入失败(重复插入)返回false,此时返回的是已经存在的元素,则可以取到它的实值。由于这个实值是引用传递,所以作为左值或者右值都可以

map是怎么扩容的

首先申请一块更大的内存空间,然后将原有的元素逐个复制到新的内存空间中。复制的过程中,会按照红黑树的中序遍历顺序,将每个元素的键值对存储到新的位置,释放旧的空间。

set 底层与迭代器

  • 底层是红黑树,几乎所有操作都是转调用红黑树的操作行为。
  • set不允许迭代器修改元素的值,迭代器是一种constance iterators。

unordered_map和unordered_set

都是无序的,既不会按照大小来排序,也不会按照插入顺序来排序。unordered_map底层是hash_table.

hash_table表格内的元素称为桶,选择vector作为存放桶元素的基础容器,因为vector容器本身具有动态扩容的能力。
为什么sunordered_set不是按照插入顺序排序的?因为是按照散列值哈希值组织到桶中,以便于快速访问某个元素

multimap、unordered_multimap 和multiset 、unordered_multiset

mutimap:底层为红黑树,可以重复
unordered_mutimap:底层为哈希表,无序,可以重复
mutiset:底层为红黑树,有序,可重复
unordered_mutiset:底层为哈希表,无序,可以重复

unordered_map和map应用场景

map适用于有序数据的应用场景,unordered_map适用于高效查询的应用场景

list和slist

list

list不仅是一个双向链表,而且还是一个环状双向链表,所以只需要一个指针。
list是双向迭代器:Bidirectional iterators。

空间管理

默认采用alloc作为空间配置器,为了方便的以节点大小为配置单位,还定义了一个list_node_allocator函数可以一次性配置多个节点空间。

slist

slist是单向链表,迭代器是forward iterator,所耗用的空间更小,操作更快。forward_list在C++11中出现,与slist的区别是没有size()方法。C++标准委员会没有采用slist的名称。

deque

deque的数据结构如下

class deque
{
    ...
protected:
    typedef pointer* map_pointer;//指向map指针的指针
    map_pointer map;//指向map
    size_type map_size;//map的大小
public:
    ...
    iterator begin();
    iterator end();
    ...
}

deque内部有一个指针指向map,map是一小块连续的空间,其中每一个元素称为一个节点node,每一个node都是一个指针,指向另一段较大的连续空间,称为缓冲区,==这就是deque中实际存放数据的区域,默认大小是512bytes。

在这里插入图片描述
deque迭代器的数据结构如下

struct __deque_iterator
{
    ...
    T* cur;//迭代器所指缓冲区当前的元素
    T* first;//迭代器所指缓冲区第一个元素
    T* last;//迭代器所指缓冲区最后一个元素
    map_pointer node;//指向map中的node
    ...
}

在这里插入图片描述

零拷贝

通常情况下,我们需要将数据从一个应用程序的内存空间复制到操作系统的内核空间,然后再将数据从内核空间复制到另一个应用程序的内存空间。这种复制过程会产生很大的开销,因为每次复制都需要耗费时间和内存资源。

零拷贝技术可以采用以下几种方式

  1. 使用内存映射文件(mmap)技术,将文件映射到内存中,避免了文件复制的过程。
  2. 使用数据包直接内存访问(DMA)技术,将数据从网络设备的缓冲区直接传输到内存中,避免了数据复制的过程。
  3. 使用共享内存(shared memory)技术,将数据共享到多个应用程序中,避免了数据复制的过程。

在C++中,STL也支持零拷贝

  • std::vector 的 reserve() 成员函数可以预留一定的容量,从而避免了在插入元素时不必要的扩容和数据复制。
  • std::string 的 substr() 成员函数可以返回一个指向原字符串中一段子串的指针,避免了字符串的复制。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/523809.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

考研复试刷题第八天:日期累加 【日期问题】

本来以为和上次那个简单题一样的&#xff0c;没啥难度&#xff0c;就是循环就完事了&#xff0c;结果超时了 超时代码: #include <iostream> using namespace std;//平年各个月份都多少天&#xff1f; int mouths [13] {0,31,28,31,30,31,30,31,31,30,31,30,31 };//判…

Spring事务深度学习

jdbcTemp Spring 框架对 JDBC 进行封装&#xff0c;使用 JdbcTemplate 方便实现对数据库操作。 JdbcTemp的使用 对应依赖 <!-- MySQL驱动 --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><ve…

已知相机内外参通过COLMAP进行稀疏/稠密模型重建操作过程

在https://colmap.github.io/faq.html#reconstruct-sparse-dense-model-from-known-camera-poses 中介绍了已知相机内外参如何通过COLMAP进行稀疏和稠密模型重建的过程&#xff0c;这里按照说明操作一遍&#xff1a; 在instant-ngp中&#xff0c;执行scripts/colmap2nerf.py时…

request页面代码逻辑

一. 封装请求基地址 在src目录下面建一个api文件夹 然后在文件夹里面新建一个专门放用户请求的use.js 用axios发送请求 在use.js文件夹里导入request 在src目录新建发送请求的页面并导入封装好的请求 然后把这个请求封装成一个函数&#xff0c;这个函数里需要传入两个参数。 …

Xavier或TX2配置ipv4地址

输入ifconfig查看本地ipv4地址&#xff0c;发现并没有设置&#xff0c;无法通过以太网与其他主机通信。下面来配置系统的以太网地址。 1、编辑文件/etc/network/interfaces: sudo gedit /etc/network/interfaces2、用下面的内容来替换有关eth0的行&#xff0c;并且将ip地址等信…

Java中抽象类和接口的区别,一文弄懂,图文并茂

目录 前言 1. 抽象类 1.1 定义 1.2 示例 1.3 使用 1.3.1代码-抽象类 1.3.2代码-抽象类继承类使用 1.3.3输出结果为&#xff1a; 1.4UML类图展示类间的关系 2. 接口 2.1 定义 2.2 示例 2.2.1代码-接口 2.3 使用 2.3.1代码-接口实现 2.3.2代码-接口实现类使用 2…

【Linux】驱动内核调试,没有几板斧,怎么能行?

目录 前言&#xff1a; 一、基础打印工具 &#xff08;1&#xff09;printk---最常用 ①Log Buffer: ②Console&#xff1a; ③RAM Console&#xff1a; &#xff08;2&#xff09;动态打印 ①动态打印与printk之间的区别联系 ②动态打印常用的例子 ③动态打印转为pri…

C语言实战 - 贪吃蛇(图形界面)

由于本人精力有限&#xff0c;暂时先把素材和代码放上&#xff0c;等以后有空再补教程。 目录 效果预览 准备工作 EasyX图形库 音频素材 代码编写 Transfer.h文件 game.cpp文件 main.c文件 效果预览 先来看一下最终成品效果 贪吃蛇图形界面 准备工作 EasyX图形库 这…

[230513] TPO72 | 2022年托福阅读真题第1/36篇 | 10:45

Invading Algae 目录 Invading Algae 全文 题目 Paragraph 1 P1 段落大意 问题1 Paragraph 2 P2 段落大意 问题2 *问题3* Paragraph 3 P3 段落大意 问题4 Paragraph 4 P4 段落大意 Paragraph 5 P5 段落大意 *问题5* *问题6* 问题7 问题8 问题9…

【计算机组成原理】第二章 运算方法和运算器

系列文章目录 第一章 计算系统概论 第二章 运算方法和运算器 第三章 多层次的存储器 第四章 指令系统 第五章 中央处理器 第六章 总线系统 第七章 外围设备 第八章 输入输出系统 文章目录 系列文章目录第一章 计算系统概论 **第二章 运算方法和运算器** 第三章 多层次的存储器…

C++移动构造函数

一、背景 拷贝构造函数又分为浅拷贝和深拷贝&#xff1a; 浅拷贝&#xff1a;当类中有指针时&#xff0c;直接复制&#xff0c;会使多个指针指向同一块内存&#xff0c;导致重复析构 深拷贝&#xff1a;每次都是重新赋值一份&#xff0c;这种方法内存消耗较大 因此C就提供…

一觉醒来Chat gpt就被淘汰了

目录 什么是Auto GPT&#xff1f; 与其他语言生成模型相比&#xff0c;Auto GPT具有以下优点 Auto GPT的能力 Auto GPT的能力非常强大&#xff0c;它可以应用于各种文本生成场景&#xff0c;包括但不限于以下几个方面 Auto GPT的历史 马斯克说&#xff1a;“ChatGPT 好得吓…

【C++从0到王者】第三站:类和对象(中)赋值运算符重载

文章目录 一、运算符重载1.运算符重载的使用2.运算符重载的注意事项 二、赋值运算符重载1.复制拷贝与拷贝构造2.赋值运算符重载的格式3.赋值运算符重载的实现4.赋值运算符重载的注意事项 一、运算符重载 1.运算符重载的使用 当我们实现一个日期类的时候&#xff0c;我们有时候…

一个*泰NL18-20漏电保护器的拆解

一个*泰NL18-20漏电保护器的拆解&#xff0c;购买很早了&#xff0c;损坏&#xff0c;按test按钮无动作&#xff0c;昨天用一个雷*的63A漏保替换了。 NL18-20的电流只有20A。显然不适合现在的运用了。而且是无灭弧装置&#xff0c;所以分断能力有限。 好奇&#xff0c;拆开来看…

C++PrimerPlus第四章编程题

编程题 题目总览 编程题题解 题目要求输入四次信息&#xff0c;有四次交互的输入&#xff08;in&#xff09;&#xff0c;最后在一口气列举出来。同时对于firstname与lastname进行了拼接&#xff0c;而且对于输入的成绩进行降级操作。同时对于名字name的要求是可以输入多个单词…

使用Flink MySQL cdc分别sink到ES、Kafka、Hudi

环境说明 [flink-1.13.1-bin-scala_2.11.tgz](https://archive.apache.org/dist/flink/flink-1.13.1/flink-1.13.1-bin-scala_2.11.tgz)[hadoop-2.7.3.tar.gz](https://archive.apache.org/dist/hadoop/common/hadoop-2.7.3/hadoop-2.7.3.tar.gz)[flink-cdc-connectors](https:…

【Base64】前后端图片交互(2)

使用Base64去处理前后端图片交互 一、Base64编码介绍二、java.util.Base64 介绍源码分析编码译码 三、使用 Base64 前后端图片交互&#xff08;实操&#xff09;四、效果展示五、总结 绪论&#xff1a;在此之前小编发过一次前后端交互处理的方式&#xff1a;前后端图片交互的简…

深度学习之图像分类(三):VGGNet

系列文章目录 本专栏介绍基于深度学习进行图像识别的经典和前沿模型&#xff0c;将持续更新&#xff0c;包括不仅限于&#xff1a;AlexNet&#xff0c; ZFNet&#xff0c;VGG&#xff0c;GoogLeNet&#xff0c;ResNet&#xff0c;DenseNet&#xff0c;SENet&#xff0c;Mobile…

Windows磁盘空间不够,发现DriverStore文件夹特别大

正想安装一个新的VS2022&#xff0c;但是发现C盘的空间已经不足&#xff0c; 显示为红色了&#xff0c;这样不能安装。只好找一下C盘的空间为什么不足了&#xff0c;后来发现有一个目录特别大&#xff0c;这个目录就是DriverStore文件夹。由于电脑已经运行5年了&#xff0c;也…

Java的线程

介绍线程 线程是系统调度的最小单元&#xff0c;一个进程可以包含多个线程&#xff0c;线程是负责执行二进制指令的。 每个线程有自己的程序计数器、栈&#xff08;Stack&#xff09;、寄存器&#xff08;Register&#xff09;、本地存储&#xff08;Thread Local&#xff09…