Skip to content

Latest commit

 

History

History
283 lines (226 loc) · 13.2 KB

2.STL源码剖析--空间配置器(allocator).md

File metadata and controls

283 lines (226 loc) · 13.2 KB

第二讲 空间分配器(allocator)

加油站1:关于new和delete

知识点补充站

参考刘元笔记:https://github.com/LiuYuan-SHU/MyNotes/blob/master/C%2B%2B/C%2B%2B%E6%96%B0%E7%BB%8F%E5%85%B8/%E8%AF%AD%E8%A8%80%E7%89%B9%E6%80%A7/18_%E5%86%85%E5%AD%98%E9%AB%98%E7%BA%A7%E8%AF%9D%E9%A2%98.md

通常情况下,C++内存分配和释放的操作如下:

class Foo{···};
Foo *pf = new Foo;
delete pf;

其中操作符new/deleteoperator new/operator delete有什么关系呢?

  1. new 做了两件事情:
  • 调用operator new分配内存;
  • 调用构造函数构造对象;
  1. delete做了两件事情:
  • 调用析构函数析构对象;
  • 调用operator delete释放内存;

这里需要我们注意的是operator new和operator delete是两个可以调用的函数。

加油站2:关于set_new_handler函数

知识点补充站

参考博客:https://blog.csdn.net/wzxq123/article/details/51502356

关于set_new_handler函数的使用方法,***该函数的主要作用就是当我们new操作或者new []操作失败的时候调用的处理函数。***设置的处理函数可以尝试使得更多空间变为可分配的状态,这样的话新一次的new操作就可能成功。

当我们没有使用该函数去设置处理函数的时候,或者设置的处理函数为空的时候,其将会调用默认的处理函数,该函数在内存分配失败的时候抛出bad_alloc异常

这里注意,我们下面的写法都是设置处理函数为空:

std::set_new_handler(0);
std::set_new_handler(nullptr);

源码中关于该函数是这样定义的:

// defined in header <new>

typedef void(*new_handler)();
// 1. 即将将函数指针起了一个别名, 叫做new_handler
// 这样的话会有利于我们去理解相关的函数

// 2. 下面是set_new_handler函数的声明,分别是两种不同的声明 
new_handler set_new_handler(new_handler new_p) throw(); // C++98
new_handler set_new_handler(new_handler new_p) noexcept;// C++11

// 3. 关于throw和noexcept,其实就是声明函数不会抛出任何异常,即使我们函数在执行的过程中有抛出异常,但是我们不会去捕捉,仅仅是终止程序的执行

// 4. 如果说我们设置的处理函数为空的话,该函数就会在内存分配失败的时候抛出`bad_alloc`异常

实例代码:

#include <iostream>
#include <new>
 
void handler()
{
    std::cout << "Memory allocation failed, terminating\n";
    std::set_new_handler(0);
}
 
int main()
{
    std::set_new_handler(handler);
    try {
        while (true) {
            new int[1000'000'000ul]();
        }
    } catch (const std::bad_alloc& e) {
        std::cout << e.what() << '\n';
    }
}

上面的代码中,我们第一次分配内存失败的时候回去执行handler函数,即我们自定义的函数,在执行的过程中又将处理函数设置为默认的处理函数,接着程序就会捕获到内存分配失败事件,从而产生bad_alloc异常,由于该异常很特殊,所以说catch可以捕获到。

加油站3:new的三种形态

知识点补充站

参考文章:https://www.cnblogs.com/fnlingnzb-learner/p/8515183.html

  • new operator, 就是我们经常使用的new操作符;
  • operator new, 这个是我们的new操作符做的事情中的第一件,使用函数operator new来申请空间;
  • placement new, 此种就是我们上面所提到的new的第三种形态;

new的第三种形态使用来实现定位构造的,也就是说我们申请完内存之后,在获得的内存上构造一个对象,有点类似于前面代码中的p->A :: A(3), 但是这个并不是一个标准的写法,正确的写法是使用placement new

#include <new.h>

