0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

一文带你手撕 STL 容器源码(上)

Linux爱好者 来源:CSDN技术社区 作者:herongweiV 2021-04-30 15:59 次阅读
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

前言源码之前,了无秘密。

在 STL 编程中,容器是我们经常会用到的一种数据结构,容器分为序列式容器和关联式容器。

两者的本质区别在于:序列式容器是通过元素在容器中的位置顺序存储和访问元素,而关联容器则是通过键 (key) 存储和读取元素。

本篇着重剖析序列式容器相关背后的知识点。

a787a908-a83e-11eb-9728-12bb97331649.png

容器分类前面提到了,根据元素存储方式的不同,容器可分为序列式和关联式,那具体的又有哪些分类呢,这里我画了一张图来看一下。

a7d7a6a6-a83e-11eb-9728-12bb97331649.png

限于篇幅,这篇文章小贺会来重点讲解一下经常使用到的那些容器,比如 vector,list,deque,以及衍生的栈和队列其背后核心的设计和奥秘,不多 BB, 马上就来分析。

vector写 C++ 的小伙伴们,应该对 vector 都非常熟悉了,vector 基本能够支持任何类型的对象,同时它也是一个可以动态增长的数组,使用起来非常的方便。

但如果我问你,知道它是如何做到动态扩容的吗?哎,是不是一时半会答不上来了,哈哈,没事,我们一起来看看。

vector 基本数据结构

基本上,STL 里面所有的容器的源码都包含至少三个部分:

迭代器,遍历容器的元素,控制容器空间的边界和元素的移动;

构造函数,满足容器的多种初始化;

属性的获取,比如 begin(),end()等;

vector 也不例外,其实看了源码之后就发现,vector 相反是所有容器里面最简单的一种。

template 《class T, class Alloc = alloc》

class vector {public:

// 定义 vector 自身的嵌套型别

typedef T value_type;

typedef value_type* pointer;

typedef const value_type* const_pointer;

// 定义迭代器, 这里就只是一个普通的指针

typedef value_type* iterator;

typedef const value_type* const_iterator;

typedef value_type& reference;

typedef const value_type& const_reference;

typedef size_t size_type;

typedef ptrdiff_t difference_type;

。。.

protected:

typedef simple_alloc《value_type, Alloc》 data_allocator; // 设置其空间配置器

iterator start; // 当前使用空间的头

iterator finish; // 当前使用空间的尾

iterator end_of_storage; // 当前可用空间的尾

。。.

};

因为 vector 需要表示用户操作的当前数据的起始地址,结束地址,还需要其真正的最大地址。所以总共需要 3 个迭代器,分别来指向数据的头(start),数据的尾(finish),数组的尾(end_of_storage)。

构造函数

vector 有多个构造函数, 为了满足多种初始化。

a7e98254-a83e-11eb-9728-12bb97331649.png

我们看到,这里面,初始化满足要么都初始化成功, 要么一个都不初始化并释放掉抛出异常,在 STL 里面,异常机制这块拿捏的死死的呀。

因为 vector 是一种 class template, 所以呢,我们并不需要手动的释放内存, 生命周期结束后就自动调用析构从而释放调用空间,当然我们也可以直接调用析构函数释放内存。

void deallocate() {

if (start)

data_allocator::deallocate(start, end_of_storage - start);

}

// 调用析构函数并释放内存

~vector() {

destroy(start, finish);

deallocate();

}

属性获取

下面的部分就涉及到了位置参数的获取, 比如返回 vector 的开始和结尾,返回最后一个元素,返回当前元素个数,元素容量,是否为空等。

这里需要注意的是因为 end() 返回的是 finish,而 finish 是指向最后一个元素的后一个位置的指针,所以使用 end() 的时候要注意。

public:

// 获取数据的开始以及结束位置的指针。 记住这里返回的是迭代器, 也就是 vector 迭代器就是该类型的指针。

iterator begin() { return start; }

iterator end() { return finish; }

reference front() { return *begin(); } // 获取值

reference back() { return *(end() - 1); }

。。.

size_type size() const { return size_type(end() - begin()); } // 数组元素的个数

size_type max_size() const { return size_type(-1) / sizeof(T); } // 最大能存储的元素个数

size_type capacity() const { return size_type(end_of_storage - begin()); } // 数组的实际大小

bool empty() const { return begin() == end(); }

//判断 vector 是否为空, 并不是比较元素为 0,是直接比较头尾指针。

