C++ Memory System Part1: new和delete

在深刻探索自定义内存系统以前,咱们须要了解一些基础的背景知识,这些知识点是咱们接下来自定义内存系统的基础。因此第一部分,让咱们来一块儿深刻了解一下C++的newdelete家族,这其中有不少使人吃惊的巧妙设计,甚至有不少高级工程师都对其细节搞不清楚。html

 

new operator and operator new

首先咱们来看一个使用new的简单语句:数组

T* i = new T;

 

这是一个new operator最简单的用法,那么该操做符到底作了些什么呢?wordpress

  • 首先,调用operator new为单个T分配内存
  • 其次,在operator new返回的地址上调用T的构造函数,建立对象

 

若是T是C++的基础类型,或者POD,或者没有构造函数的类型,则不会调用构造函数,上面的语句就只是调用最简单的operator new,定义以下:函数

void* operator new(size_t bytes);

 

编译器会使用正确的字节大小来调用operator new,即sizeof(T).工具

 

到如今为止都还比较好理解,可是关于new operator的介绍尚未结束,还有一个版本的new operator称为placement new布局

void* memoryAddress = (void*)0x100;
T* i = new (memoryAddress) T; // placement new

 

这是专门用来在特定的内存地址上构造对象的方法,也是惟一一个直接调用构造函数,而无需任何内存分配操做的方法。上面代码的new operator调用的是另外一个重载的operator new函数:ui

void* operator new(size_t bytes, void* ptr);

 

该形式的operator new并无分配任何内存,而是直接返回该指针。spa

 

placement new是一个很是强大的工具,由于利用它,咱们能够重载咱们本身的operator new,重载的惟一规则是operator new的第一个参数必须是size_t类型,编译器会自动传递该参数,并根据参数选择正确的operator new设计

看下面这个例子:指针

void* operator new(size_t bytes, const char* file, int line)
{
  // allocate bytes
}

// calls operator new(sizeof(T), __FILE__, __LINE__) to allocate memory
T* i = new (__FILE__, __LINE__) T;

 

抛开全局operator new和类operator new的区别不谈,全部placement形式的new operator均可以归结为如下形式:

// calls operator new(sizeof(T), a, b, c, d) to allocate memory
T* i = new (a, b, c, d) T;

等价于:

T* i = new (operator new(sizeof(T), a, b, c, d)) T;

 

调用operator new的魔法是由编译器作了。此外,每个重载的operator new均可以被直接调用。

咱们也能够实现任意形式的重载,若是咱们乐意,甚至可使用模板:

template<class ALLOCATOR>
void* operator new(size_t bytes, ALLOCATOR& allocator, const char* file, int line)
{
  returnallocator.Allocate(bytes);
}

 

这种形式的重载咱们在后面的自定义allocator时会遇到,使用该形式的placement new,内存分配就可使用不一样的allocator,例如:

T* i = new (allocator, __FILE__, __LINE__) T;

 

delete operator / operator delete

 

对前面new出来的实例调用delete operator时,将会首先调用对象的析构函数,而后调用operator delete删除内存。这点跟new的顺序恰好是反的。这里须要注意一点,与new不一样的是,不管咱们使用的是那种形式的new来建立实例,都会使用同一个版本的operator delete,看下面这个例子:

// calls operator new(sizeof(T), a, b, c, d)
// calls T::T()
T* i = new (a, b, c, d) T;

// calls T::~T()
// calls operator delete(void*)
delete i;

 

只有在调用operator new的过程当中发生异常时,编译器才会去调用对应版本的delete,这样才能保证在返回到调用端时,内存被正确释放。若是你并无定义匹配的delete则系统什么都不作,这就会致使内存泄漏。这也是为何每个重载的operator new操做符,都要有一个对应的operator delete。这部分知识在Effective C++第52条款中有详细的论述。

 

operator new同样,operator delete能够被直接调用,实例代码:

template<class ALLOCATOR>
voidoperator delete(void* ptr, ALLOCATOR& allocator, const char* file, int line)
{
  allocator.Free(ptr);
}

// call operator delete directly
operator delete(i, allocator, __FILE__, __LINE__);

 