void main()
{
   char s[sizeof(A)];
   A* p = (A*)s;
   new(p) A(3); //p->A::A(3);
 // 即后面的A(3)显式的调用了构造函数来给前面指向的内存赋值
   p->Say();
}

我们一般是不使用这样的写法的,因为我们使用new操作符的时候编译器就会自动的将其编译并生成对placement new的调用的代码。当我们觉得默认的new operator对内存的管理不能满足我们的需要,而希望自己手工的管理内存时,placement new就有用了。STL中的allocator就使用了这种方式,借助placement new来实现更灵活有效的内存管理。

1. allocator必要的接口

// 这里的接口可以理解为该类中必须要有这样的实现与声明,来供我们调用
allocator::value_type;			// 变量的类型
allocator::pointer;					// 指针
allocator::const_pointer;
allocator::reference;				// 引用
allocator::const_reference;
allocator::size_type;       // 分配的大小的类型
allocator::difference_type;	// 该类型是迭代器之间的距离的类型

allocator::rebind;  // 一个嵌套的类模板,该类中只有一个成员typedef allocator<U> other

allocator::allocator(); // 默认的构造函数

allocator::allocator(const allocator&); // 拷贝构造函数

template<class U>allocator::allocator(const allocator<U> &) // 泛化的 allocator 构造函数
  
allocator::~allocator(); // 析构函数

pointer allocator::address(reference x) const;
// 返回某一个对象的地址,a.address(x) 相当于&x, 所以我们将参数设置为引用,这样才可以真正获得对象的真正地址。
const_pointer allocator::address(const_reference x) const; // 获得某一个const对象的地址

pointer allocator::allocate(size_type n, const T&x);// 该函数的作用是配置空间,足以存储n个T对象,第二个参数是一个提示,这里就相当于我们new操作符中的operator new函数

void allocator::deallocate(pointer p, size_type n);// 归还先前配置的内存空间,就相当于new操作符中的operator new函数

size_type allocator::max_size() const; // 返回成功配置的最大内存空间的量

void allocator::construct(pointer p, const T& x);// 等同于new ((void*) p) T(x),这里是new操作的第三种形态,具体查看加油站3相关知识点

void allocator::destory(pointer p); // 等同于调用p->~T(),这里是由于STL使用了placement,编译器不会自动产生调用析构函数的代码,需要我们手工的去实现

2. SGI STL的空间分配器

这里只需要我们了解一下即可,SGI STL的分配器和我们标准规范是不一样的,其名称是alloc而非是allocator,而且不接受任何参数。就是说按照SGI的标准,我们不能按照标准写法去定义一个变量:

vector<int, std::allocator<int>> iv;

我们需要按照SGI的规格来写,当然了这只是针对SGI标准下的写法,我们平时写的代码都是按照STL标准来的。

vector<int, std::alloc> iv;

你也可以去看看其是怎么定义的,我们不需要传进去任何参数: stl_alloc.h

当然了,SGI也是定义了一个符合部分标准,名为allocator的分配器,但是其并未使用,也建议我们不使用,因为其效率不佳!

defalloc.h

我们可以看他写的源码,可以看到仅仅是非常简单的申请内存,然后初始化等等。

一般而言,我们习惯的C++内存配置操作和释放是这样的:

class Foo{}
Foo * pf = new Foo;
delete pf;

其中的具体知识点请参考加油站1的相关内容。

看完上面的知识点之后,我们回头看STL allocator,也会发现STL allocator也是将new和delete的操作进行了分离:

  • 内存分配:该部分是由alloc::allocate()负责的;
  • 内存释放:该部分是由alloc::deallocate()负责的;
  • 对象构造:是由alloc::construct()负责的;
  • 对象析构:是由alloc::destroy负责的;

3. 构造和析构:construct()destroy()

相关的源码文件: stl_construct.h

// 我们接下来对这一份代码一一进行解析

