探讨new/delete
在C++中,new与delete(或者new[]/delete[])用于在堆上构建与销毁对象,那么它们是怎么工作的呢?本篇博文将对此进行简易的探讨。
我们都知道,C++中new其实代表了三种含义,new operator,operator new,placement new:
1. new operator是new操作符,即与'+'等同样的是属于C++的操作符,常见的使用方法为SomeClass *pobj = new SomeClass(param-list)。这里请注意,new operator是由编译器实现的,它负责创建对象所需的内存,并调用类的相应的构造函数来构建对象。
2. operator new是函数,功能是分配用于建立对象的内存块,常见的使用方法为SomeClass::operator new(size)。这里请注意,operator new不负责调用类的构造函数,即,它只负责分配内存,不负责构建对象。
3. placement new,在已有的一块内存上构建对象,一般的使用方法为:SomeClass *pobj = new(pmem) SomeClass(param-list)。
相对来说,delete/delete[]就比较简单了,就是析构对象,销毁内存(注意顺序)。
之前,我看过一篇博文,上面有关于new operator的实现(http://blog.csdn.net/masefee/article/details/4460947)。该博文里说的很清楚,new operator其实是先调用了operator new去分配足够的内存,然后再调用类的构造函数去构建对象的。一般来说,各个编译器大致都是如此实现的,只是细节有所不同,同样的类对应着不同的编译器,其单个对象分配的内存大小也有所不同。
new operator调用了operator new,那么operator new又是如何工作的呢?operator new的声明在new文件中(VC或者linux下都能找到),打开文件后,可以看到其声明的格式:
void* operator new(std::size_t) throw (std::bad_alloc);
void* operator new[](std::size_t) throw (std::bad_alloc);
void operator delete(void*) throw();
void operator delete[](void*) throw();
void* operator new(std::size_t, const std::nothrow_t&) throw();
void* operator new[](std::size_t, const std::nothrow_t&) throw();
void operator delete(void*, const std::nothrow_t&) throw();
void operator delete[](void*, const std::nothrow_t&) throw();
// Default placement versions of operator new.
inline void* operator new(std::size_t, void* __p) throw() { return __p; }
inline void* operator new[](std::size_t, void* __p) throw() { return __p; }
// Default placement versions of operator delete.
inline void operator delete (void*, void*) throw() { }
inline void operator delete[](void*, void*) throw() { }
根据上面显示的代码,可以发现,new/new[]有多种形式,相应的delete/delete也是一样的。不同平台下的operator new形式大致相同(我没验证过,有兴趣的可以查看一下),但是其实现在不同平台下,是不一样的。不过,一般的编译器都实现了一下几点功能:
1. 自动填充字节数,以达到字节按4对齐的效果。比如:
#include <stdio.h>
#include <iostream>
using namespace std;
class Base
{
public:
int a;
char c;
};
class A: public Base
{
public:
bool b;
};
int main()
{
cout << "sizeof(Base): " << sizeof(Base) << endl;
cout << "sizeof(A): " << sizeof(A) << endl;
Base *pb = new Base;
printf("addr of pb: %p\n", pb);
printf("addr of pb->a: %p\n", &pb->a);
printf("addr of pb->c: %p\n", &pb->c);
A *pa = new A;
printf("addr of pa: %p\n", pa);
printf("addr of pa->a: %p\n", &pa->a);
printf("addr of pa->c: %p\n", &pa->c);
printf("addr of pa->b: %p\n", &pa->b);
return 0;
}
其结果如图:

sizeof(int)是4,sizeof(char)是1,但是sizeof(Base)是8,由此可见,编译器在内存块的尾部自动填充了3字节的内存,以达到内存按字长对齐的效果。同样的sizeof(bool)是1,由于sizeof(Base)等于8,则为了对齐,编译器自动填充3字节,使sizeof(A)为12.
2. 自动附加与分配内存大小相关的信息,这个功能是由malloc实现的。operator new调用了malloc:
_GLIBCXX_WEAK_DEFINITION void *
operator new (std::size_t sz) _GLIBCXX_THROW (std::bad_alloc)
{
void *p;
/* malloc (0) is unpredictable; avoid it. */
if (sz == 0)
sz = 1;
p = (void *) malloc (sz);
while (p == 0)
{
new_handler handler = __new_handler;
if (! handler)
_GLIBCXX_THROW_OR_ABORT(bad_alloc());
handler ();
p = (void *) malloc (sz);
}
return p;
}
以上是operator new在libstdc++中的实现,可以明显的看出,malloc被调用了。
3. 对于非内置类型(即类类型),使用new[]运算符分配对象数组时,会在对象数组内存区域的前面(逻辑上),添加相应的信息,以表示该数组的大小,方便内存的回收。
#include <iostream>
class SomeClass
{
public:
SomeClass(): a(1), b(true), c('c') {}
~SomeClass() {}
private:
int a;
bool b;
char c;
};
int main()
{
int *i = new int[5];
SomeClass *psc = new SomeClass;
psc = new SomeClass();
psc = new SomeClass[5];
int *pi = (int *)((int *)psc - 1);
std::cout << "pi: " << *pi << std::endl;
return 0;
}
以上代码的运行结果如下:

可以看到,在psc所指向的内存区域的前方,紧挨着有4个字节(32bit)表示SomeClass对象数组中元素的个数:5. 这是在win32平台下的,如果在64位linux平台下,指示数组元素个数的字节数是8(64bit),同样,这8个字节也是在数组的内存区域之前。
需要注意的是,以上的实现并不是由operator new[]实现的,operator new[]其实就是简单的包装了一下operator new。那么,这个数字5是怎么赋值给那4个(或者8个)字节的呢?答案是编译器添加了一些代码,使数组元素个数得以保留。
psc = new SomeClass[5]; 000000013FC01169 mov ecx,2Ch 000000013FC0116E call operator new[] (13FC01D40h) // 调用operator new[]分配内存 000000013FC01173 mov qword ptr [rsp+78h],rax // rsp为栈指针,通常指向栈顶,rax通常保存着上一个函数调用的返回值,这里就是分配好的内存首地址 000000013FC01178 cmp qword ptr [rsp+78h],0 000000013FC0117E je main+13Dh (13FC011CDh) 000000013FC01180 mov rax,qword ptr [rsp+78h] 000000013FC01185 mov dword ptr [rax],5 // 向分配好的内存块中的首4个字节赋值5 000000013FC0118B mov rax,qword ptr [rsp+78h] 000000013FC01190 add rax,4 // 偏移4个字节,即跳过头4个字节 000000013FC01194 lea rcx,[SomeClass::~SomeClass (13FC01041h)] 000000013FC0119B mov qword ptr [rsp+20h],rcx 000000013FC011A0 lea r9,[SomeClass::SomeClass (13FC0103Ch)] 000000013FC011A7 mov r8d,5 000000013FC011AD mov edx,8 000000013FC011B2 mov rcx,rax 000000013FC011B5 call `eh vector constructor iterator' (13FC01DC0h) // 这里应该是迭代调用构造函数,不过我不太确定 000000013FC011BA mov rax,qword ptr [rsp+78h] 000000013FC011BF add rax,4 000000013FC011C3 mov qword ptr [rsp+98h],rax 000000013FC011CB jmp main+149h (13FC011D9h) 000000013FC011CD mov qword ptr [rsp+98h],0 000000013FC011D9 mov rax,qword ptr [rsp+98h] 000000013FC011E1 mov qword ptr [rsp+70h],rax 000000013FC011E6 mov rax,qword ptr [rsp+70h] 000000013FC011EB mov qword ptr [psc],rax // 将结果给psc
以上是psc = new SomeClass[5]反汇编后得到的汇编代码(VC平台下)。通过汇编代码可以清晰的看到new SomeClass[5]的动作。
值得注意的是,调用operator new[](size_t size)时,传入的size的值,实际上已经是sizeof(SomeClass) + 4。
最后再说一点,对于内置类型,比如int *a = new int[5],其操作实际与int *a = (int *)malloc(5 * sizeof(int))一样,并没有像类类型的数组分配一样,在头上划出额外的4个字节来数组的大小。
浙公网安备 33010602011771号