push 和 pop 操作

vector 的 push 和 pop 操作都只是对尾进行操作, 这里说的尾部是指数据的尾部。当调用 push_back 插入新元素的时候,首先会检查是否有备用空间,如果有就直接在备用空间上构造元素,并调整迭代器 finish。

a7f741a0-a83e-11eb-9728-12bb97331649.png

当如果没有备用空间,就扩充空间(重新配置-移动数据-释放原空间),这里实际是调用了另外一个函数:insert_aux 函数。

a8081d7c-a83e-11eb-9728-12bb97331649.png

在上面这张图里,可以看到,push_back 这个函数里面又判断了一次 finish != end_of_storage 这是因为啥呢?这里的原因是因为 insert_aux 函数可能还被其他函数调用哦。

在下面的 else 分支里面,我们看到了 vector 的动态扩容机制:如果原空间大小为 0 则分配 1 个元素,如果大于 0 则分配原空间两倍的新空间,然后把数据拷贝过去。

a81ea650-a83e-11eb-9728-12bb97331649.png

pop 元素:从尾端删除一个元素。

public:

//将尾端元素拿掉 并调整大小

void pop_back() {

--finish;//将尾端标记往前移动一个位置 放弃尾端元素

destroy(finish);

}

erase 删除元素

erase 函数清除指定位置的元素, 其重载函数用于清除一个范围内的所有元素。实际实现就是将删除元素后面所有元素往前移动,对于 vector 来说删除元素的操作开销还是很大的,所以说 vector 它不适合频繁的删除操作,毕竟它是一个数组。

//清楚[first, last)中的所有元素

iterator erase(iterator first, iterator last) {

iterator i = copy(last, finish, first);

destroy(i, finish);

finish = finish - (last - first);

return first;

}

//清除指定位置的元素

iterator erase(iterator position) {

if (position + 1 != end())

copy(position + 1, finish, position);//copy 全局函数

}

--finish;

destroy(finish);

return position;

}

void clear() {

erase(begin(), end());

}

我们结合图解来看一下:

a8722f82-a83e-11eb-9728-12bb97331649.png

清楚范围内的元素,第一步要将 finish 迭代器后面的元素拷贝回去,然后返回拷贝完成的尾部迭代器,最后在删除之前的。

删除指定位置的元素就是实际就是将指定位置后面的所有元素向前移动, 最后析构掉最后一个元素。

insert 插入元素

vector 的插入元素具体来说呢,又分三种情况:

1、如果备用空间足够且插入点的现有元素多于新增元素;

2、如果备用空间足够且插入点的现有元素小于新增元素;

3、如果备用空间不够;

我们一个一个来分析。

插入点之后的现有元素个数 》 新增元素个数

a8a3c646-a83e-11eb-9728-12bb97331649.png

插入点之后的现有元素个数 《= 新增元素个数

a8b35a52-a83e-11eb-9728-12bb97331649.png

如果备用空间不足

a8c46f68-a83e-11eb-9728-12bb97331649.png

这里呢,要注意一个坑,就是所谓的迭代器失效问题。通过图解我们就明白了,所谓的迭代器失效问题是由于元素空间重新配置导致之前的迭代器访问的元素不在了,总结来说有两种情况:

由于插入元素,使得容器元素整体迁移导致存放原容器元素的空间不再有效,从而使得指向原空间的迭代器失效。

由于删除元素,使得某些元素次序发生变化导致原本指向某元素的迭代器不再指向期望指向的元素。

前面提到的一些全局函数,这里在总结一下:

copy(a,b,c):将(a,b)之间的元素拷贝到(c,c-(b-a))位置

uninitialized_copy(first, last, result):具体作用是将 [first,last)内的元素拷贝到 result 从前往后拷贝

copy_backward(first, last, result):将 [first,last)内的元素拷贝到 result 从后往前拷贝

vector 总结

到这里呢,vector 分析的就差不多了,最后提醒需要注意的是:vector 的成员函数都不做边界检查 (at方法会抛异常),使用者要自己确保迭代器和索引值的合法性。

我们来总结一下 vector 的优缺点。

优点

在内存中是分配一块连续的内存空间进行存储,可以像数组一样操作,并且支持动态扩容。

因此元素的随机访问方便,支持下标访问和 vector.at() 操作。

节省空间。

缺点

由于其顺序存储的特性,vector 插入删除操作的时间复杂度是 O(n)。