// 1. 下面的代码就是一个很简单的析构函数的版本,我们接受一个指针,然后去调用相应对象的析构函数
template <class T>
inline void destroy(T* pointer) {
    pointer->~T();
}
// 2. 这里的代码是我们的构造函数,利用placememt new运算子来将初始值设定到指针所指向的空间上
// 这里需要注意一点的是,原本placement new是编译器经过翻译得到的代码,现如今我们手动写了出来,那么我们就得手动调用析构函数来析构我们的对象
template <class T1, class T2>
inline void construct(T1* p, const T2& value) {
  new (p) T1(value);
}

// 3. 下面的析构函数是我们第二个版本的析构函数,接受first和last两个迭代器,函数所要做的事情就是将[first,last)范围内的所有的对象全部析构掉,这个时候就会涉及到那些基本类型的变量,我们还要对其进行一一的调用相应的析构函数吗?他们的析构函数都无关痛痒(这也就是我们下面见到的trivial destructor,翻译过来就是琐碎的析构函数,的来由)。这样就会对我们的效率造成极大的干扰,所以说我们需要进行优化
// 这里STL利用了一种名为“type traits”的技巧,根据我们穿进去的参数的类型来调用相关的析构函数
// 这里我们可以看到__destroy的第三个参数是value_type(first),该函数的目的就是为了获得变量所对应的指针类型
template <class ForwardIterator>
inline void destroy(ForwardIterator first, ForwardIterator last) {
  __destroy(first, last, value_type(first));
}
// 4. 我们这里通过特化来实现根据我们参数类型的不同来执行不同的模板函数, 所实现的效果就是当我们传入基本变量类型的参数的时候, has_trivial_destructor就会被命名为 __true_type(是一种类,里面什么都没有)的别名, 这样的话, 就会执行重载函数版本2; 否则的话就会去执行重载函数版本1
template <class ForwardIterator, class T>
inline void __destroy(ForwardIterator first, ForwardIterator last, T*) {
  typedef typename __type_traits<T>::has_trivial_destructor trivial_destructor;
  __destroy_aux(first, last, trivial_destructor());
}

// 5. 重载函数版本1 针对那些不是trivial的析构函数,即那些不简单的参数类型
template <class ForwardIterator>
inline void
__destroy_aux(ForwardIterator first, ForwardIterator last, __false_type) {
  for ( ; first < last; ++first)
    destroy(&*first);
}

// 6. 重载版本2 针对那些是trivial的析构函数,也就是我们经常使用的参数类型int之类的
template <class ForwardIterator> 
inline void __destroy_aux(ForwardIterator, ForwardIterator, __true_type) {}

// 7. 下面的两个函数是针对另外的两种类型的特化版
inline void destroy(char*, char*) {}
inline void destroy(wchar_t*, wchar_t*) {}

实际上上面所讲述的关于STL Traits编程技法,实质上就是模板的强大的地方,我们可以利用特化或者偏特化来使得程序可以在编译的时候确定我们的程序执行的模板具体是哪一个,这里后面在第三章中会讲到。

总之这里关键的就是value_type会获得迭代器所指对象的型别,然后我们利用__type_traits来判断该型别的是否无关痛痒,即是否是trivial destructor.

type_traits.h

4. 内存配置和释放,std::alloc

此处的理解过于浅薄,先不作笔记,关于内存池的部分有点难。

先这样看,就是我们申请不一样大的内存空间的时候。STL回去调用不同的分配函数,去执行,来搭配更多的机制来使得我们分配内存的方案为最优。

5.内存基本处理工具

STL定义了5个全局函数,作用于未初始化空间上,有助于容器的实现:

  • 作用于单个对象(见3.1 对象构造与析构,SGI STL定义在头文件<stl_construct.h>中)
    • construct()函数(构造单个对象)
    • destroy()函数(析构单个对象)
  • 作用于容器的区间(本节,SGI STL定义在头文件<stl_uninitialized.h>中,是高层copy()、fill()、fill_n()的底层函数)

容器的全区间构造函数通常分2步

  1. 分配内存区块,足以包含范围内的所有元素;
  2. 调用上述3个函数在全区间范围内构造对象(因此,这3个函数使我们能够将内存的分配与对象的构造行为分离;并且3个函数都具有”commit or rollback“语意,要么所有对象都构造成功,要么一个都没有构造