深入解析new、全局和局部operator new、::new、placement new

1. new分为两步

a、内存分配
    调用相应的operator new( )分配内存
  b、构造函数
    调用相应的构造函数

实验代码:

class T{  
public:  
    T(){  
        cout << "构造函数。" << endl;  
    }  

    ~T(){  
        cout << "析构函数。" << endl;  
    }  

    void * operator new(size_t sz){  

        T * t = (T*)malloc(sizeof(T));  //operator new就是简单的分配内存即可
        cout << "内存分配。" << endl;  

        return t;  
    }  

    void operator delete(void *p){  

        free(p);  
        cout << "内存释放。" << endl;  

        return;  
    }  
};  

int main()  
{  
    T * t = new T(); // 先 内存分配 ,再 构造函数  

    delete t; // 先 析构函数, 再 内存释放  

    return 0;  
}  

输出为:

内存分配。
构造函数。
析构函数。
内存释放。

我们都知道malloc和free是c语言的分配内存和销毁内存的语句,而new和delete是c++分配内存和销毁内存的语句。
2、new的内部实现机制
  我们都知道new分为两步完成,但是new是怎样完成这两步的呢?
开始我以为是在new内部封装了一个两个函数,一个用来分配内存,另一个用来调用相应的构造函数。

但事实上并非如此,根本就没有一个“隐藏”的new函数,编译器会自动将new改写成一个内存分配函数,一个构造函数。
这里其实还有一个更本质的问题需要说明:new的本质是关键字,编译器在背后的操作可能与我们的想象大不相同。能够重载的是函数,是运算符,关键字是不能重载的(有的博客中说operator new可以重载,new不能重载就是这个原因,更本质的原因可以概括为不能重载关键字),这也是为什么运算符重载前面必须要有operator的原因。+(int a),new(int a)都是错的。
简而言之,new在编译器的操作下会变成两部分代码,一部分分配内存,一部分调用构造函数。

这其实是可以做实验验证的:
  就在上述代码中加入new T并打上断点,再F10一步一步地调试,发现先进入类中的operator new,执行完后并没有到new T的下一行,而是依然停留在new这一行,继续F10进入构造函数。就是说new T这一行代码停留了两次。这意味着编译器确实将new T这一行代码翻译成了两次函数调用。

第二种验证方式就是直接使用反汇编查看代码(推荐):
  在这里插入图片描述
      
  可以看到的确有两个call,第一个call T::operator new( )。第二个call T::T( )。注意:真正的汇编中没有具体的函数名,只有相应的地址。显示函数名是VS的一个比较便利的功能。

3、::new与new
  在全局命名空间中有一个自带的、隐藏的operator new专门用来分配内存。默认情况下编译器会将new这个关键字翻译成这个operator new和相应的构造函数。

但在有的情况下,用户自己会在类中重载operator new,这种情况下,编译器默认会使用类中重载的operator new(本质上因为编译器会从命名空间由内而外查找自己想要的函数,选用第一个)。

如果我们想要继续使用默认的operator new,就应该写成::new 字面意思就是调用最外层命名空间中的operator new
比如将上面的代码主函数中的 T * t = new T(); 修改为T * t = ::new T();
这个时候的结果就是:

构造函数。
析构函数。
内存释放。

看到了吗,没有自己定义的内存分配这四个大字了,说明系统调用了自己默认的内存分配方式。
 值得一提的是最外层的(也是默认的)operator new也是可以被重载的。通过这种方式我们可以改变所有new的部分行为。
 代码如下:

using namespace std;
void* operator new (size_t size)
{
	cout << "内存分配"<<endl;
	return malloc(size);
}
class T {
public:
	T() {
		cout << "构造函数。" << endl;
	}

	~T() {
		cout << "析构函数。" << endl;
	}

	void * operator new(size_t sz) {

		T * t = (T*)malloc(sizeof(T));  //operator new就是简单的分配内存即可
		cout << "内存分配。" << endl;

		return t;
	}

	void operator delete(void *p) {

		free(p);
		cout << "内存释放。" << endl;

		return;
	}
};

int main()
{
	T * t = ::new T; // 先 内存分配 ,再 构造函数  

	delete t; // 先 析构函数, 再 内存释放  

	return 0;
}

看到不一样的地方了吗,我们在全局中重载了一个new关键字,然后再在主函数中调用全局new(::new),这时候,输出的结果是:

内存分配
构造函数。
析构函数。
内存释放。

令人欣喜的是,内存分配四个大字又出现在了我们眼中。ok,我们继续验证是否这个全局new在所有分配内存是不是都起效呢,我们再在主函数中动态分配一个数组,看看会有什么效果:

int main()
{
	char * c = new char[20];
	T * t = ::new T; // 先 内存分配 ,再 构造函数  
	delete t; // 先 析构函数, 再 内存释放  

	return 0;
}

编译结果如下:

内存分配
内存分配
构造函数。
析构函数。
内存释放。

结果出现了两个内存分配,这说明我们的想法是正确的。
4、placement new
  new分为两步走,先分配内存,后调用构造函数。我们自然而然可以想到,可不可以保持一块内存,反复构造析构,这样可以省略中间的多次分配内存。由于malloc内存会导致系统调用(malloc()之后,内核发生了什么),这可以节省大量的系统开销。C++标准也是这么想的,所以他们提供了placement new。
使用示例:

char* ptr = new char[sizeof(T)]; // allocate memory  
T* tptr = new(ptr) T;            // construct in allocated storage ("place")  
tptr->~T();                      // destruct  
delete[] ptr;                    // deallocate memory  

需要注意的是当对象使用定位new运算符的时候,需要显式地调用析构函数,就如上面代码tptr->~T(); 所示一样,这是因为定位new运算符不是在堆heap中储存数据,而是在缓冲区储存数据。
另外如果需要两个不重叠的内存空间,可以使用地址偏移量来解决,比如在上面代码加一点小小的修改:

char* ptr = new char[sizeof(T)]; // allocate memory  
T* tptr = new(ptr) T;            // construct in allocated storage ("place")  
T* tptr2 = new(ptr+sizeof(T)) T; 
tptr2->~T();
tptr->~T();                      // destruct  
delete[] ptr;                    // deallocate memory  

最后一点,我们需要注意正确的删除顺序,对于使用定位new运算符创建的对象,应该与创建的顺序相反的顺序进行删除,这是因为晚创建的对象可能依赖早创建的对象,然后当所有对象的析构函数完成之后,才能释放用于存储这些对象的缓冲区。

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值