zoukankan      html  css  js  c++  java
  • C++ Memory System Part1: new和delete

    在深入探索自定义内存系统之前,我们需要了解一些基础的背景知识,这些知识点是我们接下来自定义内存系统的基础。所以第一部分,让我们来一起深入了解一下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不同的是,无论我们使用的是那种形式的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/

  • 相关阅读:
    git线上操作
    IDEA快捷方式
    Java 四种线程池
    java 获取当前天之后或之前7天日期
    如何理解AWS 网络,如何创建一个多层安全网络架构
    申请 Let's Encrypt 通配符 HTTPS 证书
    GCE 部署 ELK 7.1可视化分析 nginx
    使用 bash 脚本把 AWS EC2 数据备份到 S3
    使用 bash 脚本把 GCE 的数据备份到 GCS
    nginx 配置 https 并强制跳转(lnmp一键安装包)
  • 原文地址:https://www.cnblogs.com/hellobb/p/10087030.html
Copyright © 2011-2022 走看看