当应用程序对于内存分配有特殊要求时,需要重载operator new和operator delete运算符.
new表达式的工作机制
new表达式的工作机理
当我们使用一条new表达式时,实际执行了三步操作.
- 第一步,调用名为operator new(或operator new[])的标准库函数, 分配一块足够大的, 原始的, 未命名的内存空间以便存储特定类型的对象(或对象数组).
- 运行相应的构造函数构造这些对象,并初始化这些对象
- 对象被分配了空间并构造完成,返回一个指向该对象的指针.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23class Complex {
public:
explicit Complex(double _real = 0.0, double _vir = 0.0) : real(_real), vir(_vir) {}
private:
double real;
double vir;
};
Complex *pc = new Complex(1, 2);
//equal to:
void *p = operator new(sizeof(Complex)); // 分配内存
Complex *pc = static_cast<Complex *>(p); // 转型
pc->Complex::Complex(1, 2); // 调用构造函数
delete pc;
//equal to
pc->~Complex(); // 调用对象的析构函数
operator delete(p); // 释放内存空间delete表达式的工作机理
当我们使用一条delete表达式时,实际执行了两步操作.
- 对delete表达式中指针所指向的对象执行相应的析构函数
- 调用名为operator delete(或operator delete[])的标准库函数释放对象内存空间
几个疑问
为什么array new一定要搭配array delete?(array new 即new [size])?
发生内存泄露不在于array new 分配的数组,而在于delete 表达式仅仅调用一次析构函数,而array delete表示调用多次(取决于array new分配的数组大小)析构函数.如果在对象的构造函数中进行了动态内存分配,那么需要在析构函数中进行释放,但是delete表达式仅仅调用一次而非多次析构函数,无法释放数组中所有对象分配的动态内存,因此会造成内存泄露.
定位new表达式
可以通过自定义operator new和operator delete函数来控制内存分配过程.
但是有一个operator new函数不允许被用户重载:
1 | void *operator new(size_t, void*) |
此形式的operator new(定位new)只供标准库使用,不允许用户重载.
该函数并不分配内存,而是直接返回void参数传入的指针;然后由new表达式负责在指定的地址初始化对象以完成整个工作.即*定位new允许我们在一个特定的,预先分配的内存地址上构造对象**.传给定位new表达式的指针可以是堆内存,也可以不是.
使用定位new表达式
定位new表达式的形式: new (place_address) type [n] {initializer list}
其中,place-address是一个指针,指向已经分配好的内存地址, type表示要构造的对象的类型, n可选参数,表示要构造的对象的个数, {initializer list}为初始化列表用于初始化对象.
- 指针place_address指向的是堆内存
1 | void f1(){ |
运行结果
1 | p1:0x55821bbafe70 p2:0x55821bbafe70 |
- 指针place_address指向的是静态内存
1 | const int BUF = 512; |
运行结果
1 | 静态分配的地址buffer: 0x556ab55fd140 |
- 指针place_address指向的是栈内存
1 | void f3(){ |
运行结果
1 | &a:0x7ffe52fda13c p:0x7ffe52fda13c |
重载operator new和operator delete
用户可以自定义oeprator new和operator delete,但是自定义版本必须位于全局作用于或者类的作用域中.
当自定义类的operator new和operator delete时,它们是隐式静态的,无须显式声明static.operator new在对象构造之前调用,而opertator delete在对象销毁之后调用,所以这两个成员必须是静态的,而且它们不操纵类的任何数据成员.
重载类的operator new和operator delete
首先类的声明即定义如下:
1 | class Foo { |
测试代码:
当没有定义成员 operator new和operator delete时就调用全局的operator new和operator delete
1 | void test_foo1() { |
在64位系统上,string占32个字节,int占4个字节,long占8个字节.因此,sizeof(Foo)=32+4+8=44,又因为44不是8的倍数,因此,在Foo中添加一些padding,最后sizeof(Foo)=48字节.
最后的运行结果为:
1 | custom operator new size = 48 |
可以看到,这里调用了自定义的operator new和operator delete函数.此外,分别使用new Foo和new Foo[1]动态分配一个Foo对象时,打印出的size不同,这是因为在operator new[]中分配的内存的最上面有一个额外内存用于记录数组中元素的个数,以备记录调用delete []时,需要调用析构函数的次数.
当new Foo[2]时:size=104.首先两个Foo对象占用内存为48 * 2 = 96字节, 而在分配内存的最前端有一个counter记录数组中元素个数,因此,最后size = 96 + 8 = 104.即这个动态分配的数组在占用内存如下图所示:
此外,可以看到,在delete[]表达式中调用析构函数是逆序调用的,即数组中最后一个元素首先调用析构函数,然后倒数第二个,依次类推,直到第一个元素.
强制调用全局operator new和operator delete
1 | void test_foo2() { |
运行结果:
1 | default ctor.this=0x55e231ba2e70 id=0 |
重载placement new
我们可以重载class member operator new(),写出多个版本,前提是每一版本的声明都必须有独特的参数列,其中第一参数必须是size_t,其余参数是以new所指定的placement arguments为初值.出现于new(…)小括号内的便是所谓placement arguments.也有其他定义说,new()括号内有一个指针做参数时才称为placement new.
1 | Foo* pf = new (300, 'c')Foo; |
我们也可以重载class member operator delete()(或者称此为placement operator delete),写出多个版本.但它们绝不会被delete调用.在旧版本的编译上,只有当new所调用的ctor(构造函数)抛出exception时,才会调用这些重载版的operator delete().它只能这样被调用,主要用来归还未能完全创建成功的object所占用的memory.
下面的示例程序说明只有当new所调用的ctor抛出异常时,才会调用重载版本的operator delete()
1 | class Bad { |
运行结果:
在gnu7.4.0测试,没有调用自定义的operator delete.
new表达式的作用域查找规则
如果被分配(释放)的对象是类类型,则编译器首先在类及其基类的作用域中查找.此时如果该类含有operator new成员或者operator delete成员,则相应的表达式将调用这些成员.否则,编译器在全局作用域中查找匹配的函数.此时如果编译器找到了用户自定义的版本,则使用该版本执行new表达式或delete表达式;否则,则使用标准库定义的版本.