C++ STL std::list探索
How std::list works.
Overview
本文主要是侯捷《STL与泛型编程》课程关于std::list容器的学习笔记,此外,在课程的基础上做了一些简单的验证和实验,加深了对std::list对象内存布局的理解。本文主要包含以下内容:
std::list的变化:从GCC 2.9到GCC 4.9到GCC 13.0std::list的迭代器部分成员函数的实现
一个简单的测试实验
std::list in GCC 2.9

GCC 2.9中std::list实现比较简单,只有一个Delegation关系,list中包含一个__list_node类型的指针对象。
可以看到这个实现比较粗糙:
__list_node中的指针对象是void*类型,意味着在进行操作的时候会发生类型转换;迭代器的定义传递的参数过多,所有需要的类型都通过template传递;
std::list in GCC 4.9

GCC 4.9中std::list的设计变得更加符合C++的风格,充分运用了C++的特性,加入继承和多态,使用Composition关系组织各个类。

_List_node
_List_iterator
总的来说,迭代器是一个_List_node_base型的指针。每种Containers(除vector)都含有一个iterator成员,实现了一系列的操作符重载,用于模拟指针操作。
operator++()和operator++(int)
std::list重载了两个++操作符,这是因为对于++这类单目操作符,可以是++ite或ite++,即有prefix form和postfix form两种形式。
operator++()
这是prefix form,即++ite。我们知道前置自增意味着我们需要先自增在返回,因此prefix form的重载比较简单,直接让当前指向下一个节点再返回引用即可。
operator++(int)
这是postfix form,即ite++。我们知道后置自增意味着我们需要返回当前值再进行自增操作,它的实现分为三步:
_Self __tmp = *this;:调用拷贝构造创建一个临时变量来记录原值,注意这个*this并不会调用operator*()来完成对ite的“解引用”(iterator只是在模拟指针,因此叫解引用并不严谨)。_M_node = _M_node->_M_next;:当前节点指向下一个节点。return __tmp;:返回记录下来的原值,注意这里是return by value而不是prefix form中的return by reference。
operator*()
因为_M_node是_List_node_base型的指针,当我们想要访问_M_node->_M_data的时候,我们需要进行强制类型转换static_cast<_List_node<_Tp>>(_M_node)->_M_valptr()。这个设计尚未明晰设计原因。
operator->()
成员函数
push_back()
这里的_M_hook函数是在_List_node_base中声明的:
如果画图分析一下(这里就不画图了),就可以发现_M_hook的作用是将this挂(hook)到position之前:
_M_hook之前:
尾<-->C<-->B<-->A<-->头_M_hook之后:(position指向
end()尾节点)尾<-->新节点<-->C<-->B<-->A<-->头
size()
可以看到size()的复杂度是O(1)的,直接从_M_node中取_M_size就好。
begin()/end()/empty()
_M_impl包含的是一个结点对象,而不是一个指针,由于哨兵结点指向尾结点,在非空情况下尾节点又指向头节点,所以这几个函数的实现比较简单。
内存布局实验
上述代码分别输出了一个std::list对象l的地址,以及begin()和end()迭代器地址和大小:

可以看到l对象的内存地址是0x016fdff3e0,begin()迭代器指向的地址是0x016fdff3b0,end()迭代器指针指向的地址是0x016fdff3b8。需要注意的是,l的大小是24字节,而不是前文提到的8字节,在GCC 4.9版本中一个std::list对象仅包含一个_List_base_node类型的对象作为哨兵节点,其中仅有两个指针_M_prev和_M_next,侯捷所用的环境是32位OS,所以一个指针4字节,两个指针8字节。而我所用的编译环境是Clang 13.0.0,据资料至少在GCC 5.4之时就修改了哨兵节点的定义:
定义了一个_List_node<size_t>类型的对象,也就是说哨兵节点除了_M_prev和_M_next两个指针以外,还有一个size_t类型的_M_data对象。
而到了目前我用的Clang 13.0.0(GCC 13),_List_impl所包含的对象为新定义的专门用于存储std::list哨兵节点的__detail::_List_node_header类型,而在其中直接定义了一个std::size_t类型的_M_size对象:

在64位机器上,指针和size_t类型都占8字节,因此std::list对象的大小为8*2+8 bytes,也就是上面输出的24字节。
下面使用lldb具体查看对象的内存布局:

可以看出迭代器对象其实会分配自己的内存空间,并指向对应的
std::list节点;end()迭代器对象指向std::list对象地址,也即_M_mode节点的地址;end()迭代器对象的_M_next和begin()迭代器指向同一地址,即std::list对象的头节点;对于一个长度为3的
std::list<int>对象,它的占用的内存大小应该是8*2+4 bytes,这里为了对齐所以直接打印了24bytes的内容。
对于上述代码中的这个长度为3的std::list<int>对象可以画出如下的结构图:

参考资料
侯捷,《STL与泛型编程》
侯捷,《STL源码剖析》
gcc,stl_list.h
GetLeft,C++ STL std::list部分实现
Last updated
Was this helpful?