深度探索c++对象模型之placement operator new语意

       首先谈谈new、delete和operator new、operator delete的区别:new和delete只是c++中的运算符而已!而operator new和operator delete则是c++中的函数,是可以重载的函数【但不能在全局域中重载,一般都是在类内重载】,其中operator new被重载时,第一个参数是是要求分配空间的大小(字节),类型一般是size_t,除此之外,还可以带其它的参数,但该函数的返回类型必须是void *;此外,它与new不同的是,它只分配所要求的空间,并不调用相关类的constructor(构造函数),如果分配内存失败,则它会调用new_handler或抛出bad_alloc异常。而operator delete的重载限制为,返回类型是void型,传递参数是void*型。让我们先来看一个示例:
#include <iostream>
#include <string>
using namespace std;

class A{
public:
	A(){ cout << "这是A的构造器在执行!" << endl; }
	~A(){ cout << "在执行A的析构器!" << endl; }

	void *operator new(size_t size, string str)
	{
		cout << "new函数的尺寸为" << size << ",字符串是:" << str << endl;
		return ::operator new(size); //在这里如果我们不调用标准的、全局的operator new,比如直接返回NULL,那么在后面的main函数中,delete p函数就不会得到执行,因为编译器会首先测试p指针是不是NULL值,是就跳过delete的执行
	}

	void operator delete(void *p)
	{
		cout << "delete版的重载函数在执行" << endl;
		::operator delete(p);
	}

private:
	int n;
};

int main()
{
	cout << "A的尺寸是:" << sizeof(A) << endl;
	A *p = new("一个新的A对象") A; //在这里编译器会有扩展,先传递一个sizeof(A),再传递那个汉语字符串
	delete p;

	return 0;
}

在上面的例子中,如果我们能写下“    A* p = new A; ”【但现实情况下,我们这样写会编译错误,因为我们在A的类中已经给new定义了一个重载函数,而这个函数需要传递两个参数,而我们只传递过去一个sizeof(A)】,那么这里的new操作符会做以下这些事,首先是调用operator new类分配内存,接着再调用A类的constructor构造器,最后返回对应的指针。而我们这里的operator new和operator delete和C语言中的malloc和free差不多,只负责内存的分配与回收。事实上,在上面的代码运行时【我用的vs013】,结果显示调用了A的构造函数,但这并不是在operator new里面调用的,而是编译器的功劳】

      那什么是placement new呢?首先说说placement new的好处,它适用于那些对效率要求很高【相比而言,单纯的new需要查找空间等】,能长时间运行不被打断的程序。再说说它的使用方法,以上面的自定义类A为例,如果我们是单个使用,那么只要预先在堆中开辟好内存【void *buf = new[sizeof(A)];】,然后再分配一下即可【A *p = new(buf) A;】,但是对于数组形式来说就稍微有些不同了:

void *buf = new[X*sizeof(A) + sizeof(int)];//其中,X是我们所需要的类对象数组的大小,而sizeof(int)则是一个多出来的用于存放数组大小的字节内存
然后分配时写【A *p = new(buf) A;】即可。。。

      所谓的placement new,其实就是operator new的一个全局域的版本【这也是为什么我们不能在全局域中重载operator new函数的原因】,但与其它operator new不同的是,它不允许被用户自定义的版本的代替。对placement new的调用,一定要传递两个参数,首先是对象的大小,其次是void*类型的指针,但placement new会忽略掉第一个参数,既size_type,因为这个参数对它没啥用,但它一定会以void*形式返还第二个参数,既new出来的指针所指地址。看到这里也许有读者朋友会问,既然这个placement new做的是返回void*指针,那么我们何必调用它呢?我们在堆中开辟好一块内存之后,直接把这个内存起始地址赋值给我们的指针不就行了?嗯,对于那些内建类型如int、char等,确实如此,但对于那些自定义的有着constructor构造器的类类型来说,就不行了,因为我们除了给它们“安家置户”之外,还需要调用它们自带的构造器对它们constructor,让我们来看一下编译器扩展后的伪代码:

//经过编译器处理的伪代码
A *p = (A *)buf;//同上,buf是我们预先开辟好的起始内存地址
if( p != NULL )
  p->A::A();//需要调用A的constructor
      接下来,让我们来考虑下面的代码段:

void fooBar()
{
  A *p = new(buf) A;
  ...//buf没有改变
  //在这里该不该写【delete p;】?
  A *p2 = new(buf) A;
}
如果placement new在原来已经存在的对象上构造一个新对象,而这个对象有一个析构器,那么这个析构器并不会被调用,调用它的析构器的方法是把那个指针delete掉,然和人如果我们真像注释里面那样写的话,那这样同时也会释放buf所指的内存,这并不是我们希望看到的。所以我们应该先明确调用一下A的析构器,然后再写【A *p2 = new(buf) A;】,不过据说,现在的c++ standard用一个placement operator delete矫正了这个错误:它会对类执行该类的析构器,而且不会释放对应的内存。

      还有一个问题,关系到buf所表现出的真正类型,c++ standard说它要么指向相同类型的class,要么指向一块新鲜的内存,足够容纳该类型的对象,所以因为后者,derived class并不在被支持之列,对于derived class,其行为可能是不可控制的,因为如果一个派生类的占用内存如果比它的基类大,比如【A *P3 = new(buf) B;//B是A的derived class】,那么B的constructor很可能导致严重的破坏。

      在placement operator new被引入到cfront2.0时,最隐晦问题是下面这个:

struct Base{int i; virtual void fun();}
struct Derived:Base { void fun(); }

void fooBar()
{
  Base b;
  b.fun();//Base::fun被调用
  b.~Base();
  new(&b) Derived;
  b.fun();//在这里,哪个版本的fun被调用??
}
由于上述两个类有着相同的内存大小,所以把Derived对象放在为Base类中而配置的内存是安全的。但是,要支持这一点,就必须放弃对于“经由对象静态调用所有virtual函数”通常都会有的优化处理。所以最后结果是上面的placement operator new在c++ standard未能获得支持,因为上面代码中的问题并不能明确回答或定义,你可以说它调用的是base的fun,也可以说它调用的是derived里的fun。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值