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

    文章

    4276

    浏览量

    62316
  • 迭代器
    +关注

    关注

    0

    文章

    43

    浏览量

    4296

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

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

收藏 人收藏

    评论

    相关推荐

    智慧公交是什么?带你详解智慧公交的解决方案!

    智慧公交是什么?带你详解智慧公交的解决方案!
    的头像 发表于 11-05 12:26 81次阅读
    智慧公交是什么?<b class='flag-5'>一</b><b class='flag-5'>文</b><b class='flag-5'>带你</b>详解智慧公交的解决方案!

    容器断电电为什么会炸管

    容器断电后重新电时发生“炸管”现象,通常是由以下几个原因引起的: 1、 残余电荷影响 : 电容器在断电后可能会残留定的电荷。如果没有正确的放电措施,当重新
    的头像 发表于 09-18 14:13 249次阅读
    电<b class='flag-5'>容器</b>断电<b class='flag-5'>上</b>电为什么会炸管

    带你了解IP地址别名

    、什么是IP地址别名 IP地址别名是将多个IP地址与个网络接口关联起来的种方式。实现在网络个节点可以与网络建立多个连接,每个连接
    的头像 发表于 09-05 14:11 156次阅读

    ElfBoard技术贴|在NXP源码基础适配ELF 1开发板的按键功能

    ,将以按键功能的适配作为具体示例,深入解析整个适配的流程。、准备工作NXP源码路径:ELF1开发板资料包\07-NXP原厂资料\07-1NXP官方源码\linux-
    的头像 发表于 07-10 09:54 511次阅读
    ElfBoard技术贴|在NXP<b class='flag-5'>源码</b>基础<b class='flag-5'>上</b>适配ELF 1开发板的按键功能

    什么是源码源码有什么作用?源码组件是什么?源码可二次开发吗?

    源码,也称为源程序,是指未编译的按照定的程序设计语言规范书写的文本文件,是系列人类可读的计算机语言指令。
    的头像 发表于 05-25 14:55 1.4w次阅读
    什么是<b class='flag-5'>源码</b>?<b class='flag-5'>源码</b>有什么作用?<b class='flag-5'>源码</b>组件是什么?<b class='flag-5'>源码</b>可二次开发吗?

    正片负片pcb如何分辨?带你读懂

    正片负片pcb是指在PCB制造过程中,制作两个图案的方法之。今天捷多邦小编就与大家聊聊正片负片pcb~ 正片是PCB去除电路板不需要的部分,留下需要的金属图案。而负片则是PCB保留不需要的部分
    的头像 发表于 04-15 17:41 1782次阅读

    带你了解PWM原理、频率与占空比

    ,电机的转速就是周期内输出的平均电压值,所以实质我们调速是将电机处于种,似停非停,似全速转动又非全速转动的状态,那么在个周期的平均速度就是我们占空比调出来的速度了,在电机控制中,电压越大,电机转速
    发表于 03-27 14:12

    带你了解红墨水实验!

    、什么是红墨水实验? 将焊点置于红色墨水或染料中, 让红墨水或染料渗入焊点的裂纹之中,干燥后将焊点强行分离, 焊点般会从薄弱的环节(裂纹处)开裂。 因此,红墨水实验可以通过检查开裂处界面的染色
    的头像 发表于 02-26 11:24 1954次阅读
    <b class='flag-5'>一</b><b class='flag-5'>文</b><b class='flag-5'>带你</b>了解红墨水实验!

    容器的作用及原理 电容器的使用方法

    容器,又称电容,是种用于存储电能的装置。它由两个导体电极和介质组成,能够在这两个电极存储和释放电荷。电容器的作用和原理,以及其使用方法都是电学中基础的概念。下面将详细介绍电
    的头像 发表于 01-18 13:47 2.2w次阅读

    带你了解FPGA直方图操作

    方面,统计过程中需要地址信息,因此选择 RAM 形式的存储器。 基,直方图统计步骤如下: 将当前统计值读出,加 1 后重新写入 RAM 重复以上步骤,直到当前图像统计完毕 在下幅图像到来之前将结果读出
    发表于 01-10 15:07

    带你了解 DAC

    了解 DAC
    的头像 发表于 12-07 15:10 8420次阅读
    <b class='flag-5'>一</b><b class='flag-5'>文</b><b class='flag-5'>带你</b>了解 DAC

    读懂,什么是BLE?

    读懂,什么是BLE?
    的头像 发表于 11-27 17:11 2101次阅读
    <b class='flag-5'>一</b><b class='flag-5'>文</b>读懂,什么是BLE?

    epoll源码分析

    个函数进行源码分析。 源码来源 由于epoll的实现内嵌在内核中,直接查看内核源码的话会有些无关代码影响阅读。为此在GitHub写的简化
    的头像 发表于 11-13 11:49 955次阅读
    epoll<b class='flag-5'>源码</b>分析

    STL内容介绍

    1 什么是STLSTL(Standard Template Library),即标准模板库,是个具有工业强度的,高效的C++程序库。它被容纳于C++标准程序库(C++ Standard
    的头像 发表于 11-13 11:32 757次阅读
    <b class='flag-5'>STL</b>内容介绍

    C++中STL容器中的常见容器及基本操作

    、什么是容器? 所谓容器,就是可以承载,包含元素的个器件,它是STL六大组件之,是
    的头像 发表于 11-10 11:23 426次阅读
    C++中<b class='flag-5'>STL</b><b class='flag-5'>容器</b>中的常见<b class='flag-5'>容器</b>及基本操作