之前一直在看侯捷大佬写的《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: 在position位置插入元素
- 版本2: 在position位置插入first~last迭代器范围内的元素
常量迭代器版本
非常量迭代器版本
- 版本3: 在position位置插入个数n,插入数据是参数x
emplace()
底层调用insert函数,在position位置插入元素
erase()