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

1,233 阅读9分钟

之前一直在看侯捷大佬写的《STL源码剖析》的vector篇,看完了整个源码并把注释都写上去了.然后写写面试题内容,后来才发现SGI 版本没有emplace_back函数.本来打算看完这个过渡一下再去看gcc的libstdc++版本的STL源码.
直接索性一步到尾,看现代版本的STL源码,比较接近实际. SGI版本虽然代码写的好,但是确实是过时了.

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

从包含的头文件开始吧.

1. polymorphic_allocator(多态分配器)

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

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

如果继承memory_resource配合使用polymorphic_allocator

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

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

先看两个版本的定义
普通版本的vector image.png

bool类型的vector image.png

普通版本提供了默认分配器.
bool版本vector是模板偏特化.

当我们定义如下代码时:

vector<bool> vec; 

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


1.1 分析vetor<bool>源码

源码在 gcc-11.4.0\libstdc++-v3\include\bits\stl_bvector.h中

1.1.1 reference引用类型

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

image.png

_Bit_reference类的关系图如下 image.png

_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.

image.png

1.1.2 迭代器类型

image.png

有一个常量和非常量迭代器都继承自 _Bit_iterator_base
基类_Bit_iterator_base又继承自std::iterator, random_access_iterator_tag表明这个迭代器可以随机访问

image.png

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

image.png

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

_M_bump_up()

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

image.png

_M_bump_down()

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

image.png

_M_incr()

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

image.png

1.1.3 vector<bool>类

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

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 就表示容器总容量

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

image.png

1.1.3.1 初始化

vector<bool>默认使用new_allocator来分配、释放内存.该底层使用的是operator new来分配内存.所以当我们自己重写了全局operator new,分配内存的时候会来调用我们自己编写的函数.
源代码均在gcc-11.4.0\libstdc++-v3\include\ext目录中
libstdc++一共提供了如下几个allocator:

  1. bitmap_allocator
  2. debug_allocator
  3. extptr_allocator
  4. malloc_allocator
  5. mt_allocator
  6. new_allocator
  7. pool_allocator

先在这放个假链接,写一篇关于allocator的

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

比如下图构造函数:

image.png

1.1.3.2 成员访问

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

image.png

具体原理就是:
获取begin()迭代器,该迭代器有成员_M_p和_M_offset.
然后设置_M_p偏移量, 将at函数的参数传给_M_incr()函数设置_M_offset. 然后 获取_M_p指向的内存中第 (1UL << _M_offset)位的内容

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

front()

image.png
获取begin()指向的内存

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

data()返回底层指针

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


1.1.3.3 迭代器

begin() image.png 获取_M_p指向的内存

end() image.png 获取_M_finishi指向的内存

rbegin()、rend() image.png

image.png 返回reverse_iterator反向迭代器,该迭代器定义在gcc-11.4.0\libstdc++-v3\include\bits\stl_iterator.h


1.1.3.4 容量

empty() image.png

size() image.png

reserve() image.png image.png

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

shrink_to_fit()
image.png image.png


1.1.3.5 修饰符

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

insert()
_M_insert_aux函数:
image.png

  1. 版本1: 在position位置插入元素

image.png

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

常量迭代器版本 image.png

非常量迭代器版本 image.png

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

image.png image.png

emplace()

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

erase() image.png 分别有两个版本的erase,底层都是调用 _M_erase()函数,该函数也有两个版本.

第一个版本的 _M_erase()函数 image.png

第二个版本的 _M_erase()函数 image.png image.png 原理如下图所示 image.png image.png 第一步std::copy将last~end()范围内的元素复制到first.然后 _M_finish 指向std::copy()的返回值

push_back() image.png image.png

emplace_back() image.png 底层调用push_back()

pop_back() image.png 将finish指向上一个元素

resize() image.png

swap() 分别有三个不同版本 image.png


1.2 分析普通版本vector

源码在 gcc-11.4.0\libstdc++-v3\include\bits\stl_vector.h中

1.2.1 reference(引用)类型

image.png 上面这个_Alloc_traits 使用了模板参数_Tp_alloc_type, _Tp_alloc_type的具体细节如下 image.png

_Alloc_traits又是 __gnu_cxx::__alloc_traits 的别名,这个定义在include/ext/alloc_traits.h文件中 image.png

1.2.2 iterator(迭代器)类型

image.png __gnu_cxx::__normal_iterator定义在 include/bits/stl_iteartor.h 文件中.
该模板接收两个模板参数: 迭代器和容器
请注意: 这里传入了 pointervector 类型给__normal_iterator
__normal_iterator具体细节如下:
image.png 该类定义了一个迭代器成员,类型是用户传递的迭代器类型.由于上面传递的是pointer类型,所以底层操作都是指针.

1.2.3 vecotr类剖析

关系图如下:
image.png 实现的细节和vector<bool>差不多.也都有 _M_start、_M_finish、_M_end_of_storage三个成员.
只不过分配元素的时候不再以unsigned long为单位,而是以用户传递的元素类型为单位分配内存.
上面三个成员都是以指针pointer形式,不再是以iterator类型操作.

关系图了解了,接下来还是老样子,分析每个成员函数来查看底层是如何实现的.

1.2.4 源码解析(源码中注释写的非常详细了)

各个原理大致和vector<bool>差不多,下面介绍有点区别的函数.

1.2.4.1 reserve

image.png

1.2.4.2 emplace

image.png 注意一下,函数的第二个参数是_Args&&...

这是一个右值引用可变参数,并不是容器元素的类型.然后底层调用 _M_emplace_aux函数,并将参数包__args转发给这个函数.
也就是,传递给这个函数并不需要元素类型的对象,元素值也可以.
这也是push_back和emplace_back的区别,后面再说

下面是_M_emplace_aux细节 image.png

1.2.4.3 emplace_back和push_back的区别

push_back实现细节(一共有两个版本):
image.png

emplace_back实现细节:
image.png

对比一下两个函数:
image.png

  • 主要区别就是两个函数的参数类型不一样.其余实现细节完全一样.而且push_back也有个接收右值引用的参数.
  • emplace_back在 _M_finish上构造内存时,使用了转发,保留了参数的引用类型.所以当emplace_back传递的是右值引用,那么调用 _Alloc_traits::contruct() 时,最终使用移动构造.否则传递的是左值引用,最终调用拷贝构造.
  • push_back无论传递什么类型,最终都会调用拷贝构造在_M_finish上构造对象.

这就是这两个函数的差异.

1.2.4.4 vector当内存不足时分配策略

vector当容量不足时,分配当前元素数量的2倍大小内存

看一下容器满的时候调用 _M_realloc_insert() 函数,内部是如何分配内存大小的

image.png image.png

完结