【c++】new和delete解析

某些程序对内存分配有特殊要求,因此我们就无法将标准内存管理机制直接应用于这些程序。它们常常需要自定义内存分配的细节,比如关键字new将对象放置在特定的内存空间中,为了实现这一个目的可以重载new运算符和delete运算符以控制内存分配的过程
(1)重载new和delete
想要重载这两个运算符首先要对这两个运算符有所了解
当我们执行下面一条语句:

string *sp = new string("a value");
string *arr = new string[10];

实际执行了三步操作。
第一步,new表达式调用一个operator new(或者operator new[])的标准库函数。该函数分配一块足够大的、原始的、未命名的内存空间以便存储特定类型的对象,(或者对象的数组)
第二步,编译器运行相应的构造函数以构造这些对象,并为其传入初始值,
第三步,对象被分配了空间并构造完成,返回一个指向该对象的指针
当我们使用一条delete语句时,

delete sp;
delete [] arr;

实际执行了两步操作,
第一步:对sp所指的对象或者arr所指的数组中的元素执行对应的析构函数
第二步:编译器调用名为operator delete(或者operator delete[])的标准库函数释放空间
如果应用程序希望控制内存分配的过程,则它们就需要定义自己的operator new函数和operator delete函数,即使在标准库中已经存在这两个函数定义,我们仍然可以定义自己的版本。编译器不会对这种重复的定义提出异议,相反,编译器,将使用我们自定义的版本。编译器不会对这种重复的定义提出意义,相反,编译器将使用我们自定义的版本替换标准库中定义的版本。
注意:
当定义了全局的operator new函数和operator delete函数后,我们就担负起了控制动态内存内存分配的职责。这两个函数必须是正确的:因为它们是程序整个处理过程中至关重要的一部分。
应用程序可以在全局作用域中定义operator new函数和operator delete函数,也可以将它们定义为成员函数。当编译器发现一条new表达式或delete表达式后将在程序中查找可供调用的operator函数。如果被分配(释放)的对象是类类型,则编译器首先在类及其基类的作用域中查找,此时如果该类含有operator new成员或operator delete成员,则相应的表达式将调用的这些成员,否则,编译器在全局作用域查找匹配的函数。此时如果编译器找到了用户自定义的版本,则使用该版本执行new表达式或者delete表达式。
我们可以使用作用域运算符令new表达式或者delete表达式忽略定义在类中的函数,直接执行全局作用域中的版本。例如,::new只在全局作用域中查找匹配的operator new 函数,::delete与之类似。
operator new接口和operator delete接口
标准库定义了operator new函数和operator delete函数的8个重载版本。其中前4个版本可能抛出bad_alloc异常,后4个版本则不会抛出异常:

//这些版本可能抛出异常
void *operator new(size_t);//分配一个对象
void *operator new[](size_t);//分配一个数组
void *operator delete(void*) noexpect;//释放一个对象
void *operator delete[](void*) noexpect;//释放一个数组

//这些版本承诺不会抛出异常
void *operator new(size_t,nothrow_t&)noexpect;
void *operator new[](size_t,nothrow_t&)noexpect;
void *operator delete(void*,nothrow_t&)noexpect;
void *operator delete[](void *,nothrow_t&)noexpect;

类型nothrow_t 是定义在new头文件中的一个struct,在这个类型中不包含任何成员。new头文件还定义了一个名为nothrow的const对象,用户可以通过这个对象请求new的非抛出版本,与析构函数类似,operatordelete也不允许抛出异常。当我们重载这些运算符时必须使用noexpect异常说明符指定其不抛出异常。
应用程序可以自定义上面函数中的任意一个,前提是自定义的版本必须位于全局作用域或者类作用域中。当我们将上述运算符函数定义成类的成员时,它们是隐式静态的。我们无需显式的说明static,当然这么做也不会引发错误,因为operator new用在对象构造之前,而operator delete用在对象销毁之后,所以这两个成员(new和delete)必须是静态的,而且它们不能操纵类的任何数据成员。
对于operator new函数或者operator new[]函数来说,它们的返回值类型必须是void*,第一个形参的类型必须是size_t且该形参不能有默认实参,当我们为一个对象分配空间时使用operator new,为一个数组分配空间时使用operator new,为一个数组分配空间时使用operator new[],当编译器调用operator new时,把存储指定类型对象所需的字节数传给size_t形参。当调用operator[]时,传入参数的则是存储整个数组中所有元素所需的空间。
如果我们想定义operator new函数,则可以为它提供额外的形参,此时,用到这些自定义函数的new表达式必须使用new的定位形式,将实参传给新增的形参。尽管在一般情况下,我们可以自定义具有任何形参的operator new,但是下面这个函数却无论如何不能被用户重载:

