type
status
date
slug
summary
tags
category
icon
password
Property
当对内存分配有特别需求的时候可以重载
new
和 delete
操作符来控制内存分配。重载 new 和 delete
重载
new
和 delete
操作符的方式与重载其它操作符的方式有非常大的不同。使用
new
表达式时会依次发生三件事:- 调用库函数
operator new
或者operator new[]
来分配足够的裸的没有类型的内存以存储对象或数组
- 调用构造函数构造对象
- 返回指向这个新分配构建的对象的指针
使用
delete
表达式时将发生两件事:- 调用析构函数进行析构
- 编译器调用库函数
operator delete
或者operator delete[]
来释放内存
定义自己的 operator new 和 operator delete 函数可以 hook 掉内存分配过程。即便是库中已经包含了这些函数的定义,依然可以定义自己的版本,而编译器并不会抱怨说重复定义。反而,编译器会使用我们的版本。
当定义全局的 operator new 和 operator delete 函数,程序将在任何时候需要分配内存时都调用我们的函数。当编译器看到 new 或 delete 表达式时就会找到对应的操作符函数进行调用。如果被分配的对象是类类型,那么将首先在类作用域内(包括其基类作用域中)查找这两个函数,如果存在就调用它们。否则将从全局作用域中查找,如果编译器找到一个用户定义的版本,将调用此版本,如果还是找不到将调用库中的版本。
使用作用域操作符来指定使用的 new 或 delete 的版本,如:
::new
和 ::delete
operator new 和 operator delete 接口
库中定义了 8 个重载的 operator new 和 delete 函数,前面四个支持抛出 bad_alloc 异常,后面的则支持不抛出异常。如:
nothrow_t
是定义在 new 头文件中的 struct,这个类型没有成员。new 头文件中还定义了 nothrow 的常量对象,用户可以将其传递给不抛出异常的 new 。与析构函数一样,operator delete 是永远不会抛出异常的。
如果应用程序定义以上函数,必须定义在全局作用域或者作为类的成员,如果定义为类的成员,那么这些操作符函数则隐式是静态的。因而,这些函数不能操作类中的数据成员。
当编译器调用 operator new 时,其第一个参数被初始化为对象的大小。调用 operator new[] 则传递存储给定数目元素的大小。
当定义我们自己的 operator new 时,可以传递额外的参数。如果 new 表达式想要调用这种函数的话,就需要使用定位 new 的形式来传递额外的实参。定义的 operator new 一定不会是以下形式,如:
这个特别形式的函数原型是保留的给库使用,是不可以重复定义的。
调用 operator delete 时,编译器会传递待删除的对象的指针给这个函数。当将 operator delete 或 operator delete[] 定义为一个类成员时,函数可以有第一个参数为
size_t
类型,如果有的话,那么它将会被初始化为第一个参数所表示的对象的大小。真正调用的 delete 函数由被删除的对象的动态类型决定。定义 operator new 和 operator delete 函数可以改变内存分配的方式,但是不能改变 new 和 delete 操作符的基本含义。
malloc 和 free 函数
malloc 和 free 是继承自 C 的库函数,我们的 operator new 和 operator delete 函数可以将底层工作交给这两个函数。其中 malloc 的参数是
size_t
乃是要分配的字节数,返回内存指针或者在失败时返回 0 。而 free 则以 malloc 的返回值作为参数,释放相关的内存,调用 free(0) 是合法的但是没有任何效果。定位(placement)new 表达式
operator new 和 operator delete 是库中的常规函数,意味着可以直接调用这些函数。在语言的早期版本,应用程序为了分离分配内存和初始化工作,会调用 operator new 和 operator delete,这与现在的 allocator 中的 allocate 和 deallocate 成员函数的效果是一致的,它们只负责分配和回收内存。
与 allocator 不同的,早期版本无法直接调用构造函数对内存进行对象构造,相反,我们必须使用定位 new 的方式进行构造对象。定位 new 将提供额外的地址信息,如:
其中
place_address
就是希望在此处构建的内存地址。当我们使用以上形式时,调用的就是 operator new(size_t, void*)
的函数,将返回我们给出的指针实参,这个函数是编译器不允许我们重载的。定位 new 的工作就是在我们指定的地方进行对象初始化。传递给 placement new 的指针可以不是之前 operator new 分配的内存的指针,甚至可以不是动态内存的指针。
显式调用析构函数
虽然不能直接调用构造函数,但是可以直接调用析构函数,与调用任何其它的成员函数一样的方式去调用析构函数。如:
与调用 allocator.destroy 一样,析构函数将清理对象但是不会释放其内存,我们可以重用此内存