首先是一些基本的数据结构原则,然后是关于分配器的注释和一些链接......
STL 容器使用许多不同的数据结构。 map、set、multimap 和 multiset 通常被实现为具有红黑平衡规则的二叉树,例如,deque 可能(比知识更多)是数组中的循环队列,利用数组加倍或类似的增长模式.
标准实际上并未定义任何数据结构 - 但指定的性能特征显着限制了选择。
通常,您包含的数据直接包含在数据结构节点中,这些节点(默认情况下)保存在堆分配的内存中。您可以通过在指定容器时提供分配器模板参数来覆盖节点的内存源 - 稍后会详细介绍。如果您需要容器节点来引用(不包含)您的项目,请将指针或智能指针类型指定为包含类型。
例如,在 std::set 中,节点将是二叉树节点,其中包含用于 int 和两个子指针的空间,以及库所需的元数据(例如红/黑标志)。二叉树节点不会在您的应用程序地址空间中移动,因此您可以根据需要将指向数据项的指针存储在其他地方,但并非所有容器都如此 - 例如向量中的插入将插入点上方的所有项目向上移动 1,并且可能必须重新分配整个向量,移动 所有 个项目。
容器类实例通常非常小——通常只有几个指针。例如,std::set 等通常有一个根指针、一个指向最低键节点的指针和一个指向最高键节点的指针,可能还有更多元数据。
STL 面临的一个问题是在多项目节点中创建和销毁实例而不创建/销毁节点。例如,这发生在 std::vector 和 std::deque 中。严格来说,我不知道 STL 是如何做到的 - 但显而易见的方法需要放置新的和显式的析构函数调用。
Placement new 允许您在已分配的内存中创建对象。它基本上为您调用构造函数。它可以带参数,所以它可以调用复制构造函数或其他构造函数,而不仅仅是默认构造函数。
要析构,您可以通过(正确键入的)指针显式调用析构函数。
((mytype*) (void*) x)->~mytype ();
如果您没有声明显式构造函数,甚至对于不需要破坏的内置类型(如“int”),这也有效。
同样,要将一个构造实例分配给另一个实例,您需要显式调用 operator=。
基本上,容器能够相当容易地在现有节点中创建、复制和销毁数据,并且在需要时,元数据会跟踪节点中当前构建的项目 - 例如。 size() 指示当前在 std::vector 中构造了哪些项目 - 可能还有其他未构造的项目,具体取决于当前的 capacity()。
编辑 - STL 可以通过(直接或有效地)使用 std::swap 而不是 operator= 来移动数据来优化。这在数据项是(例如)其他 STL 容器的情况下会很好,因此拥有大量引用的数据 - 交换可以避免大量复制。我不知道标准是否要求这样做,或者允许但不强制要求。但是,有一种众所周知的机制可以使用“特征”模板来做这种事情。默认的“特征”可以提供使用赋值的方法,而特定的覆盖可以通过使用交换方法来支持特殊情况类型。抽象将是一种移动,您不关心源中剩余的内容(原始数据、来自目标的数据等),只要它有效且可破坏。
当然,在二叉树节点中,不需要这样做,因为每个节点只有一个项目,并且总是被构造的。
剩下的问题是如何在节点结构中保留正确对齐和大小正确的空间以保存未知类型(指定为模板参数),而不会在创建/销毁节点时调用不必要的构造函数/析构函数。这在 C++0x 中会变得更容易,因为联合将能够保存非 POD 类型,从而提供方便的未初始化空间类型。在那之前,有一系列技巧或多或少地适用于不同程度的可移植性,毫无疑问,一个好的 STL 实现是一个值得学习的好例子。
就我个人而言,我的容器使用空格换类型模板类。它使用特定于编译器的分配检查来确定编译时的对齐方式,并使用一些模板技巧从正确大小的字符数组、短数组、长数组等中进行选择。使用“#if defined”等选择了不可移植的对齐检查技巧,当有人向它抛出 128 位对齐要求时,模板将失败(在编译时),因为我还不允许这样做。
如何实际分配节点?好吧,大多数(全部?)STL 容器都采用“分配器”参数,默认为“分配器”。该标准实现从堆中获取内存并将其释放到堆中。实现正确的接口,它可以用自定义分配器替换。
这样做是我不喜欢做的事情,当然我的办公桌上没有 Stroustrups “C++ 编程语言”。您的分配器类需要满足很多要求,至少在过去(情况可能有所改善),编译器错误消息没有有帮助。
Google 说你可以看这里……