new的三种形态
C++语言一直被认为是复杂编程语言中的杰出代表之一,不仅仅是因为其繁缛的语法规则,还因为其晦涩的术语。下面要讲的就是你的老熟人—new:
它是一个内存管理的操作符,能够从堆中划分一块区域,自动调用构造函数,动态地创建某种特定类型的数据,最后返回该区域的指针。该数据使用完后,应调用delete运算符,释放动态申请的这块内存。
如果这就是你对new的所有认识,那么我不得不说,你依旧被new的和善外表所蒙蔽着。看似简单的new其实有着三种不同的外衣。
是的,你没有看错,也不用感到惊奇,一个简单的new确实有三种不同的形态,它扮演着三种不同的角色,如下所示:
- new operator
- operator new
- placement new
下面的代码片段展示的是我们印象中熟悉的那个new:
- string *pStr = new string("Memory Management");
- int *pInt = new int(2011);
这里所使用的new是它的第一种形态new operator。它与sizeof有几分类似,它是语言内建的,不能重载,也不能改变其行为,无论何时何地它所做的有且只有以下三件事,如图3-2所示。
图3-2 new operator所完成的三件事 |
所以当写出“string *pStr = new string("Memory Management");”代码时,它其实做的就是以下几件事:
- //为string对象分配raw内存
- void *memory = operator new( sizeof(string) );
- //调用构造函数,初始化内存中的对象
- call string::string()on memory;
- //获得对象指针
- string *pStr = static_cast<string*>(memory);
- 当然,对于内置类型,第二步是被忽略的,即:
- //为int分配raw内存
- void *memory = operator new( sizeof(int) );
- //获得对象指针
- int *pInt = static_cast<int*>(memory);
其实new operator背后还藏着一个秘密,即它在执行过程中,与其余的两种形态都发生了密切的关系:第一步的内存申请是通过operator new完成的;而在第二步中,关于调用什么构造函数,则由new的另外一种形态placement new来决定的。
对于new的第二种形态—内存申请中所调用的operator new,它只是一个长着“明星脸”的普通运算符,具有和加减乘除操作符一样的地位,因此它也是可以重载的。
operator new在默认情况下首先会调用分配内存的代码,尝试从堆上得到一段空间,同时它对事情的结果做了最充分的准备:如果成功则直接返回;否则,就转而去调用一个new_hander,然后继续重复前面过程,直到异常抛出为止。所以如果operator new要返回,必须满足以下条件之一:
内存成功分配。
抛出bad_alloc异常。
通常,operator new函数通过以下方式进行声明:
- void* operator new(size_t size);
注意,这个函数的返回值类型是void*,因为这个函数返回的是一个未经处理的指针,是一块未初始化的内存,它像极了C库中的malloc函数。如果你对这个过程不满意,那么可以通过重载operator new来进行必要的干预。例如:
- class A
- {
- public:
- A(int a);
- ~A();
- void* operator new(size_t size);
- ...
- };
- void* A::operator new(size_t size)
- {
- cout<<"Our operator new...");
- return ::operator new(size);
- }
这里的operator new调用了全局的new来进行内存分配(::operator new(size))。当然这里的全局new也是可以重载的,但是在全局空间中重载void * operator new(size_t size)函数将会改变所有默认的operator new的行为方式,所以必须十二分的注意。还有一点需要注意的是,正像new与delete一一对应一样,operator new和operator delete也是一一对应的;如果重载了operator new,那么也得重载对应的operator delete。
最后,要介绍的是new的第三种形态—placement new。正如前面所说的那样,placement new是用来实现定位构造的,可以通过它来选择合适的构造函数。虽然通常情况下,构造函数是由编译器自动调用的,但是不排除你有时确实想直接手动调用,比如对未初始化的内存进行处理,获取想要的对象,此时就得求助于一个叫做placement new的特殊的operator new了:
- #include <new>
- #include "ClassA.h"
- int main()
- {
- void *s = operator new(sizeof(A));
- A* p = (A*)s;
- new(p) A(2011); //p->A::A(2011);
- ... // processing code
- return 0;
- }
placement new是标准C++库的一部分,被声明在了头文件中,所以只有包含了这个文件,我们才能使用它。它在文件中的函数定义很简单,如下所示:
- #ifndef __PLACEMENT_NEW_INLINE
- #define __PLACEMENT_NEW_INLINE
- inline void *__CRTDECL operator new(size_t, void *_Where) _THROW0()
- { // construct array with placement at _Where
- return (_Where);
- }
- inline void __CRTDECL operator delete(void *, void *) _THROW0()
- { // delete if placement new fails
- }
- #endif /* __PLACEMENT_NEW_INLINE */
这就是placement new需要完成的事。细心的你可能会发现,placement new的定义与operator new声明之间的区别:placement new的定义多一个void*参数。使用它有一个前提,就是已经获得了指向内存的指针,因为只有这样我们才知道该把placement new初始化完成的对象放在哪里。
在使用placement new的过程中,我们看到的却是"new(p) A(2011)"这样奇怪的调用形式,它在特定的内存地址上用特定的构造函数实现了构造一个对象的功能,A(2011)就是对构造函数A(int a)的显式调用。当然,如果显式地调用placement new,那么也得本着负责任的态度显式地调用与之对应的placement delete:p->~A();。这部分工作本来可以由编译器独自完成的:在使用new operator的时候,编译器会自动生成调用placement new的代码,相应的,在调用delete operator时同样会生成调用析构函数的代码。所以,除非特别必要,不要直接使用placement new。但是要清楚,它是new operator的一个不可或缺的步骤。当默认的new operator对内存的管理不能满足我们的需要,希望自己手动管理内存时,placement new就变得有用了。就像STL中的allocator一样,它借助placement new来实现更灵活有效的内存管理。
最后,总结一下:
如果是在堆上建立对象,那么应该使用 new operator,它会为你提供最为周全的服务。
如果仅仅是分配内存,那么应该调用operator new,但初始化不在它的工作职责之内。如果你对默认的内存分配过程不满意,想单独定制,重载operator new 是不二选择。
如果想在一块已经获得的内存里建立一个对象,那就应该用placement new。但是通常情况下不建议使用,除非是在某些对时间要求非常高的应用中,因为相对于其他两个步骤,选择合适的构造函数完成对象初始化是一个时间相对较长的过程。
请记住:
不要自信地认为自己对new很熟悉,要正确区分new所具有的三种不同形态,并能在合适的情形下选择合适的形态,以满足特定需求。