STL容器(libstdc++11.4版本) --- vector

之前一直在看侯捷大佬写的《STL源码剖析》的vector篇,看完了整个源码并把注释都写上去了.然后写写面试题内容,后来才发现SGI 版本没有emplace_back函数.本来打算看完这个过渡一下再去看gcc的libstdc++版本的STL源码.

直接索性一步到尾,看现代版本的STL源码,比较接近实际. SGI版本虽然代码写的好,但是确实是过时了.

根据libstdc++11.4版本,STL源码在 gcc-11.4.0/libstdc++-v3/include 文件夹中

从包含的头文件开始吧.

1. polymorphic_allocator(多态分配器)

在C++17版本中提供了一个多态分配器类,配合抽象基类memory_resource,memory_resource提供了接口,我们继承实现接口,使用不同的分配算法或与特定的资源进行交互.这两个类在头文件 memory_resource中
作用主要是: 在以前的版本中,每种分配器使用不同的类型.比如不同分配器类型如果想复制给另一个,是不能做到的

cpp 复制代码
vector<int> vec_1;	// 默认分配器
vector<int, __gnu_cxx::new_allocator<int>> vec_2;	// 使用new_allocator
vec_1 = vec_2;	// 编译报错

如果继承memory_resource配合使用polymorphic_allocator

cpp 复制代码
struct my_memory_resource : public std::pmr::memory_resource 
{
    /* 实现接口 */
    void* do_allocate(size_t __bytes, size_t __alignment) 
    {
        const char* str = "a";
        return (void*)str;
    }

    void do_deallocate(void* __p, size_t __bytes, size_t __alignment) {}

    bool do_is_equal(const memory_resource& __other) const noexcept 
    { return true; }
};

struct other_memory_resource : public std::pmr::memory_resource 
{
    /* 实现接口 */
    void* do_allocate(size_t __bytes, size_t __alignment) 
    {
        // 测试代码
        const char* str = "a";
        return (void*)str;
    }

    void do_deallocate(void* __p, size_t __bytes, size_t __alignment) {}

    bool do_is_equal(const memory_resource& __other) const noexcept 
    { return true; }
};

int main()
{
    // 定义两个对象
    my_memory_resource mem_res;
    auto my_vec = std::pmr::vector<int>(0, &mem_res);

    other_memory_resource other_res;
    auto my_other_vec = std::pmr::vector<int>(0, &other_res);
        
    my_vec = my_other_vec;    // 此处可以正常赋值    
    return 0;
}

如果想使用多态分配器polymorphic_allocator,将C++版本设置为17,然后使用 std::pmr::vector就可以使用了,源码中已经给我们声明了一个别名.

2. 针对bool类型的vector

先来看一下如何针对不同类型来匹配两种模板的

先看两个版本的定义

普通版本的vector

bool类型的vector

普通版本提供了默认分配器.

bool版本vector是模板偏特化.

当我们定义如下代码时:

cpp 复制代码
vector<bool> vec; 

因为第二个是bool偏特化版本,所以第二个匹配更加契合.然后模板参数 _Alloc 从第一个模板的默认值推断出来.


1.1 分析vetor<bool>源码

1.1.1 reference引用类型

我分析源码一般是先看成员都定义了哪些东西,然后看一下当前类定义的类型萃取。vector中没有定义任何成员,但是定义了几种类型萃取,引用类型(reference)和迭代器类型(iterator、const_iterator)

_Bit_reference类的关系图如下

_Bit_reference定义了两个成员,其中一个是: 指针_M_p ,类型是 _Bit_type 类型.

_M_p指向底层数组

  • vector<bool>的底层类型就是unsigned long类型, 分配内存大小都是以unsigned long为单位分配
  • 枚举_S_word_bit的作用是: gcc编译器在不同平台下unsigned long类型的位数大小. 比如32位unsigned为4字节,64位编译为8字节.用来表示偏移量是否超过了位数

_Bit_reference还定义了 _M_mask 成员,类型是unsigned long

配合_M_p使用, 对这个内存进行位操作. 比如对这段内存的第5位设置为1或者0.

1.1.2 迭代器类型

有一个常量和非常量迭代器都继承自 _Bit_iterator_base

基类_Bit_iterator_base又继承自std::iterator, random_access_iterator_tag表明这个迭代器可以随机访问

基类定义了两个成员: 指针 _M_p 和无符号整数类型 _M_offset ,作用和_Bit_reference里面的两个成员一样,只不过 _M_offset 是提供给 _Bit_reference 用来将第几个位置位.