只能在末端进行 pop 和 push。

当动态长度超过默认分配大小后,要整体重新分配、拷贝和释放空间。

list好了,下面我们来看一下 list,list 是一种双向链表。

list 的设计更加复杂一点,好处是每次插入或删除一个元素,就配置或释放一个元素,list 对于空间的运用有绝对的精准,一点也不浪费。而且对于任何位置的元素插入或删除,list 永远是常数空间。

注意:list 源码里其实分了两个部分,一个部分是 list 结构,另一部分是 list 节点的结构。

那这里不妨思考一下,为什么 list 节点分为了两个部分,而不是在一个结构体里面呢? 也就是说为什么指针变量和数据变量分开定义呢?

如果看了后面的源码就晓得了,这里是为了给迭代器做铺垫,因为迭代器遍历的时候不需要数据成员的,只需要前后指针就可以遍历该 list。

list 的节点结构如下图所示:

a8cf43c0-a83e-11eb-9728-12bb97331649.png

list 数据结构-节点

__list_node 用来实现节点,数据结构中就储存前后指针和属性。

template 《class T》 struct __list_node {

// 前后指针

typedef void* void_pointer;

void_pointer next;

void_pointer prev;

// 属性

T data;

};

来瞅一瞅,list 的节点长啥样,因为 list 是一种双向链表,所以基本结构就是下面这个样子:

a8df3bcc-a83e-11eb-9728-12bb97331649.png

基本类型

template《class T, class Ref, class Ptr》 struct __list_iterator {

typedef __list_iterator《T, T&, T*》 iterator; // 迭代器

typedef __list_iterator《T, const T&, const T*》 const_iterator;

typedef __list_iterator《T, Ref, Ptr》 self;

// 迭代器是bidirectional_iterator_tag类型

typedef bidirectional_iterator_tag iterator_category;

typedef T value_type;

typedef Ptr pointer;

typedef Ref reference;

typedef size_t size_type;

typedef ptrdiff_t difference_type;

。。.

};

构造函数

template《class T, class Ref, class Ptr》 struct __list_iterator {

。。.

// 定义节点指针

typedef __list_node《T》* link_type;

link_type node;

// 构造函数

__list_iterator(link_type x) : node(x) {}

__list_iterator() {}

__list_iterator(const iterator& x) : node(x.node) {}

。。.

};

重载

