初步理解c++的类以及常用STL的内部结构
C++ class
在平时的c语言课程中,我们几乎只用到了struct的成员变量,后来我们才知道struct
其实和class
差不多,也可以有自己的成员函数
,构造函数
,析构函数
,可以封装
,多态
,继承
,唯一的区别就是struct
的访问控制默认是public
,而class
是private
,现在我们来了解一下c++的class
1 |
|
output
1 | foo ctor |
member function(成员函数)
1 | // foo: |
顾名思义,就是struct
或者class
内部的成员,写在类内部的函数。根据访问控制tag不同,决定能否在外部调用,就像foo
private下的变量是无法在外部函数通过foo.a
来调用的
ctor & dtor(构造 & 析构)
1 | // foo: |
形如foo()
和foo(int x, int y)
这种没有返回类型的与类同名的函数就是构造函数
,通常在类对象生成时自动调用;
形如~bar()
和~foo()
这种没有返回类型,与类同名并且在前面加上~
的函数,就是析构函数
,通常在类对象生命周期结束时自动调用;
这两类函数,如果没有编写,编译器会自动生成默认构造函数和析构函数
inheritance(继承)
1 | class bar : public foo |
bar
继承自foo
,是foo
的派生类,这个public
是访问修饰符access-specifier
当一个类派生自基类,该基类可以被继承为 public、protected 或 private 几种类型。继承类型是通过上面讲解的访问修饰符 access-specifier 来指定的。
我们几乎不使用 protected 或 private 继承,通常使用 public 继承。当使用不同类型的继承时,遵循以下几个规则:
- 公有继承(public):当一个类派生自公有基类时,基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员,基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有和保护成员来访问。
- 保护继承(protected): 当一个类派生自保护基类时,基类的公有和保护成员将成为派生类的保护成员。
- 私有继承(private):当一个类派生自私有基类时,基类的公有和保护成员将成为派生类的私有成员。
从输出结果可以看出,bar
初始化的时候调用了foo
的默认构造函数default ctor
,然后调用了bar
的ctor
,而且在最后变量销毁析构时,先调用了bar
的dtor
,然后调用了foo
的dtor
。
这说明子类也会继承父类的私有变量,并且先构造父类的变量,然后构造子类的变量,最后先析构子类的变量,最后析构父类的变量,这刚好是相反的顺序。
Polymorphism(多态)
我们可以发现输出结果都输出了foo
的成员函数, 这应该是错误的输出,因为编译器在调用函数前把这个成员函数设置为基类
的版本。
但当我们在foo
的成员函数前加上virtual
tag,就能正常输出
1 | foo member function |
此时foo
的成员函数声明为虚函数,编译器在调用函数时,根据调用的对象来动态确定这个函数的版本,这就是C++的多态
class internal
现在我们通过动态调试来看看class的内部情况
通过ida可以发现
在构造对象时将vtable
放在结构体的首地址
bar
确实继承了foo
类并且调用了foo default ctor
这两处call eax/edx
是foo
和bar
的member
函数调用
首先取类地址,存放到对应的指针变量处,然后取出vtable
的值,最后取出vtable
的第一个函数地址,调用该函数
虚函数表是存放在.rdata
段
这两处call eax
是foo
和bar
的fun
函数调用,仅仅只是在最后取函数地址时加了偏移
STL internal in windows
(本文仅讨论部分STL的内部结构和内存管理)
vector
memory management
1 |
|
output
1 | size: 0, capacity: 0 |
可以发现在windows平台下的vector
与linux的vector
的内存策略不相同,在linux下,若vector
的内存池满了,会直接申请两倍的内存池,并将原来的内容copy过去(若原来的内存池大小为0,会变成1)
push_back
1 | _CONSTEXPR20 void push_back(const _Ty& _Val) { // insert element at end, provide strong guarantee |
push_back
会根据_val
的类型调用对应的**_Emplace_one_at_back
**
1 | template <class... _Valty> |
当内存池满了,调用
_Emplace_reallocate(_Mylast, _STD forward<_Valty>(_Val)...)
1 | // ... |
_Oldsize
就是原来内存池的大小,然后_Newsize
为_Oldsize + 1
后调用
_Calculate_growth(_Newsize)
1 | _CONSTEXPR20 size_type _Calculate_growth(const size_type _Newsize) const { |
看到这里我们就清楚了vector
的内存管理,新内存池大小就是max(_Oldsize + 1, _Oldsize + _Oldsize / 2),这确实与上文的输出结果相匹配
internal
1 | using _Alty = _Rebind_alloc_t<_Alloc, _Ty>; |
vector
就一个成员变量_Mypair
,是_Compressed_pair
模板类
1 | template <class _Ty1, class _Ty2, bool = is_empty_v<_Ty1> && !is_final_v<_Ty1>> |
可以看到_Compressed_pair
派生于第一个模板参数_Alty
,其实也就是_Compressed_pair
就是一个内存配置器,通过_Get_first()
可以得到内存配置器,内含第二个模板参数对象_Scary_val
,命名为_Myval2
1 | template <class _Val_types> |
可以看到_Vector_val
派生于_Container_base
,有三个指针对象_Myfirst, _Mylast, _Myend
,这与linux的STL如出一辙。_Container_base
没啥重要的东西,直接略过,这三个指针才是vector
内存维护的关键
string
在本文中,我只介绍std::string
, 使用 char
类型的元素将 basic_string
类模板的专用化描述为 string
的类型
memory management
1 |
|
output
1 | 114 514 |
默认构造
1 | _CONSTEXPR20 |
_Tidy_init()
1 | _CONSTEXPR20 void _Tidy_init() noexcept { |
根据上面的输出结果,并且多次运行,我们可以发现_BUF_SIZE
是固定值为0x10
,capacity
也就是_Myres
的值
用字符串构造,会自动计算字符串长度
1 | _CONSTEXPR20 basic_string(_In_z_ const _Elem* const _Ptr) : _Mypair(_Zero_then_variadic_args_t{}) { |
然后我们看下这个_Construct
函数
1 | template <_Construct_strategy _Strat, class _Char_or_ptr> |
从源码中我们可以得出
- 当字符串的长度
_Count
<_BUF_SIZE
时,会直接记录到_Mysize(_Count)
和_Myres(_BUF_SIZE - 1)
,也就是size
和capacity
的值,然后copy后return - 否则,会先记录
_Myres
为_BUF_SIZE - 1
调用_Calculate_growth(_Count)
计算新的capacity
,然后记录到_Mysize(_Count)
和_Myres(_New_capacity)
,copy后return
_Calculate_growth(_Count)
1 | _NODISCARD _CONSTEXPR20 size_type _Calculate_growth(const size_type _Requested) const noexcept { |
1 | _NODISCARD static _CONSTEXPR20 size_type _Calculate_growth( |
上文中的_BUF_SIZE
和_ALLOC_MASK
实际上都来源于_String_val
类
1 | static constexpr size_type _BUF_SIZE = 16 / sizeof(value_type) < 1 ? 1 : 16 / sizeof(value_type); |
由于这里的value_type
是char
,所以_BUF_SIZE
是0x10,_ALLOC_MASK
是0xF
看到这我们就清楚了,当_Count
>_BUF_SIZE - 1
时,_New_capacity
的值为max(_Count | 0xF, _Old_capacity + _Old_capacity >> 1)
internal
1 | using _Alty = _Rebind_alloc_t<_Alloc, _Elem>; |
可以看到string
也就只有_Compressed_pair
这一个类成员
_Compressed_pair
1 | template <class _Ty1, class _Ty2, bool = is_empty_v<_Ty1> && !is_final_v<_Ty1>> |
和vector
一样,_Compressed_pair
派生于第一个模板参数_Alty
,内含第二个模板参数对象_Scary_val
等同于_String_val
类,命名为_Myval2
_String_val
1 | template <class _Val_types> |
所以,实际上string
就三个成员变量_Bx
、_Mysize
、_Myres
,其中_Bx
是union
联合体_Bxty
,union
的长度按union
成员中最长的计算,其实也就是0x10
的大小
- 当
_Myres
<_BUF_SIZE
,_Bx
通过copy
或assign
直接保存字符串内容 - 当
_Myres
>=_BUF_SIZE
,_Bx
保存的是预留内存的指针,这个可以从上文的_Construct
函数中得出
1 | // _Count >= _BUF_SIZE |
先计算_New_capacity
也就是新的预留空间大小_Myres
,然后开辟_New_capacity + 1
大小的空间,并将指针保存到_Bx
,并将字符串的内容copy
或assign
到开辟的内存
deque
internal
deque
与vector
、string
不一样,不提供capacity
接口,而且deque的内部数据结构更为复杂,我们直接从ida开始看
一共有5个成员变量,我们再配合源码分析每个成员的用途
1 | // class deque |
deque
同样也只有一个成员变量_Compressed_pair
,_Scary_val
等同于_Deque_val
类
_Deque_val
1 | template <class _Val_types> |
从_Deque_val
的源码中,我们可以清楚的知道后四个成员变量的作用
1 | _Mapptr _Map; // pointer to array of pointers to blocks |
再跟进一下_Container_base12
类
1 | struct _Container_base12; |
从这里我们可以_Myproxy
是_Container_base12
类的成员变量,是_Container_proxy
的指针类型,_Container_proxy
有两个变量:_Mycont
、_Myfirstiter
,一个是container
的指针,一个是iterator
的指针,至此,全部成员变量的作用就清晰明了了。
memory management
知道了deque
成员变量的基本作用,我们可以开始通过常见的成员函数来了解deque
的内存管理
push_back
1 | void push_back(_Ty&& _Val) { |
_Emplace_back_internal
1 | template <class... _Tys> |
_Block_size
在_Deque_val
中定义,总是2的阶乘,默认构造初始化,_Map
、_Mapsize
、_Myoff
、_Mysize
都为0,所以会调用_Growmap(1)
_Growmap
1 | void _Growmap(size_type _Count) { // grow map by at least _Count pointers, _Mapsize() a power of 2 |
当_Mapsize
的值为0,_Newsize
取1,否则,_Newsize
取_Mapsize
的值。当_Newsize
的值小于_Minimum_map_size
也就是8的时候,并且_Count
为1,所以_Newsize
最后会取到8,_Count
也会更新到8,然后开辟_Mapsize + _Count
大小的_Newmap
,然后copy原_Map
上的内容到新_Newmap
上,并通过_Uninitialized_value_construct_n_unchecked1
根据value_type
将其他新开辟的区域进行初始化,最后析构并释放原_Map
,并更新_Map
和_Mapsize
_Uninitialized_value_construct_n_unchecked1
1 | template <class _NoThrowFwdIt, class _Diff> |
根据value_type
进行零初始化或者默认构造,我们再回到_Emplace_back_internal
1 | // ... |
_Myoff
获得当前所在的元素
的位置,_Newoff
获得下一个元素
的位置,通过_Getblock(_Newoff)
获得下一个元素_Block
的位置,若_Map[_Block]
此处未申请_Block
,申请个_Block
,并构造一个value
,最后++_Mysize
Push_front
1 | void push_front(const _Ty& _Val) { |
emplace_front
1 | template <class... _Valty> |
_Emplace_front_internal
1 | template <class... _Tys> |
_Emplace_front_internal
实际上和_Emplace_back_internal
差不多,但是当_Myoff
值为0时,_Newoff
就会赋值为最后一个block
的最后一个位置。再push_front
就相当于从后往前放置元素