_Bit_iterator_base提供了三个重要的成员函数: _M_bump_up()、_M_bump_down()、_M_incr()

_M_bump_up()

针对向容器添加元素,判断当前操作的位是否超过了上限.如果达到上限,将 _M_offset 设置为0,然后将 _M_p 增加一个单位.也就是表示在unsigned long大小的位数已经全部用完了,指向下一个unsigned long. 如果这句话还不懂,就是数组下标加加,指向数组下一个位置.然后在一个位一个位的设置.

_M_bump_down()

针对删除容器元素. 和_M_bump_up()相反

_M_incr()

针对随机访问或数组下标访问.

1.1.3 vector类

接下来就分析主角vector了,关系图如下:

vector<bool>继承子Bvector_base

Bvector_base里面定义了两个类: 分别是Bvector_impl和_Bvector_impl_data

可以看到, _Bvector_base 类定义了一个成员: _M_impl . vector<bool>底层操作的就是这个成员.这个成员追溯到就是 _Bvector_impl_data 类型.

然后再看_Bvector_impl_data类型,该类定义了三个成员,分别是:

  • _M_start: 指向内存首地址,也是begin()、rbegin()所使用的
  • _M_finish: 指向下一个空闲的位置.比如容器容量为5,已经插入了3个元素,那么finish则指向第4个位置,用于下一次插入.
    _M_finish - _M_start也就是size()函数的返回值,表示容器有多少个元素
  • _M_end_of_storage: 指向容器末尾元素, 这个值减去 _M_start 就表示容器总容量

画一个图就是下面这样,分别指向不同的位置

1.1.3.1 初始化

vector总共了使用以下几个函数用来初始化:

  • _M_initialize: 分配内存,并设置相应的指针
  • _M_initialize_value: 对已分配的内存设置相应的值
  • _M_copy_aligned: 将first~last迭代器范围内的数据复制到目标迭代器result中,返回目标迭代器拷贝后的末尾元素地址
  • _M_initialize_range: 将first~last迭代器范围内的数据复制到当前容器中
1.1.3.2 成员访问

at()函数 以下是函数调用流程图

具体原理就是:

获取begin()迭代器,该迭代器有成员_M_p和_M_offset.

然后设置_M_p偏移量, 将at函数的参数传给_M_incr()函数设置_M_offset. 然后 获取_M_p指向的内存中第 (1UL << _M_offset)位的内容

operator[] 和at()函数原理一样

front()

获取begin()指向的内存首元素内容

back() end()函数指向末尾元素的下一个位置,所以需要减去1,即获取最后一个元素内容

data()返回底层指针

vector<bool>的data函数不返回任何东西


1.1.3.3 迭代器

begin() 获取_M_p指向内存的数据

end() 获取_M_finishi指向内存的数据

rbegin()、rend()


1.1.3.4 容量

empty()

size()

reserve()

capacity() 末尾地址减去首地址获取的偏移量,然后乘上 _S_word_bit,则是容器容量

shrink_to_fit()


1.1.3.5 修饰符

clear() 将_M_finish重新指向内存首地址,这样就达到了清空的功能

insert()

_M_insert_aux函数:

  1. 版本1: 在position位置插入元素
  1. 版本2: 在position位置插入first~last迭代器范围内的元素

常量迭代器版本

非常量迭代器版本

  1. 版本3: 在position位置插入个数n,插入数据是参数x

emplace()

底层调用insert函数,在position位置插入元素

erase()

未写完,明天再写。。。

相关推荐
AskHarries1 小时前
Java字节码增强库ByteBuddy
java·后端
佳佳_1 小时前
Spring Boot 应用启动时打印配置类信息
spring boot·后端
许野平2 小时前
Rust: 利用 chrono 库实现日期和字符串互相转换
开发语言·后端·rust·字符串·转换·日期·chrono
BiteCode_咬一口代码3 小时前
信息泄露!默认密码的危害,记一次网络安全研究
后端
齐 飞4 小时前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb
LunarCod4 小时前
WorkFlow源码剖析——Communicator之TCPServer(中)
后端·workflow·c/c++·网络框架·源码剖析·高性能高并发
码农派大星。5 小时前
Spring Boot 配置文件
java·spring boot·后端
杜杜的man6 小时前
【go从零单排】go中的结构体struct和method
开发语言·后端·golang
幼儿园老大*6 小时前
走进 Go 语言基础语法
开发语言·后端·学习·golang·go
llllinuuu6 小时前
Go语言结构体、方法与接口
开发语言·后端·golang