C++ Memory System Part1: new和delete
在深入探索自定义内存系统之前,我们需要了解一些基础的背景知识,这些知识点是我们接下来自定义内存系统的基础。所以第一部分,让我们来一起深入了解一下C++的new和delete家族,这其中有很多令人吃惊的巧妙设计,甚至有很多高级工程师都对其细节搞不清楚。
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[]
到目前为止,我们只讲解了new和delete的非数组版本,它们还有一对为数组分配内存的版本:
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[]的行为跟delete和operator 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的更多相关文章
- gem5: 使用ruby memory system中的mesh结构 出现AssertionError错误
问题:在使用ruby memory system中的mesh结构測试时,出现例如以下错误: Traceback (most recent call last): File "<stri ...
- 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 ...
- 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 ...
- Power management in semiconductor memory system
A method for operating a memory module device. The method can include transferring a chip select, co ...
- C++ Memory System Part2: 自定义new和delete
在第一部分中,我们介绍了new / delete的具体用法和背后的实现细节,这次我们将构建我们自己的小型工具集,可以使用我们自定义的allocator类来创建任意类型的实例(或者实例数组),我们需要做 ...
- armv8 memory system
在armv8中,由于processor的预取,流水线, 以及多线程并行的执行方式,而且armv8-a中,使用的是一种weakly-ordered memory model, 不保证program or ...
- C++ Memory System Part3 : 优化
前面的系列我们讲了自定义new和delete操作,其中针对deleteArray的问题还有需要优化的地方.我们这次就针对POD类型进行一次优化. 下面的代码是针对POD类型的模板函数实现,分别为New ...
- 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 ...
- STM32 microcontroller system memory boot mode
The bootloader is stored in the internal boot ROM memory (system memory) of STM32 devices. It is pro ...
随机推荐
- java 支付宝即时到帐提交订单dome
package com.tian.batis; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; imp ...
- 以太坊系列之十四: solidity特殊函数
solidity中的特殊函数 括号里面有类型和名字的是参数,只有类型是返回值. block.blockhash(uint blockNumber) returns (bytes32): hash of ...
- MongoDB插入时间不正确的问题
关于mongodb插入时间不正确的问题 今天在给mongodb插入日期格式的数据时发现,日期时间相差8个小时,原来存储在mongodb中的时间是标准时间UTC +0:00,而中国的时区是+8.00 . ...
- 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, ...
- 解决JAR包里面打开源代码都是乱码
下面是解决方案 通过eclipse浏览源代码时,发现中文注释为乱码的问题.其实这个eclipse默认编码造成的问题.可以通过以下方法解决: 修改Eclipse中文本文件的默认编码:windows-&g ...
- [原创]ObjectARX开发环境搭建之VS2010+ObjectARX2012Wizard+Addin工具条问题修复
目前ObjectARX版本越来越高,也越来越简化开发,如果需要同时开发低版本和高版本的ARX程序,就需要搭建批量编译环境,以满足ARX开发的需要. 批量编译的搭建网络上已经有了很多的教程,基本上都是基 ...
- javascript中构造器(函数)的__proto__与prototype初探
背景:最近没什么需求,快要闲出屁了,所以重温了一下js的原型,结果大有收获,且偶然看到Snandy大神的<JavaScript中__proto__与prototype的关系> 这篇文章,感 ...
- WebGL学习之法线贴图
实际效果请看demo:纹理贴图 为了增加额外细节,提升真实感,我们使用了漫反射贴图和高光贴图,它们都是向三角形进行附加纹理.但是从光的视角来看是表面法线向量使表面被视为平坦光滑的表面.以光照算法的视角 ...
- loj #535. 「LibreOJ Round #6」花火 树状数组求逆序对+主席树二维数点+整体二分
$ \color{#0066ff}{ 题目描述 }$ 「Hanabi, hanabi--」 一听说祭典上没有烟火,Karen 一脸沮丧. 「有的哦-- 虽然比不上大型烟花就是了.」 还好 Shinob ...
- phpstudy下安装phalcon
其实,一共也就下面几步,顺利的话,两分钟完事. 第一步:下载和当前php版本对应的php_phalcon.dll 文件 第二步:将此文件放到php版本下的ext里面. 第三步:在php.ini中添加如 ...