1. string
1.1 存储管理
- 实际数据空间申请由
_M_dataplus
负责管理,其中指针_M_p
指向存储位置。 - 当数据量较小时,数据存储在一个
union
的_M_local_data
数组中(char数组长度小于等于15,不包含’\0’标识符),并使用该结构体中的指针初始化_M_dataplus
。
数据量较大时,union
结构体中保存的是分配空间的容量
1.2 构造函数
-
初始化时,都是先用
_M_local_data
初始化,构造函数底层大多调用_M_construct
。
小于等于15,则按照15为容量。如果需要的空间大于15,则调用分配器进行分配;_M_limit
是限制最大值;
堆区分配规则是:分配空间时,通过_M_create
申请的空间大小为max(2*old_capacity, __capacity) + 1
,设置新的指针
最后,调用assign
进行赋值
注意细节:调用_S_copy_chars
拷贝时,不论是栈空间还是堆空间都并没有将’\0’标识符拷贝进来。但申请空间时也都额外多申请了一个,这个额外空间就是为了标识end
。(栈区的大小为16,实际存储字符的为15,最后一个保留) -
移动拷贝构造时,首先将原
str
的分配器移动拷贝过来
如果小于等于15,需要调用copy
进行复制
否则,只需要把原分配器的指针复制一份即可。 -
列表初始化
initializer_list
,即使用花括号 {} 或圆括号 () 来提供初始化的值。 -
析构函数中调用
_M_dispose
,若局部存储足够则什么都不做,否则,释放申请的空间 -
push_back
或append
(字符串拼接),如果超出当前容量,会调用_M_mutate
重新分配大小(第3条的分配规则)。
1.3 常用函数
substr
调用构造函数实现
2. sort算法的实现
其参数为两个随机访问迭代器begin
和end
。
比较函数以greater
为例,其返回值为a > b
;
-
首先计算下,
2×log(end - begin)
确定最大的迭代深度,这里的乘2的含义是:假设是理想情况下,每次都能选取到区间中位数的情况,这样每一边都是log(end - begin)
,递归调用时也是每次只调用一边。 -
__introsort_loop
函数中
(1)如果当前元素数量大于16,才会进行后续流程。否则,停止。
(2)__unguarded_partition_pivot
中选取的是第二个元素(__first + 1
)、中间元素(mid
),最后一个元素(__last - 1
),这三个值的中间值作为枢轴,并移动到第一个元素位置处
(3)__unguarded_partition
执行的就是按照选取的枢轴,对元素进行排序。具体而言,是在左侧找比枢轴大的元素,右侧找比枢轴小的元素,进行交换。不断重复这个过程即可。左侧为>=,右侧为<
(4)如果达到最大迭代步长,调用堆排序。 -
最后执行插入排序
3. pair实现
pair是一个struct,拥有两个成员变量first和second
4. initializer_list
可认为是一个只读视图,里面只存储迭代器,无法进行修改
initializer_list
拷贝构造是浅拷贝,底层的对象只有一份
initializer_list<string> s1 = { "aa", "bb", "cc", "dd" };
initializer_list<string> s2 = s1;
initializer_list<string> s3(s1);
initializer_list<string> s4;
s4 = s1;
cout << &(*s4.begin()) << endl; // 0x7ffe0e382fa0
cout << &(*s2.begin()) << endl; // 0x7ffe0e382fa0
5.deque的实现
-
实质上是指针数组,数组中的每个元素指向一块存储空间,
__deque_buf_size
表明每个空间存储几个对象
该指针数组的初始大小为max(_S_initial_map_size, __num_nodes + 2)
,至少预留了两个空间 -
_Deque_impl
中_M_map
指向指针数组的起始位置;_M_start
、_M_finish
为迭代器,指向第一个buffer和最后一个buffer;
其中_Deque_iterator
迭代器中包含,_M_cur
、_M_first
、_M_last
和_M_node
,其中前三个为T*
,后一个为T**
-
push_front
函数中
(1)先判断_M_cur != _M_first
,若满足,则说明此buffer中还有空间,向该buffer中添加元素,调整_M_cur
,直接结束。
(2)若不满足,需要在其前面添加buffer。判断前面是否有剩余空间,执行第4步;若没有,则重新分配;执行第3步。
(3)重新分配时,判断当前数组大小是否大于新结点数量的2倍,若满足,说明有空间,但当前位置太靠前,则只需要进行移位即可。若不满足,重新分配指针数组的大小。
(4)重新申请一个buffer,将_M_cur
设置为其最后一个元素的位置。 -
pop_front
函数中逻辑相对简单,析构对象,如果满足_M_cur == _M_last - 1
,则将该buffer释放,修改_M_start
即可。如不满足,调整_M_cur
即可。 -
insert
函数,先判断是否是插入最前面或最后面的情况,若是,则调用相应的push_front/back
方法
否则,是插入在中间位置。需要判断下哪边的元素数量较小,则移动哪边,调用带三个参数的move
方法。移动之前为了确保有空间,先执行一个push_front/back
方法。
迭代器失效情况总结
- 从上述过程可知,向头尾插入数据,因为可能会改变指针数组或改变指针指向,因此迭代器可能会失效,各buffer并未改变,因此对象的引用和指针仍然有效。
- 向中间插入数据时,迭代器、引用、指针都可能失效,因为涉及到buffer中对象的移动(理论上来说只会改变相对插入位置元素数量较小的一边的引用和指针,但在外部不好判断是哪一边)。
6.现代C++
std::move
和 std::forward
只是执行类型转换(static_cast)的模板函数
对于const变量,调用move()转换的是const右值引用,仍然是采用的拷贝构造而非移动构造