void *operator new(size_t,void*);

这种形式只供标准库使用,不能被用户重新定义
对于operator delete函数或者operator delete[]函数来说,它们的返回类型必须是void,第一个形参的类型为size_t的形参。此时,该形参的初始值是第一个形参所指对象的字节数,size_t形参可用于删除继承体系中的对象。如果基类有一个虚析构函数,则传递给operator delete的字节数将因待删除指针所指对象的动态类型不同而有所区别。而且,实际运行的operator delete函数版本也由对象的动态类型决定。
术语解释:new表达式与operator new函数
标准库函数operator new和operator delete的名字容易让人误解。和其他operator函数不同(比如operator=),这两个函数并没有重载new表达式或delete表达式。实际上,我们根本无法定义自定义new表达式或delete表达式的行为
一条new表达式的执行过程总是先调用operator new函数以获取内存空间,然后在得到的空间中构造对象,然后调用operator delete函数释放对象所占的空间
我们提供新的operator new函数和operator delete函数的目的在于改变内存分配的方式,但是不管怎么样我们都不能改变new运算符和delete运算符的基本含义

malloc和free函数
当你定义了自己的全局operator new和operator delete后,这两个函数必须以某种方式执行分配分配内存和释放内存的操作。也许你的初衷仅仅是使用一个特殊定制的内存分配器,但是这两个函数还应该同时满足某些测试的目的,即检验其分配的内存的方式是否与常规方式类似。我们可以使用malloc和free函数来简单实现operator
new 和 operator delete函数

void* operator new(size_t size)
{
    if(void *mem = malloc(size))
    return mem;
    else
    throw bad_alloc();
}
void operator delete(void *mem) noexcept
{
    free(mem);
}

定位new表达式
尽管operator new函数和operator delete函数一般用于new表达式,然而它们毕竟是标准库的两个普通函数,因为普通函数代码也可以直接调用它们。
在c++早期版本allocator类还不是标准库的一部分。应用程序如果想把内存分配与初始化分开来的话,需要调用operator new和operator delete。这两个函数的行为与allocator的allocate和deallocate成员类似,它们负责分配或释放内存空间,但是不会构造或销毁对象。
与allocator不同的是,对于operator new分配的内存空间来说我们无法使用construct函数构造函数。相反,我们应该使用new的定位new形式构造对象。如我们所知,new的这种形式未分配函数提供了额外的信息。我们可以使用定位new传递一个地址,此时定位new的形式如下所示。
new (place_address) type
new(place_address) type (initializers)
new(place_address)type [size]
new (place_address) type [size] {braced initializer list}
其中place_address必须是一个指针,同时在initializers中提供一个(可能为空的)以逗号分隔的初始值列表,该初始值列表将用于构造新分配的对象
当仅通过一个地址值调用时定位new使用operator new(size_t ,void*)”分配”它的内存。这是一个我们无法自定义的operator new版本。该函数不分配任何内存,它只是简单的返回指针实参,然后由new表达式负责在指定的地址初始化对象已完成整个工作。事实上,定位new允许new允许我们在一个特定预先分配的内存地址上构造对象
当仅通过一个地址值调用时,定位new使用operator new(size_t,void*)”分配它的内存”,这是一个我们无法自定义的operator new版本。该函数不分配任何内存,它只是简单的返回指针实参,然后由new表达式负责在指定的地址初始化对象以完成整个工作。事实上,定位new允许我们在一个特定的预先分配的内存地址上构造对象。

显式的调用析构函数
就像定位new与使用allocate类似一样,对析构函数的显式调用也与使用destroy很类似。我们既可以通过对象调用析构函数,也可以通过对象的指针或引用调用析构函数,这与调用其他成员函数没有什么区别:
string *sp = new string(“a value”);
sp->~string();
在这里我们直接调用了一个析构函数。箭头运算符解引用指针sp以获得sp所指向的对象,然后我们调用析构函数。析构函数的形式是(~)加上类型的名字
和调用destroy类似,调用析构函数可以清除给定的对象但是不会释放该对象所在的空间,如果需要的话,我们可以重新使用该空间。
调用析构函数会销毁对象,但是不会释放内存。

特别的:new[]和delete[] 会在开辟空间的前四个字节(一个int)保存数组成员个数来确定来调用几次构造和析构函数

猜你喜欢

转载自blog.csdn.net/flowing_wind/article/details/81272022