STL容器

STL容器

  • STL的allocator
    • allocator用途
    • allocator原理
  • STL 技巧
  • STL的内存优化
    • 第一级配置器
    • 第二级配置器
    • 总结STL内存管理原理
  • STL组件
    • STL的适配器
      • STL的二元函数binary_function
    • STL中迭代器的作用
    • 迭代器和指针的区别
    • STL迭代器是怎么删除元素
    • 容器对应的迭代器
  • 基础知识
    • STL里resize和reserve
    • 遍历容器
    • clear()
    • STL中的assign()
  • 序列式容器
    • vector
      • vector概念
    • vector性能:
    • list
      • list概念
      • list性能
  • 关联式容器
    • set
    • map
      • 其他
    • 下标操作
    • hash_table哈希表
      • 简介
      • 实现原理
      • 解决冲突的方法
      • 哈希表的性能
      • 扩容
      • 基本操作
    • slist
    • deque
      • deque的实现
    • stack
    • queue
    • priority_queue
      • heap
      • 大根堆
        • 堆排序
    • priority_queue
    • 红黑树
  • 容器对比
    • map和set的区别、实现
    • vector和list的区别
    • map与unordered_map区别及使用
    • 平衡二叉树与二叉查找树
      • 二叉查找树
      • 平衡二叉树
      • 红黑树

STL的allocator

allocator用途

标准库中包含一个名为allocator的类,允许我们将分配和初始化分离
使用allocator通常会提供更好的性能和更灵活的内存管理能力。std::allocator是标准库容器的默认内存分配器(可以替换成你的分配器,这允许你控制标准容器分配内存的方式)。

new有一些灵活性上的局限,其中一方面表现在它将内存分配和对象构造组合在一起。类似的,delete将对象析构和内存释放组合在了一起。一般情况下,将内存分配和对象构造放在一起可能会导致不必要的浪费,同时带来性能上不必要的消耗。

标准库allocator类定义在头文件memory中,它帮助我们将内存分配和对象构造分开来。它提供一种类型感知的内存分配方法,它分配的内存是原始的,未构造的。

allocator原理

STL的分配器用于封装STL容器在内存管理上的底层细节。在C++中,其内存配置和释放如下:

  • new运算分两个阶段:(1)调用::operator new配置内存;(2)调用对象构造函数构造对象内容
  • delete运算分两个阶段:(1)调用对象析构函数;(2)调用::operator delete释放内存
    为了精密分工,STL allocator将两个阶段操作区分开来:内存配置有allocator::allocate()负责,内存释放由allocator::deallocate()负责;对象构造由::construct()负责,对象析构由::destroy()负责。
    同时为了提升内存管理的效率,减少申请小内存造成的内存碎片问题,SGI STL采用了两级配置器。
    当分配的空间大小 超过128B时,会使用第一级空间配置器。
    第一级空间配置器直接使用malloc()、realloc()、free()函数进行内存空间的分配和释放。
    当分配的空间大小 小于等于128B时,将使用第二级空间配置器。
    第二级空间配置器采用了复杂的内存池技术,通过空闲链表来管理内存。

STL 技巧

首先SGI STL将可用内存整块的分配,使之成为当前进程可用的内存,当程序中确实需要分配内存时,先从这些已请求好的大内存块中尝试去取得内存,如果失败的话再尝试整块的分配大内存。这种做法有效的避免了大量内存碎片的出现,提高了内存管理效率。

为了实现这种方式,STL使用了placement new,通过在自己管理的内存空间上使用placement new来构造对象,以达到原有new operator所具有的功能。

traits可以省略简单类型的析构函数调用的耗费。即像int这种类型是不需要析构函数的,可以不调用析构函数。

STL的内存优化

STL内存管理使用二级内存配置器,第一级配置器,第二级配置器

第一级配置器

  • 以malloc()、realloc()、free()等c函数执行实际的内存配置、释放、重新配置等操作,并且能在内存需求不被满足时调用一个指定的函数。
  • 调用的是大于128字节的空间
  • 如果分配不成功,调用句柄释放一部分内存。
  • 如果还分配不成功,抛出异常。

第二级配置器

根据情况来判定,如果配置区块 大于128字节 ,说明足够大,调用 第一级配置器
小于等于128字节 ,则采用 复杂内存池(memory pool)来管理。
在STL的二级空间配置器中还多了一些机制,避免太多小区块造成的内存碎片,小区块带来的不仅是内存碎片,配置时还有额外的负担,区块越小,额外负担所占的比例就越大。

二级内存池

  • 采用了16个空闲链表,分别管理大小为8、16、24、……128的数据块。
  • 这里用了一个联合体既可以表示下一块空闲数据块(存于空闲链表中)的地址,也可以表示已经被用户使用的数据块(不存在空闲链表中)的地址。

总结STL内存管理原理

