STL容器

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 <iostream>
#include <functional>         
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模式以上的特性:与聚合对象耦合,在一定程度上限制了它的广泛运用。一般仅用于底层聚合支持类,如STL的list、vector、stack等容器以及ostream_iterator等扩展iterator。

  • 迭代器产生的原因,为何有指针还需要迭代器
    Iterator类的访问方式就是把不同集合类的访问逻辑抽象出来,使得不用暴露集合内部结构而达到循环遍历的效果
  • 迭代器在遍历的时候为什么不用 <
    迭代器的实现中没有重载这个操作符,所以遍历的时候应该是 != v.end()

迭代器和指针的区别

迭代器不是指针,是类模板,表现的像指针。它只是模拟了指针的一些功能,通过重载了指针的一些操作符,->、*、++、–等。
迭代器封装了指针,是一个“可遍历STL(Standard Template Library)容器内全部或部分元素"的对象。本质是封装了原生指针,是指针概念的一种提升,提供了比指针更高级的行为。相当于一种智能指针,它可以根据不同类型的数据结构来实现不同的++、–等操作。
迭代器返回的是对象引用而不是对象的值,所以cout输出迭代器使用 *取值后的值而不能直接输出其自身。

STL迭代器是怎么删除元素

这个主要考察的是迭代器失效的问题
1、对于序列容器vector,deque来说,使用erase(iterator)后,后边的每个元素的迭代器都会失效,后边的每个元素都会往前移动一个位置,但是erase会返回下一个有效的迭代器
2、对于关联容器map、set来说,使用了erase(iterator)后,当前元素的迭代器失效,但是其结构是红黑树,删除当前元素,不会影响到下一个元素的迭代器,所以在调用erase之前,记录下一个元素的迭代器即可。
3、对于list来说,它使用了不连续分配的内存,并且它的erase方法也会返回下一个有效的iterator,因此上面两种正确的方法都可以使用。
其实都可以通过记录下一个有效位置来继续遍历

容器对应的迭代器

容器 迭代器
vector、deque 随机访问迭代器
stack、queue、priority_queue
list、(multi)set / map 双向迭代器
unordered_(multi)set/map、forward_list 前向迭代器

基础知识

STL里resize和reserve

  • resize():
    改变当前容器内含有元素的数量(size())
    eg:vector<int>v; v.resize(len);
    v v v s i z e size size变为 l e n len len,如果原来

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值