这里要注意,若是你是直接调了operator delete,那么必定要记得在此以前手动调用对象的析构函数:

// call the destructor
i->~T();

// call operator delete directly
operator delete(i, allocator, __FILE__, __LINE__);

 

 

new[] / delete[]

 

到目前为止,咱们只讲解了newdelete的非数组版本,它们还有一对为数组分配内存的版本:

new[] / delete[]

 

从这里开始,才是new和delete系列最有趣的地方,也是最容易被人忽略的地方,由于在这里包含了编译器的黑魔法。C++标准只是规定了new[]delete[]应该作什么,可是没有说如何作,这如何实现就是编译器本身的事情了。

 

先来看一个简单的语句:

int* i = new int [3];

上面的代码经过调用operator new[]为3个int分配了内存空间,由于int是一个内置类型,因此没有构造函数能够调用。像operator new同样,咱们也能够重载operator new[],实现一个placement语法的版本:

// our own version of operator new[]
void* operator new[](size_t bytes, const char* file, int line);

// calls the above operator new[]
int* i = new (__FILE__, __LINE__) int [3];

 

delete[]operator delete[]的行为跟deleteoperator delete是同样的,咱们也能够直接调用operator delete[],可是必须记得手动调用析构函数。

 

可是,若是是非POD类型呢?来看一个例子:

structTest
{
  Test(void)
  {
    // do something
  }

  ~Test(void)
  {
    // do something
  }

  inta;
};

Test* i = new (__FILE__, __LINE__) Test [3];

在上面的状况下,尽管sizeof(Test) == 4,咱们分配了3个实例,可是operator new[]仍是会使用一个16字节的参数来调用,为何呢?多出的4个字节从哪里来的呢?

 

要想知道这是为何,咱们要先想一想数组应该如何被删除:

delete[] i;

删除数组,编译器须要知道到底要删除多少个Test实例,不然的话它没办法挨个调用这些实例的析构函数,因此,为了获得这个数据,大部分的编译器是这么实现new[]的:

  • 对N个类型为T的实例,operator new[]须要为数组分配sizeof(T)*N + 4 bytes的内存
  • 将N存储在前4个字节
  • 使用placement new从ptr + 4的位置开始,构造N个实例
  • 返回ptr + 4处的地址给用户

 

最后一点很是重要:若是你重载了operator new[],返回的内存地址为0x100,那么实例Test* i这个指针指向的位置则是0x104!!!这16个字节的内存布局以下:

0x100: 03 00 00 00    -> number of instances stored by the compiler-generated code

0x104: ?? ?? ?? ??    -> i[0], Test* i
0x108: ?? ?? ?? ??    -> i[1]
0x10c: ?? ?? ?? ??    -> i[2]

当调用delete[]时,编译器会插入代码,从给定指针处减4个字节的位置读取实例的数量N,而后再反序调用析构函数。若是是内置类型或者POD,则没有这4个字节的内存,由于不须要调用析构函数。

不幸的是,这个编译器定义的行为给咱们本身重载使用operator new,operator new[],operator delete,operator delete[]带来了问题,即便咱们能够直接调用operator delete[],也须要经过某种方法获取有多少个析构函数须要调用。

可是咱们作不到!由于咱们不知道编译器是否插入了额外的四个字节,这彻底是根据各个编译器本身实现决定的,也许这样作能够,但也有可能会致使程序崩溃。

 

在了解了以上的知识后,咱们能够在自定义的内存系统中,定义本身的allocator函数,这样就能够正确的处理简单的和数组形式的内存分配和释放,避免了直接重载operator delete[]的问题。同时能够在内存分配时插入更多有用的信息,如文件名,行号等调试信息,也能够定制更多高级特性,更多的内容能够看内存系统的第二部分。

 

 

 

 

 

 

参考link:

https://stoyannk.wordpress.com/2018/01/10/generic-memory-allocator-for-c-part-3/

https://bitsquid.blogspot.com/2010/09/custom-memory-allocation-in-c.html

https://blog.molecular-matters.com/

相关文章
相关标签/搜索