1、使用allocator向内存池请求size大小的内存空间,如果需要请求的内存大小 大于128 B,直接使用malloc()
2、如果需要的内存大小 小于等于128 B,allocator::allocate()根据size找到最适合的自由链表
①如果链表不为空,返回第一个node,链表头改为第二个node
②如果链表为空,使用blockAlloc,请求分配内存
③如果内存池中有一个大于一个node的空间,分配尽可能多的node(但最多20个),将一个node返回,其他的node添加到链表中。
④如果内存池只有一个node的空间,直接返回给用户
⑤若连一个node都没有,再次向操作系统请求分配内存
a.分配成功,再次进行(2的②)
b.分配失败,循环各个自由链表,寻找空间
①找到空间,再次进行(2的②)
②找不到空间,抛出异常
3、用户调用allcator::deallocate()释放内存空间,如果要求释放的内存空间大于 128 B,直接调用free()
4、否则按照其大小找到适合的自由链表,将其插入。

STL组件

  • STL主要由以下几个部分组成:
    容器、迭代器、仿函数、算法、分配器、配接器。
  • 容器:STL提供的存放数据的类
    算法:处理元素的方法和操作
    迭代器:为容器提供一组公共接口
    仿函数:仿函数具有泛型编程强大的威力,是纯粹抽象概念的例证。(仿函数本质就是类重载了一个operator(),创建了一个行为类似于函数的对象。)
    配接器(适配器):实现不同类之间的数据转换
    分配器:内存管理

STL的适配器

适配器:一种用来修饰容器、仿函数或迭代器接口的东西。例如,STL 提供的 queue 和 stack,虽然看似容器,其实只能算是一种容器适配器,因为它们的底部完全借助 deque,所有操作都由底层的 deque 供应;改变仿函数(functor)接口的,称为 function adapter等。
适配器(adapter) 在 STL 组件的灵活组合运用功能上,扮演者转换器的角色。也是一种设计模式(适配器模式)。

  • 应用于容器(container adapter):STL 提供的两个容器 queue 和 stack,它们修饰 deque 的接口而形成的。
  • 应用于迭代器(iterator adapter):STL 提供了许多应用于迭代器的适配器。
  • 应用于仿函数(function adapter)。

代码举例function adapter:
 count_if(计算符合条件的个数)第三个参数是一个仿函数。minn的代码,这是有两个入参,我们希望使用minn这个仿函数作为count_if的第三个参数。所以我们需要写个配接器适配一下(将两个参数化成一个)。在STL中,提供了相关的函数 bind2nd的适配器,把第二个参数绑定让一个二元函数变成一元的。 此前定义的minn不能直接使用在count_if上,因为和count_if的pred不符合,需要继承binary_function:

template<class InputIterator, class Predicate, class Size>
void count_if(const InputIterator first, const InputIterator last, Predicate pred, Size& n)//第三个参数pred是一个仿函数 
{
   //计算容器内小于5的元素个数 
	for( ; first != last; first++)
	{
   
		if(pred(*first))//只有一个参数
			++n;
	}
}

template<class T>//在定义一个仿函数(functor)时不会使用binary_function,但是如果需要函数适配器便需要使用它实现:
struct minn : public binary_function<T, T, bool>{
   //返回bool型值的二元函数 
	bool operator()(const T&x, const T&y) const{
   return x < y;}
}; 

class Foo
{
   
public:
	bool operator()(int i)//用一个算法判断是否小于5,是的话返回1
	{
   
		return minn<int>() (i, 5); 
	}
};

int main()
{
   
	vector<int> v = {
   1, 2, 3, 4, 5, 4, 3};
	cout<<count_if(v.begin(), v.end(), Foo())<<endl;
	return 0;
}

STL的二元函数binary_function

在我们使用STL的一些算法的时候,比如find_if等,需要使用仿函数,如果仿函数有2个参数,但是算法需要一个一元的仿函数的时候,我们可以使用适配器,比如:bind1st和bind2nd来将仿函数适配成一元的操作符。这个时候,如果仿函数是我们自己实现的,而不是STL提供的less、greater等等内置好的仿函数时,我们如果要让仿函数支持适配器,那么就必须从binary_function派生出来。
补充:该结构在C++11标准中已废弃,在C++17标准中已移除该结构,可使用该结构的最高C++标准为C++14。

binary_function可以作为一个二元函数对象的基类,它只定义了参数和返回值的类型,本身并不重载()操作符,这个任务应该交由派生类去完成。
使用实例:

#include 
#include          
using namespace std;

struct TCompareNumSize : public std::binary_function<int,int, int>{
   
 
    int operator() (int num1, int num2)
    {
   
        return num1 < num2 ? num2 : num1;
    }
 
};

int main()
{
   
	TCompareNumSize oCompareSize;
	int iMaxNum = oCompareSize(1,2);
	std::cout<<"最大数是:"<<iMaxNum<<endl;
	return 0;
}

【补充】
对应的有unary_function,unary_function可以作为一个一元函数对象的基类,它只定义了参数和返回值的类型,本身并不重载()操作符,这个任务应该交由派生类去完成。

STL中迭代器的作用

Iterator(迭代器)模式又称Cursor(游标)模式,用于提供一种方法顺序访问一个聚合对象中的元素,而又不需要暴露该对象的内部表示。
或者这么说:Iterator模式是运用于聚合对象的一种模式,通过运用该模式是的我们可以在** 不知道对象内部表示的情况下,按照一定顺序(由Iterator提供的方法)访问聚合对象中的各个元素**。
由于Iterator模式以上的特性:与聚合对象耦合,在一定程度上限制了它的广泛运用。一般仅用于底层聚合支持

你可能感兴趣的:(study,review,容器,c++)