探讨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个字节来数组的大小。

posted on 2013-10-10 16:02  husoling  阅读(493)  评论(0)    收藏  举报

导航