new,operator new,placement new

来源:互联网 发布:js中的element 编辑:程序博客网 时间:2024/05/16 13:49

本篇文章的内容主要来自《C++ Prime》第五版的19.1节,《Effective C++》第三版的条款49-52,《Linux多线程服务端编程》的12.2节。该博文就权当这几个知识点的整理。

第一篇章:基础知识点

(一)new表达式和operator new
首先,需要先了解new表达式和delete表达式的工作机理:
实际上,一个new表达式做了两件事:
1.调用名为operator new的标准库函数,分配一块满足需求的原始内存;
2.若成功获取到所需内存,则调用相应的构造函数以构造对象;

delete表达式的过程如下:
1.调用对象的析构函数析构对象;
2.调用名为operator delete的标准库函数来归还内存空间;

可以看到,一条new表达式或delete表达式对内存空间的分配和归还操作实际上是在operator new和operator delete这两个标准库函数里面做的,所以当我们想控制内存分配的过程,就可以通过自定义operator new和operator delete函数来替换标准库的这两个函数。我们通常所说的重载new和delete,实质上不是重载new表达式和delete表达式,而只是重载了这两个标准库函数而已。

(二)重载new和delete
重载new和delete的目的在于控制内存分配的方式。
在了解了new表达式和operator new之间的关系之后,再来看重载new就简明多了,其实就是自定义operator new函数来替换标准库的函数而已。另外,重载operator delete,operator new []等实质上是一样的,下面如果没有特殊情况下,就以operator new讲解。
可以针对全局空间重载operator new,也可以在类类型中重载operator new。《C++ Primer》中讲到编译器匹配operator new的规则:
如果是类类型,则先在类的作用域中查找,如果该类含有operator new成员函数,则将调用这个operator new成员函数,否则,编译器将在全局作用域查找匹配的函数,此时,如果用户自定义了版本,则用该版本的operator new函数,最后,如果没找到,则使用标准库的版本。

在全局区间和在类在重载operator new对整个程序的影响是不同的,后面会专门讲这一点。

(三)operator new和placement new
标准库定义了operator new的三个重载版本:
void *operator new(size_t);
void *operator new(size_t,nothrow_t &) noexpect;
void *operator new(size_t , void *);      //这个后面还会重点讲到,请先稍微留意一下这个标准库函数
可以看到,operator new有两种函数调用形式,第一种调用形式只有一个形参size_t,我们经常使用的new表达式的常见形式,例如,下面的表达式:
int *p = new  int() ;
就是调用的第一种形式的operator new(size_t),该表达式将会把int类型所需的字节数传给size_t形参;
第二种调用形式有两个形参,除了size_t,还会有另一个额外的参数:operator new(size_t , placement param),这个placement param可以是任何类型,这种形式的operator new函数无疑可以多传一个额外的参数,例如下面的表达式:
int *p1 = new (nothrow) int() ; 
该表达式额外传多了一个nothrow的参数,最终会调用operator new(size_t , nothrow_t &)函数。而这就是我们常说的placement  new (new的定位形式)。

在重载placement new形式时,一般我们可以自定义具有任何形参的operator new,但有一种形式是只供标准库使用,不能被用户重新定义的:
void *operator new(size_t , void *) ;
在之前讲到标准库重载的三个operator new版本时,其中一个就是这种形式了。之所以用户不能重载这个形式的operator new,是因为它有特殊的定义:这个函数不分配任何内存,它只是简单地返回指针实参(即传给void *形参的那个实参)。
那这种新式的placement new有什么用处呢?答案是可以实现内存分配和对象构造的分离,例如下面的例子:
int *p = (int *) operator new(sizeof(int));new(p) int(1) ;
先用普通的operator new分配需要的内存,然后用new(void *)表达式实现对象的构造。



第二篇章:operator new,placement new的应用

上面三点是new表达式,operator new,placement new的基础知识点,接下来将进一步讨论如何使用这些相关的知识点,以及在使用时要注意的事项。

(四)new,delete和异常
思考一下,如果在执行new表达式的过程中抛出异常,或在执行delete表达式的过程中抛出异常,会分别出现什么结果呢?
先给出结论:执行new表达式的过程中抛出异常是安全的,即允许在operator new函数和对象的构造函数中抛出异常;而执行delete表达式的过程中抛出异常是不安全的,不安全是指可能导致内存泄露,所以在operator delete函数和对象的析构函数中不能抛出异常。

通常情况下,operator new在分配内存失败时会抛出std::bad_alloc的异常,我们可以通过placement new的形式调用标准库的nothrow版operator new。如果要构造的对象在其构造函数中抛出异常,则C++的运行时系统会帮我们调用对应的operator delete版本,来归还之前分配的内存,所以不会有内存泄露的问题。
既然构造函数有可能抛出异常,那么这种不抛出异常的Nothrow new实际上是不能够保证new表达式不抛异常的:
class AA ;
new(std::nothrow) AA ;   //不能够保证这条语句不抛出异常,所以这个工具实质上有局限性。

相反,delete表达式是先执行对象的析构函数,如果在析构函数中抛出异常,则没有机会执行operator delete函数,很明显,这将导致内存泄露。另一方面,如果在operator delete函数中抛出异常,有可能是在释放内存之前或释放的过程中出现的,这时当然也是不安全的。