template《class T, class Ref, class Ptr》 struct __list_iterator {

。。.

// 重载

bool operator==(const self& x) const { return node == x.node; }

bool operator!=(const self& x) const { return node != x.node; }

。。.

// ++和--是直接操作的指针指向next还是prev, 因为list是一个双向链表

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 结构

list 自己定义了嵌套类型满足 traits 编程, list 迭代器是 bidirectional_iterator_tag 类型,并不是一个普通指针。

a9291c24-a83e-11eb-9728-12bb97331649.png

list 在定义 node 节点时, 定义的不是一个指针,这里要注意。

template 《class T, class Alloc = alloc》

class list {protected:

typedef void* void_pointer;

typedef __list_node《T》 list_node; // 节点 就是前面分析过的

typedef simple_alloc《list_node, Alloc》 list_node_allocator; // 空间配置器public:

// 定义嵌套类型

typedef T value_type;

typedef value_type* pointer;

typedef const value_type* const_pointer;

typedef value_type& reference;

typedef const value_type& const_reference;

typedef list_node* link_type;

typedef size_t size_type;

typedef ptrdiff_t difference_type;

protected:

// 定义一个节点, 这里节点并不是一个指针。

link_type node;

public:

// 定义迭代器

typedef __list_iterator《T, T&, T*》 iterator;

typedef __list_iterator《T, const T&, const T*》 const_iterator;

。。.

};

list 构造和析构函数实现

构造函数前期准备:

每个构造函数都会创造一个空的 node 节点,为了保证我们在执行任何操作都不会修改迭代器。

list 默认使用 alloc 作为空间配置器,并根据这个另外定义了一个 list_node_allocator,目的是更加方便以节点大小来配置单元。

template 《class T, class Alloc = alloc》

class list {protected:

typedef void* void_pointer;

typedef __list_node《T》 list_node; // 节点

typedef simple_alloc《list_node, Alloc》 list_node_allocator; // 空间配置器

其中,list_node_allocator(n) 表示配置 n 个节点空间。以下四个函数,分别用来配置,释放,构造,销毁一个节点。

class list {protected:

// 配置一个节点并返回

link_type get_node() { return list_node_allocator::allocate(); }

// 释放一个节点

void put_node(link_type p) { list_node_allocator::deallocate(p); }

// 产生(配置并构造)一个节点带有元素初始值

link_type create_node(const T& x) {

link_type p = get_node();

__STL_TRY {

construct(&p-》data, x);

}

__STL_UNWIND(put_node(p));

return p;

}

//销毁(析构并释放)一个节点

void destroy_node(link_type p) {

destroy(&p-》data);

put_node(p);

}

// 对节点初始化

void empty_initialize() {

node = get_node();

node-》next = node;

node-》prev = node;

}

};

基本属性获取

template 《class T, class Alloc = alloc》

class list {

。。.

public:

iterator begin() { return (link_type)((*node).next); } // 返回指向头的指针

const_iterator begin() const { return (link_type)((*node).next); }

iterator end() { return node; } // 返回最后一个元素的后一个的地址

const_iterator end() const { return node; }

// 这里是为旋转做准备, rbegin返回最后一个地址, rend返回第一个地址。 我们放在配接器里面分析

reverse_iterator rbegin() { return reverse_iterator(end()); }

const_reverse_iterator rbegin() const {

return const_reverse_iterator(end());

}

reverse_iterator rend() { return reverse_iterator(begin()); }

const_reverse_iterator rend() const {

return const_reverse_iterator(begin());

}

// 判断是否为空链表, 这是判断只有一个空node来表示链表为空。

bool empty() const { return node-》next == node; }

// 因为这个链表, 地址并不连续, 所以要自己迭代计算链表的长度。

size_type size() const {

size_type result = 0;

distance(begin(), end(), result);

return result;

}

size_type max_size() const { return size_type(-1); }

// 返回第一个元素的值

reference front() { return *begin(); }

const_reference front() const { return *begin(); }

// 返回最后一个元素的值

reference back() { return *(--end()); }

const_reference back() const { return *(--end()); }

// 交换

void swap(list《T, Alloc》& x) { __STD::swap(node, x.node); }

。。.

};

template 《class T, class Alloc》

inline void swap(list《T, Alloc》& x, list《T, Alloc》& y) {

x.swap(y);

编辑:jq

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 函数
    +关注

    关注

    3

    文章

    4406

    浏览量

    66833
  • 迭代器
    +关注

    关注

    0

    文章

    45

    浏览量

    4585

原文标题:2 万字 + 20 图带你手撕 STL 容器源码

文章出处:【微信号:LinuxHub,微信公众号:Linux爱好者】欢迎添加关注!文章转载请注明出处。

收藏 人收藏
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

    STL320N4LF8 N沟道功率MOSFET技术解析与应用指南

    意法半导体 STL320N4LF8 N沟道STripFET F8功率MOSFET 采用STripFET F8沟槽式MOSFET技术制造而成。 该器件完全符合工业级标准。STL320N4LF8可降低
    的头像 发表于 10-29 15:48 394次阅读
    ‌<b class='flag-5'>STL</b>320N4LF8 N沟道功率MOSFET技术解析与应用指南

    STL325N4LF8AG N通道功率MOSFET技术解析与应用指南

    意法半导体STL325N4LF8AG N沟道功率MOSFET采用STripFET F8技术,具有增强型沟槽栅极结构。 STL325N4LF8AG可确保非常低的导通电阻。该器件还降低内部电容和栅极电荷,实现更快、更高效的开关。
    的头像 发表于 10-29 15:34 339次阅读
    <b class='flag-5'>STL</b>325N4LF8AG N通道功率MOSFET技术解析与应用指南

    STL120N10F8功率MOSFET技术解析与应用指南

    STMicroelectronics STL120N10F8100V N沟道增强模式STripFET MOSFET采用ST的STripFET F8技术,具有增强型沟槽栅极结构。它确保极低的导通电
    的头像 发表于 10-25 09:55 798次阅读
    <b class='flag-5'>STL</b>120N10F8功率MOSFET技术解析与应用指南

    智能小车设计源码和图纸资料

    智能小车设计源码和图纸
    发表于 08-25 15:38 1次下载

    带你了解海凌科毫米波雷达

    什么是毫米波雷达?毫米波雷达有什么特点?毫米波雷达有什么作用?海凌科有哪些系列毫米波雷达?带你了解!毫米波的定义毫米波是指频率在30GHz至300GHz之间、波长为1~10毫米的电磁波,兼具微波
    的头像 发表于 08-11 12:04 1090次阅读
    <b class='flag-5'>一</b><b class='flag-5'>文</b><b class='flag-5'>带你</b>了解海凌科毫米波雷达

    Docker容器安全攻防实战案例

    在云原生时代,Docker已成为现代应用部署的基石。然而,容器化带来便利的同时,也引入了新的安全挑战。作为名在生产环境中管理过数千个容器的运维工程师,我将通过真实的攻防实战案例,带你
    的头像 发表于 08-05 09:52 1006次阅读

    在 Film Frame 和 Waffle Pack 提供的 Hypersudden 调谐变容器 skyworksinc

    电子发烧友网为你提供()在 Film Frame 和 Waffle Pack 提供的 Hypersudden 调谐变容器相关产品参数、数据手册,更有在 Film Frame 和 Waffle
    发表于 07-10 18:32
    在 Film Frame 和 Waffle Pack <b class='flag-5'>上</b>提供的 Hypersudden 调谐变<b class='flag-5'>容器</b> skyworksinc

    带你了解电源测试系统的功能!

    在当今电子与电力技术飞速发展的时代,各类电子设备、电力系统以及新能源相关产品的研发、生产和维护过程中,电源测试系统扮演着至关重要的角色。本文将带你了解源仪电子的电源测试系统的功能。
    的头像 发表于 07-02 09:10 623次阅读
    <b class='flag-5'>一</b><b class='flag-5'>文</b><b class='flag-5'>带你</b>了解电源测试系统的功能!

    【经验分享】在Omni3576编译Redis-8.0.2源码,并安装及性能测试

    本文首先介绍Redis是什么,然后介绍如何在Omni3576编译Redis-8.0.2源码,以及从源码编译、安装Redis,最后介绍如何在Omni3576运行Redis性能测试,并
    的头像 发表于 06-05 08:05 763次阅读
    【经验分享】在Omni3576<b class='flag-5'>上</b>编译Redis-8.0.2<b class='flag-5'>源码</b>,并安装及性能测试

    带你深入剖析RedCap技术

    与需求。在此基础,结合RedCap的技术优势,推动其产业生态的成熟与技术的快速落地,从而有力支撑千行百业的“数智化”转型。RedCap(ReducedCapab
    的头像 发表于 05-14 10:53 2754次阅读
    <b class='flag-5'>一</b><b class='flag-5'>文</b><b class='flag-5'>带你</b>深入剖析RedCap技术

    【必看】开关电源中每个元器件的计算+51页图文详解

    开关电源的各个元器件怎么计算?损耗怎么估算?散热器的大小怎么计算? 51页图文详解,带你弄懂! 纯分享贴,有需要可以直接下载附件获取完整资料! (如果内容有帮助可以关注、点赞、评论支持
    发表于 05-12 16:20

    STM32串口接受中断使用C++STL中的queue导致所有中断失效

    heap4.c中的pvPortMalloc重载new和定义了内存分配器OSAllocator(下附源码),目的是使用STL库时用FreeRTOS托管内存 单片机启动文件中Heap_Size设置为0
    发表于 01-24 10:21

    带你读懂EBSD

    电子背散射衍射(ElectronBackscatterDiffraction,简称EBSD)技术是种基于扫描电子显微镜(SEM)的显微分析技术,它能够提供材料微观结构的详细信息,包括晶体取向
    的头像 发表于 01-14 12:00 2758次阅读
    <b class='flag-5'>一</b><b class='flag-5'>文</b><b class='flag-5'>带你</b>读懂EBSD

    在华为云 FlexusX 实例实现 Docker 容器的实时监控与可视化分析

    前言 华为云 Flexus X,以顶尖算力与智能调度,引领 Docker 容器管理新风尚。828 企业云节之际,Flexus X 携手前沿技术,实现容器运行的实时监控与数据可视化,让管理变得直观
    的头像 发表于 01-02 13:42 781次阅读
    在华为云 FlexusX 实例<b class='flag-5'>上</b>实现 Docker <b class='flag-5'>容器</b>的实时监控与可视化分析

    SSM框架的源码解析与理解

    的核心是控制反转(IoC)和面向切面编程(AOP)。 源码解析: Spring的源码主要分为以下几个部分: Bean容器: 负责实例化、配置和组装对象。核心接口是 B
    的头像 发表于 12-17 09:20 1418次阅读