码迷,mamicode.com
首页 > 编程语言 > 详细

C++ Memory System Part1: new和delete

时间:2018-12-08 13:18:02      阅读:212      评论:0      收藏:0      [点我收藏+]

标签:需要   顺序   des   函数   sys   cal   __file__   directly   cto   

在深入探索自定义内存系统之前,我们需要了解一些基础的背景知识,这些知识点是我们接下里自定义内存系统的基础。所以第一部分,让我们来一起深入了解一下C++的newdelete家族,这其中有很多令人吃惊的巧妙设计,甚至有很多高级工程师都其细节搞不清楚。

 

new operator and operator new

 

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

T* i = new T;

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

  • 首先,调用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函数:

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

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

 

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来创建实例,都要使用一个对应版本的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;

 

这里会有一点绕,如果在程序正常运行时(即没有发生异常时),你调用placement new 分配的内存是通过operator delete删除的,如上所示,它不会调用placement delete。只有在placement new发生异常时(即分配了内存,但是还没有来得及调用构造函数时发生异常),运行时系统才会去寻找匹配placement newplacement delete,如果这时你定义了匹配的placement delete则会正常的调用。如果你并没有定义匹配的placement 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[]的行为跟delete和operator 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[],也需要通过某种方法获取有多少个析构函数需要调用。

 

但是我们做不到!我们不能通过自己插入额外的字节来解决,因为我们不知道编译器是否又插入了额外的四个字节,这完全是根据各个编译器自己实现决定的,也许这样做可以,但也有可能会导致程序崩溃。所以在实际的使用中,最好不要直接去调用operator delete[]

 

通过以上的知识,我们了解到,我们可以在自定义的内存系统中,定义自己的allocator函数,然后在使用new, new[],deletedelete[]来实现自定义的内存管理。更多的内容可以看内存系统的第二部分。

 

C++ Memory System Part1: new和delete

标签:需要   顺序   des   函数   sys   cal   __file__   directly   cto   

原文地址:https://www.cnblogs.com/hellobb/p/10087030.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!