这里还有一点需要注意的是,保证构造函数抛出异常是安全的行为是有前提条件的。举个例子,我们重载了placement new的一个版本:
void *operator new(size_t  size, std::ostream &logStream) ;
然后使用下面的new表达式:
class AA ;
new(std::cout) AA ; 
如果这时AA 的构造函数中抛出了异常,C++运行系统会寻找参数个数和类型都与 operator new相同的某个operator delete。如果找不到对应版本的operator delete,那么运行期系统将什么也不做,这时将导致内存泄露。为了消除这种内存泄露,我们需要同时写下对应的operator delete:
void operator delete(void *p , std::ostream &logStream) noexpect;
类似的,这种接受额外参数的operator delete被称为placement delete。所以前面说的前提条件是:写了placement new也要写placement delete。
这也是Effective C++里面的条款52讲到的。

(五)重载operator new的时机

为什么要重载operator new呢?重载operator new能有什么好处?

总结起来有几点:

(1)特殊情形下的定制版效能更高:

编译器所带的operator new和operator delete需要适合大多数的场景,包括处理大块内存,小块内存,大小混合型内存等,这会带来内存破碎问题,性能在某些场景下偏低的问题等。通过对某些应用场景进行定制,则有可能达到用更快的速度,更少的内存完成内存分配。例如下面的场景:

需要分配大量的小块内存,例如我们要定义大量的class AA对象,那么我们可以定义AA的operator new成员函数,专门来管理类AA的内存。

(2)收集使用上的数据

        通过定制new和delete,我们可以统计内存块的分配状态:每次分配的大小,寿命,分配的最大动态内存等使用情况

(3)检测运用上的错误

最常用的一个场景是在定制的operator new中分配超额的内存,以额外空间放置特定的字节作为签名,当执行operator delete时检查签名是否被改。通过这个签名是否被改能够判断出是否有越界的行为。


六.一个合格的operator new函数应该是怎么样的

每一个operator new有其要固守的一些常规,例如:

(1)返回的地址必须符合内存对齐规则

(2)错误处理:如果内存分配成功则直接返回申请的内存地址,如果失败,最终会返回一个NULL或抛出一个异常。在返回或抛异常之前,会有一个循环:一直调用new-handling函数直到内存分配成功,或者new-handling被设置为空则跳出循环:

C++标准库的new-hanlding函数在operator new函数分配内存失败时被调用;用户可以定制自己的new-hanlding函数,通过标准库的set_new_handler()函数来替换标准库的new-handling函数。


参考一下vs2010中operaotr new的实现可以更清晰的看到这两点固守的常规:

 

void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)        {       // try to allocate size bytes        void *p;        while ((p = malloc(size)) == 0)                if (_callnewh(size) == 0)                {       // report no memory                static const std::bad_alloc nomem;                _RAISE(nomem);                }        return (p);        }

值得注意的是,内存分配是通过调用malloc()实现的,而malloc()分配的内存保证是内存对齐的;另外,可以很明显的看到确实包含了一个while循环。


七.重载operator new的方式和其对主程序和第三方库的影响

重载operator new的方式有几种,按作用域分:

(1)重载全局operator new

(2)重载的operator new作为类成员函数(隐式静态)

按函数调用形式分:

(1)正常的operator new形式,只有一个size_t类型参数

(2)placement new形式,可以有一个附加的参数


其中,全局的正常形式operator new是侵略性最大的,因为使用方无需看到该重载函数的声明,即使用方无需包含重载operator new的文件;

如果我们的程序与第三方库进行交互,那么必须注意全局operator new的这种侵略性。最重要的原则是,在哪个库申请的内存必须在这个库里面归还内存,因为一旦重载了全局operator new,就不能保证不同的库之间使用的是同个分配器。如果确实需要跨库申请和归还内存,那么库必须提供对应的接口来注册申请内存和归还内存的函数。

另外,考虑一下,库一般以二进制形式和头文件形式提供给用户使用,如果以二进制形式提供,那么不会受到我们重载的全局operator new影响,但是如果是以头文件形式提供的,那么在这些头文件中调用到的new就会在无形中使用了我们重载的operator new,最典型的就是那些模板库了。例如标准库的容器类。看看下面的一个例子:

void *operator new(size_t size){std::cout << "our opertaor new!" << std::endl ;return malloc(size) ;}int _tmain(int argc, _TCHAR* argv[]){std::vector<int > v1 ;v1.push_back(1) ;         //容器获取新的内存若是使用了我们重载的operator new,则能看到“our operator new”的输出。system("pause") ; return 0;}
输出结果:

our operaotr new!
所以,std::vector在分配内存时,调用的是我们重载的operaotr new函数


若在全局区域重载的是placement new,则需要使用方看到该placement new的声明,例如:

在A文件中:void *operator new(size_t , bool flag) ;

在B文件中必须有同样的该operator new的声明,或者直接包含A文件,才能使用下面的new表达式形式:

int *p = new(true) int ;


  这种placement new的方式貌似比正常的operator new好点,但是,有个很有趣的事如下:
  如果你的placement new不能和标准库的operator delete匹配(使用了不同的分配器,或者分配了额外的内存),那么你也必须重载你的全局operator delete,这样一来,你又必须重载全局的正常形式的operator new了。所以这种重载方式也是有很大的限制的。

最后,在类里面重载operator new,相对来说影响会小很多,因为它只对该类有效果。一般的优化内存分配效率就是采用这种方式了。







原创粉丝点击