之前一直在看侯捷大佬写的《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中
作用主要是: 在以前的版本中,每种分配器使用不同的类型.比如不同分配器类型如果想复制给另一个,是不能做到的
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
先来看一下如何针对不同类型来匹配两种模板的
先看两个版本的定义
普通版本的vector
bool类型的vector
普通版本提供了默认分配器.
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)
_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<bool>类
接下来就分析主角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<bool>默认使用new_allocator来分配、释放内存.该底层使用的是operator new来分配内存.所以当我们自己重写了全局operator new,分配内存的时候会来调用我们自己编写的函数.
源代码均在gcc-11.4.0\libstdc++-v3\include\ext目录中
libstdc++一共提供了如下几个allocator:
- bitmap_allocator
- debug_allocator
- extptr_allocator
- malloc_allocator
- mt_allocator
- new_allocator
- pool_allocator
- _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()
返回reverse_iterator反向迭代器,该迭代器定义在gcc-11.4.0\libstdc++-v3\include\bits\stl_iterator.h中
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: 在position位置插入元素
- 版本2: 在position位置插入first~last迭代器范围内的元素
常量迭代器版本
非常量迭代器版本
- 版本3: 在position位置插入个数n,插入数据是参数x
emplace()
底层调用insert函数,在position位置插入元素
erase()
分别有两个版本的erase,底层都是调用 _M_erase()函数,该函数也有两个版本.
第一个版本的 _M_erase()函数
第二个版本的 _M_erase()函数
原理如下图所示
第一步std::copy将last~end()范围内的元素复制到first.然后 _M_finish 指向std::copy()的返回值
push_back()
emplace_back()
底层调用push_back()
pop_back()
将finish指向上一个元素
resize()
swap()
分别有三个不同版本
1.2 分析普通版本vector
源码在 gcc-11.4.0\libstdc++-v3\include\bits\stl_vector.h中
1.2.1 reference(引用)类型
上面这个_Alloc_traits 使用了模板参数_Tp_alloc_type, _Tp_alloc_type的具体细节如下
_Alloc_traits又是 __gnu_cxx::__alloc_traits 的别名,这个定义在include/ext/alloc_traits.h文件中
1.2.2 iterator(迭代器)类型
__gnu_cxx::__normal_iterator定义在 include/bits/stl_iteartor.h 文件中.
该模板接收两个模板参数: 迭代器和容器
请注意: 这里传入了 pointer 和 vector 类型给__normal_iterator
__normal_iterator具体细节如下:
该类定义了一个迭代器成员,类型是用户传递的迭代器类型.由于上面传递的是pointer类型,所以底层操作都是指针.
1.2.3 vecotr类剖析
关系图如下:
实现的细节和vector<bool>差不多.也都有 _M_start、_M_finish、_M_end_of_storage三个成员.
只不过分配元素的时候不再以unsigned long为单位,而是以用户传递的元素类型为单位分配内存.
上面三个成员都是以指针pointer形式,不再是以iterator类型操作.
关系图了解了,接下来还是老样子,分析每个成员函数来查看底层是如何实现的.
1.2.4 源码解析(源码中注释写的非常详细了)
各个原理大致和vector<bool>差不多,下面介绍有点区别的函数.
1.2.4.1 reserve
1.2.4.2 emplace
注意一下,函数的第二个参数是_Args&&...
这是一个右值引用可变参数,并不是容器元素的类型.然后底层调用 _M_emplace_aux函数,并将参数包__args转发给这个函数.
也就是,传递给这个函数并不需要元素类型的对象,元素值也可以.
这也是push_back和emplace_back的区别,后面再说
下面是_M_emplace_aux细节
1.2.4.3 emplace_back和push_back的区别
push_back实现细节(一共有两个版本):
emplace_back实现细节:
对比一下两个函数:
- 主要区别就是两个函数的参数类型不一样.其余实现细节完全一样.而且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() 函数,内部是如何分配内存大小的