在深入探索自定义内存系统之前,我们需要了解一些基础的背景知识,这些知识点是我们接下来自定义内存系统的基础。所以第一部分,让我们来一起深入了解一下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 [];

上面的代码通过调用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 [];

delete[]operator delete[]的行为跟deleteoperator delete是一样的,我们也可以直接调用operator delete[],但是必须记得手动调用析构函数。

但是,如果是非POD类型呢?来看一个例子:

structTest
{
Test(void)
{
// do something
} ~Test(void)
{
// do something
} inta;
}; Test* i = new (__FILE__, __LINE__) Test [];

在上面的情况下,尽管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:        -> number of instances stored by the compiler-generated code

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

当调用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/

C++ Memory System Part1: new和delete的更多相关文章

  1. gem5: 使用ruby memory system中的mesh结构 出现AssertionError错误

    问题:在使用ruby memory system中的mesh结构測试时,出现例如以下错误: Traceback (most recent call last): File "<stri ...

  2. PatentTips - Mechanisms for strong atomicity in a transactional memory system

    BACKGROUND Advances in semi-conductor processing and logic design have permitted an increase in the ...

  3. Bit error testing and training in double data rate (ddr) memory system

    DDR PHY interface bit error testing and training is provided for Double Data Rate memory systems. An ...

  4. Power management in semiconductor memory system

    A method for operating a memory module device. The method can include transferring a chip select, co ...

  5. C++ Memory System Part2: 自定义new和delete

    在第一部分中,我们介绍了new / delete的具体用法和背后的实现细节,这次我们将构建我们自己的小型工具集,可以使用我们自定义的allocator类来创建任意类型的实例(或者实例数组),我们需要做 ...

  6. armv8 memory system

    在armv8中,由于processor的预取,流水线, 以及多线程并行的执行方式,而且armv8-a中,使用的是一种weakly-ordered memory model, 不保证program or ...

  7. C++ Memory System Part3 : 优化

    前面的系列我们讲了自定义new和delete操作,其中针对deleteArray的问题还有需要优化的地方.我们这次就针对POD类型进行一次优化. 下面的代码是针对POD类型的模板函数实现,分别为New ...

  8. A Mixed Flash Translation Layer Structure for SLC-MLC Combined Flash Memory System

    http://blog.sina.com.cn/s/blog_502c8cc40100pztk.html 摘要 1.In this paper, we propose the SLC-MLC mixe ...

  9. STM32 microcontroller system memory boot mode

    The bootloader is stored in the internal boot ROM memory (system memory) of STM32 devices. It is pro ...

随机推荐

  1. java 支付宝即时到帐提交订单dome

    package com.tian.batis; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; imp ...

  2. 以太坊系列之十四: solidity特殊函数

    solidity中的特殊函数 括号里面有类型和名字的是参数,只有类型是返回值. block.blockhash(uint blockNumber) returns (bytes32): hash of ...

  3. MongoDB插入时间不正确的问题

    关于mongodb插入时间不正确的问题 今天在给mongodb插入日期格式的数据时发现,日期时间相差8个小时,原来存储在mongodb中的时间是标准时间UTC +0:00,而中国的时区是+8.00 . ...

  4. 757. Set Intersection Size At Least Two

    An integer interval [a, b] (for integers a < b) is a set of all consecutive integers from a to b, ...

  5. 解决JAR包里面打开源代码都是乱码

    下面是解决方案 通过eclipse浏览源代码时,发现中文注释为乱码的问题.其实这个eclipse默认编码造成的问题.可以通过以下方法解决: 修改Eclipse中文本文件的默认编码:windows-&g ...

  6. [原创]ObjectARX开发环境搭建之VS2010+ObjectARX2012Wizard+Addin工具条问题修复

    目前ObjectARX版本越来越高,也越来越简化开发,如果需要同时开发低版本和高版本的ARX程序,就需要搭建批量编译环境,以满足ARX开发的需要. 批量编译的搭建网络上已经有了很多的教程,基本上都是基 ...

  7. javascript中构造器(函数)的__proto__与prototype初探

    背景:最近没什么需求,快要闲出屁了,所以重温了一下js的原型,结果大有收获,且偶然看到Snandy大神的<JavaScript中__proto__与prototype的关系> 这篇文章,感 ...

  8. WebGL学习之法线贴图

    实际效果请看demo:纹理贴图 为了增加额外细节,提升真实感,我们使用了漫反射贴图和高光贴图,它们都是向三角形进行附加纹理.但是从光的视角来看是表面法线向量使表面被视为平坦光滑的表面.以光照算法的视角 ...

  9. loj #535. 「LibreOJ Round #6」花火 树状数组求逆序对+主席树二维数点+整体二分

    $ \color{#0066ff}{ 题目描述 }$ 「Hanabi, hanabi--」 一听说祭典上没有烟火,Karen 一脸沮丧. 「有的哦-- 虽然比不上大型烟花就是了.」 还好 Shinob ...

  10. phpstudy下安装phalcon

    其实,一共也就下面几步,顺利的话,两分钟完事. 第一步:下载和当前php版本对应的php_phalcon.dll 文件 第二步:将此文件放到php版本下的ext里面. 第三步:在php.ini中添加如 ...