菜鸟笔记
提升您的技术认知

new delete完全解析

在这一系列的第一篇文章中讲了 程序的内存结构 

在堆区的内存是要通过new delete系列函数来分配释放的

那么在new delete的背后 程序到底做了什么?又有哪些注意点?

本文将一一解密

new operator

1. 这个就是我们平常用到的new ,是一种操作符 关键字 不是一种函数

2.内部的原理是 调用operator new分配足够的空间,
并调用相关对象的构造函数
在全局命名空间中有一个自带的、隐藏的operator new专门用来分配内存。
然后返回指向这块内存的指针 
默认情况下编译器会将new这个关键字翻译成这个operator new和相应的构造函数。

3.不可以被重载

4.new、delete是C++中的操作符,而malloc和free是标准库函数。

 5.

 operator new

1.默认的operator new执行的操作是只分配所要求的空间,不调用相关对象的构造函数。
void *operator new[](size_t);     //allocate an array
void *operator new(size_t);     //allocate an object

2.可以被重载 ,
重载时,返回类型必须声明为void*,重载时,
第一个参数类型必须为表达要求分配空间的大小(字节),
类型为size_t,重载时,可以带其它参数
一般也要重载后对应的operator delete operator new[]、operator delete、operator delete[]

3.使用operator new分配的空间必须使用
operator delete来释放,而不能使用free,因为它们对内存使用的登记方式不同。反过来亦是一样

4.为什么要重载operator new ?
缺省的operator new和operator delete具有非常好的通用性,
它的这种灵活性也使得在某些特定的场合下,可以进一步改善它的性能。
尤其在那些需要动态分配大量的但很小的对象的应用程序里,

5.如果类中没有重载operator new,那么调用的就是全局的::operator new来完成堆的分配

6.使用示例
	

全局版本的重载
		#include<iostream>
#include<string>
using namespace std;

class X
{
public:
 X() {cout<<"constructor of X"<<endl;}
 ~X() {cout<<"destructor of X"<<endl;}
 void* operator new(size_t size,string str)
 {
   cout<<"operator new size"<<size<<"withstring"<<str<<endl;
   return ::operator new(size);
 }
 void operator delete(void* pointee)
 {
   cout<<"operator delete"<<endl;
   ::operator delete(pointee);
 }
private:
 int num;
}

int main()
{
  X *px=new("A new class") X;
  delete px;
  return 0;
}


类内版本的重载
		   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;  
    }  

placement new

1.placement new 是对operator new 的一个标准、全局的版本的重载
它自身不能被重载
(不像普通版本的operator new和operator delete能够被替换)。
void *operator new(size_t, void * p ) throw() { return p; }


2.使用方法
Widget * p = newWidget;                   //ordinary new
pi = new (ptr) int; pi = new (ptr) int;     //placement new
括号里的参数ptr是一个指针,它指向一个内存,
placement new将在这个内存上分配一个对象。
Placement new的返回值是这个被构造对象的地址(比如括号中的传递参数)。

3.placement new允许你在一个已经分配好的内存中(栈或者堆中)构造一个新的对象。原型中void* p实际上就是指向一个已经分配好的内存缓冲区的的首地址。


4.使用placement new  应用场景
增大时空效率的问题
使用new操作符分配内存需要在堆中查找足够大的剩余空间,显然这个操作速度是很慢的,而且有可能出现无法分配内存的异常(空间不够)。

placement new/delete 主要用途是:反复使用一块较大的动态分配成功的内存来构造不同类型的对象或者它们的数组。例如可以先申请一个足够大的字符数组,然后当需要时在它上面构造不同类型的对象或数组。placement new不用担心内存分配失败,因为它根本不分配内存,它只是调用对象的构造函数。

placement new的好处:
1)在已分配好的内存上进行对象的构建,构建速度快。
2)已分配好的内存可以反复利用,有效的避免内存碎片问题。


5.使用方法

第一步  缓存提前分配

有三种方式:

1.为了保证通过placement new使用的缓存区的memory alignment(内存队列)正确准备,使用普通的new来分配它:在堆上进行分配
class Task ;
char * buff = new [sizeof(Task)]; //分配内存
(请注意auto或者static内存并非都正确地为每一个对象类型排列,所以,你将不能以placement new使用它们。)

2.在栈上进行分配
class Task ;
char buf[N*sizeof(Task)]; //分配内存

3.还有一种方式,就是直接通过地址来使用。(必须是有意义的地址)
void* buf = reinterpret_cast<void*> (0xF00F);

第二步:对象的分配

在刚才已分配的缓存区调用placement new来构造一个对象。
Task *ptask = new (buf) Task

第三步:使用

按照普通方式使用分配的对象:

ptask->memberfunction();

ptask-> member;

//...

第四步:对象的析构

一旦你使用完这个对象,你必须调用它的析构函数来毁灭它。按照下面的方式调用析构函数:
ptask->~Task(); //调用外在的析构函数

第五步:释放

你可以反复利用缓存并给它分配一个新的对象(重复步骤2,3,4)如果你不打算再次使用这个缓存,你可以象这样释放它:delete [] buf;

跳过任何步骤就可能导致运行时间的崩溃,内存泄露,以及其它的意想不到的情况。如果你确实需要使用placement new,请认真遵循以上的步骤。


6.使用实例
	
#include <iostream>
using namespace std;

class X
{
public:
    X() { cout<<"constructor of X"<<endl;}
    ~X() { cout<<"destructor of X"<<endl;}

   void SetNum(int n)
    {
        num = n;
    }

   int GetNum()
    {
        return num;
    }

private:
    int num;
};

int main()
{
    char* buf = new char[sizeof(X)];
    X *px = new(buf) X;
    px->SetNum(10);
    cout<<px->GetNum()<<endl;
    px->~X();
    delete []buf;

   return 0;
}

delete operator

1.进行的工作 对指针指向的对象运行析构函数 调用operator delete函数释放该对象所用的内存


2.new/delete  new[]/delete[]要配套使用
这就涉及到上面一节没提到的问题了。上面我提到了在 new [] 时多分配 4 个字节的缘由,因为析构时需要知道数组的大小,但如果不调用析构函数呢(如内置类型,这里的 int 数组)?我们在 new [] 时就没必要多分配那 4 个字节, delete [] 时直接到第二步释放为 int 数组分配的空间。如果这里使用 delete pia;那么将会调用 operator delete 函数,传入的参数是分配给数组的起始地址,所做的事情就是释放掉这块内存空间。不存在问题的。

这里说的使用 new [] 用 delete 来释放对象的提前是:对象的类型是内置类型或者是无自定义的析构函数的类类型!

class A *pAa = new class A[3];
delete pAa;
那么 delete pAa; 做了两件事:

调用一次 pAa 指向的对象的析构函数;
调用 operator delete(pAa); 释放内存。
显然,这里只对数组的第一个类对象调用了析构函数,后面的两个对象均没调用析构函数,如果类对象中申请了大量的内存需要在析构函数中释放,而你却在销毁数组对象时少调用了析构函数,这会造成内存泄漏。

上面的问题你如果说没关系的话,那么第二点就是致命的了!直接释放 pAa 指向的内存空间,这个总是会造成严重的段错误,程序必然会奔溃!因为分配的空间的起始地址是 pAa 指向的地方减去 4 个字节的地方。你应该传入参数设为那个地址!

 


operator delete

1.void *operator delete(void *);    //free an object
void *operator delete[](void *);    //free an array