request-free-img

C++ STL 内存分配器详解:以 std::string 为例

引言:STL 中的内存分配器(allocator)

网友说,能讲讲stl的内存分配器

好的,这一期我们就来讲一个C++标准库中的内存分配器allocator

的内存分配器(allocator)是C++标准库中用于分配和释放内存的抽象接口。它主要负责为容器(例如 std::vector、std::string、std::map 等)提供底层的内存管理支持

它的核心作用是为容器动态分配内存,并确保内存的正确管理和高效使用,标准库中的默认分配器是 std::allocator,它封装了底层的内存分配操作(如 new 和 delete)

简单示例:std::string 的内存分配行为

#include <iostream>
#include <string>

int main() { // 使用 SSO std::string s = “Hello”; std::cout << “Size: ” << s.size() << “, Capacity: ” << s.capacity() << std::endl; // 触发动态分配 s += ” World! This is a long string”; std::cout << “Size: ” << s.size() << “, Capacity: ” << s.capacity() << std::endl; return 0; }

在这个示例,咱们定义了一个std string的变量s,并初始化为字符串 hello

然后使用+=运算符将” World! This is a long string” 追加到变量 s 中。

std::string 的底层实现

在 MSVC 2019 中

using string = basic_string<char, char_traits<char>, allocator<char>>;

std::string 是 std::basic_string的别名,它被定义在 <string> 头文件中

其中 std::allocator<char> 是默认的内存分配器

basic_string 的核心结构

我们首先大概来看一下std string的实现

STL的代码向来都是天书般的存在,不管你是多少年的C++程序员,只要翻看STL的代码,你都会有一种自己是C++菜鸟的感觉

言归正传

我已经为你剥开层层代码,找到stl中关于basic_string的定义

在这个类中

_Compressed_pair<_Alty, _Scary_val> _Mypair;

_Mypair 是一个 _Compressed_pair对象,它用于存储分配器和 _String value(字符串数据)。 它通过压缩对技术(EBO),分配器通常不会占用额外空间

这个_Scary_val实际上只是_String_val的别名

Small String Optimization (SSO) 小字符串优化

_String_val包括:一个联合体_Bx,它的主要作用是SSO 或动态分配

其中_Buf是一个固定大的缓冲区,用于SSO

SSO 的全称是 Small String Optimization(也就是小字符串优化)

是 C++ 标准库中 std::string的一种实现技术。SSO 的核心思想是利用 std::string 对象内部的固定大小缓冲区来存储短字符串,从而避免动态内存分配和释放的开销。

我们接着看,_BUF_SIZE的大小是16个字节(对于 char类型来说,它可存储 15 个字符 + 一个终止符 \0)。

也就是说当你存储存15个以内的字符串的时候,它是使用这个固定的BUFFER的,这样就能提升效率

因为我们实际开发中,大多数常景都是短字符串的操作

_Mysize 是字符串的实际长度

_Myres:用于记录分配的容量。

示例代码执行分析

回过头来看咱们的代码,这一行

std::string s = “Hello”;

因为它的长度小于16,所以它会将hello存储到SSO缓冲区中

而下边这一行代码,这是一个追加操作,可能触发内存重新分配

s += ” World! This is a long string”;

我们一起来看代码,string类重载了+=运算符,实际上它内部调用了append函数来追加字符串

在append函数中,如果要添加的字符串长度仍然小于SSO缓冲串的剩余空间,那么直接将它复制到SSO缓冲区

否则就调用_Reallocate_grow_by函数重新分配缓冲区

重新分配完成后,先把之前存储的旧字符串复制到新分配的缓冲区中

再把追加的字符串复制到新分配的缓冲区中

这样就完成了字符串的追回过程

std::allocator 的实现原理

明白了std string对字符串的处理过程,对于辅助我们理解std::allocator是有帮助的

std::allocator 是标准库提供的一个模板类,定义在 <xmemory> 头文件中。它是 C++ 标准分配器的默认实现

allocator分配器主要实现了allocate函数用于分配内存,对于allocate分配函数,最终它还是调用的operator new去申请内存

deallocate用于释放内存, 而在deallocate中,最终也只是调用了operator delete去释放所分配的内存

而当一个allocator被用在具体的STL中的数据结构中时,例如string中

当字符串长度在SSO缓冲区长度内,则存储于SSO缓冲区中,如果超过SSO缓冲区长度,allocator会负责分配堆内存。

而当字符串销毁或重新分配时,allocator会释放堆内存

allocator_traits 与自定义分配器

此外,在STL中,通过 std::allocator_traits 封装

std::basic_string 并不会直接调用 std::allocator 的方法,而是通过 std::allocator_traits 提供一致的接口

在使用 C++ 标准模板库(STL)时,通常情况下不需要自己实现 std::allocator,因为默认的 std::allocator(基于 new 和 delete)已经足够高效且通用,能够满足大多数应用场景的需求

但是如果在高性能计算、实时系统或需要频繁分配/释放内存的场景中,那么用户则可以实现一个自定义的分配器,例如内存池分配器来提升性能

总结

关于stl的内存分配器,现在你明白了没?


更多问题探讨,请关注公众号:程序员角