1、重载的缘由ios
用new建立动态对象时会发生两件事:(1)使用operatoe new()为对象分配内存(常常是调用malloc)(2)调用构造函数来初始化内存。相对应的调用delete运算符会(1)首先调用析构函数(2)调用operator delete()释放内存(常常是调用free)。咱们没法控制构造函数和析构函数的调用,是由编译器调用的。但能够改变内存分配函数operator new()和operator delete()。连接:C++中的new/new[],delete/delete[]编程
使用了new和delete的内存分配系统是为了通用目的而设计的,可是在特殊的情形下并不能知足须要。最多见的改变分配系统的缘由经常是出于效率考虑:数组
(1)增长分配和归还的速度。建立和销毁一个特定的类的很是多的对象,以致于这个运算成了速度的瓶颈。安全
(2)堆碎片。分配不一样大小的内存会在堆上产生不少碎片,以致于虽然内存可能还有,但因为都是碎片,找不到足够大的内存块来知足须要。经过为特定的类建立本身的内存分配器,能够确保这种状况不会发生。例如在嵌入式和实时系统里,程序可能必须在有限资源状况下运行很长时间,这样的系统就要求内存花费相同的时间且不容许出现堆内存耗尽或者出现不少碎片。数据结构
(3)检测运用上的错误。例如:new所得的内存,delete时却失败了,致使内存泄漏;在new所得内存上屡次delete,致使不肯定行为;数据"overruns”(写入点在分配内存区块尾端以后)或“underruns”(写入点在分配区块以前)。能够超额分配,而后在额外空间放置特定的byte patterns(签名)来进行检测。函数
(4)统计使用动态内存的信息。this
(5)为了下降缺省内存管理器带来的空间额外开销。spa
(6)为了弥补缺省分配器中的非最佳齐位。.net
(7)为了将相关对象成簇集中。例如,为了将特定的某个数据结构在仪器使用,而且使用时缺页中断频率降至最低。new和delete的palcement版本有可能完成这样的集簇行为。线程
(8)得到非传统的行为。例如:分配和归还共享内存。
2、重载全局的new和delete
一、重载了全局的new和delete后将使默认的new和delete不能载被访问,甚至在这个从新定义里也不能调用它们。
二、重载的new必须有一个size_t参数。这个参数由编译器产生并传递给咱们,它是要分配的对象的长度。函数返回值为一个大于等于这个对象长度的指针。
三、operator new()的返回值是一个void*,而不是指向任何特定类型的指针。所作的是分配内存,而不是完成一个对象创建——直到构造函数调用了才完成了对象的建立,它是编译器确保的动做,不在咱们的控制范围以内。
四、operator delete()的参数是一个指向由operator new()分配的内存的void*,而不是指向任何特定类型的指针。参数是一个void*是由于它是在调用析构函数后获得的指针。析构函数从存储单元里移去对象。operator delete()的返回类型是void。
五、功能示例:
#include <stdio.h>
#include<stdlib.h>
void *operator new(size_t sz)
{
printf("operator new:%d Bytes\n",sz);
void *m=malloc(sz);
if(!m)
puts("out of memory");
return m;
}
void operator delete(void *m)
{
puts("operator delete");
free(m);
}
class S
{
int i[100];
public:
S(){puts("S:S()");}
~S(){puts("~S::S()");}
};
int main( )
{
puts("(1)Creating & Destroying an int");
int *p=new int(47);
delete p;
puts("(2)Creating & Destroying an S");
S *s=new S;
delete s;
puts("(3)Creating & Destroying S[3]");
S *sa=new S[3];
delete []sa;
return 0;
}
输出:
函数说明:
(1)使用printf()和puts()而不是iostream,是由于建立一个iostream对象时(像全局的cin/cout/cerr),它们调用new去分配内存。用printf()不会进入死锁状态,由于它不会调用new来初始化自身。
(2)数组的输出为1204,而不是1200,说明额外的内存被分配用于存放它所包含对象的数量信息。
(3)输出状况说明都使用了由全局重载版本的new和delete。
说明:程序只是示范最简单的使用方法,具体的重载还有不少细节须要考虑,具体参考第四节“注意事项”
六、全局new实现伪码
void* operator new(std::size_t size)throw(std::bad_alloc)
{
using namespce std;
if(size==0)
{
size=1;
};
while(true)
尝试分配 size bytes;
if(分配成功)
return (一个指针指向分配得来的内存);
//分配失败:找出new_handling函数
new_handler globalHandler=set_new_handler(0);
set_new_handler(globalHandler);
if(globalHandler)(*globalHandler)();
else
throw std::bad_alloc();
}
七、全局delete实现伪码
void operator delete(void *mem)throw()
{
if(mem==0) return;
归还mem所指内存;
}
3、对类重载new和delete
一、为一个重载new和delete,尽管没必要显式地使用static,但实际上仍在建立static成员函数。
二、当编译器看到使用new建立自定义的类的对象时,它选择成员版本的operator new()而不是全局版本的new()。但若是要建立这个类的一个对象数组时,全局的operator new()就会被当即调用,用来为这个数组分配内存。固然能够经过为这个类重载运算符的数组版本,即operator new[]和operator delete[]来控制对象数组的内存分配。
三、使用继承时,重载了的类的new和delete不能自动继承使用。
四、功能示例():
#include<iostream>
using namespace std;
class Widget
{
int i[10];
public:
Widget(){cout<<"*";}
~Widget(){cout<<"~";}
void* operator new(size_t sz)
{
cout<<"Widget::new: "<<sz<<"bytes"<<endl;
return ::new char[sz];
}
void operator delete(void* p)
{
cout<<"Widget::delete "<<endl;
::delete []p;
}
void *operator new[](size_t sz)
{
cout<<"Widget::new[]: "<<sz<<"bytes"<<endl;
return ::new char[sz];
}
void operator delete[](void *p)
{
cout<<"Widget::delete[] "<<endl;
::delete []p;
}
};
int main( )
{
cout<<"(1-1) new Widget"<<endl;
Widget *w=new Widget;
cout<<"\n(1-2) delete Widget"<<endl;
delete w;
cout<<"\n(2-1)new Widget[25]"<<endl;
Widget *const wa=new Widget[25];
cout<<"\n(2-2)delete []Widget"<<endl;
delete []wa;
return 0;
}
结果:
程序分析:
(1)由于没有重载全局版本的operator new()和operator delete(),所以可使用cout
(2)语法上除了多一对括号外数组版本的new与delete与单个对象版本的是同样的。无论是哪一种状况,咱们都要决定要分配的内存的大小。数组版本的大小指的是整个数组的大小。
(3)从结果能够看出:都是调用的重载版本的,new先分配内存再调用构造函数,delete先调用析构函数而后释放内存。
说明:程序只是示范最简单的使用方法,具体的重载还有不少细节须要考虑,具体参考第四节“注意事项”。完整实现的伪码为:
六、member版本operator new和operator delete实现伪码
class Base
{
public:
static void *operator new(std::size_t size)throw(std::bad_alloc);
static void operator delete(void *rawMemory,std::size_t size)throw();
...
};
void *Base::operator new(std::size_t size)throw(std::bad_alloc)
{
if(size!=sizeof(Base))//sizeof(空类)为1,所以不须要判断size==0的状况
return ::operator new(size);
...
}
void Base::operator delete(void *rawMemory, std::size_t size)throw()
{
if(rawMemory==0)return;
if(size!=sizeof(Base))
{
::operator delete(rawMemory);
return;
}
归还rawMemory所指的内存;
return;
}
动态建立时,只需写Base *pBase=new Base;size的大小编译器会计算并传递给new。
4、注意事项
一、当重载operator new()和operator delete()时,咱们只是改变了原有内存分配方法(重载operator new()惟一须要作的就是返回一个足够大的内存块的指针)。编译器将用重载的new代替默认版本去分配内存并调用构造函数。
二、在重载的operator news内应该包含一个循环,反复调用某个new_handler函数。连接:C++ new_handler和set_new_handler
三、注意数据位对齐。C++要求全部的operator news返回的指针都有适当的对齐(取决于数据类型)。malloc就是在这样的要求下工做的,所以令operator new返回一个得自malloc的指针是安全的(调用malloc获得分配的内存块指针后,不要再对指针进行偏移)。
四、除非必须,不然不要本身重载new和delete。由于可能会漏掉可移植性、齐位、线程安全等细节。必须时,能够借鉴使用一些开放源码的标准库(例如Boost程序库的Pool)。
五、客户要求0 bytes时operator new也得返回一个合法的指针。一般的处理方法是当申请0字节是,将它视为申请1-bytes。即
if(size==0)
{
size=1;
}
六、重写delete时,要保证“删除null指针永远安全”。
七、对于某个特定类class X设计的operator new和operator delete一般是为大小恰好为sizeof(X)大小的对象而设计的。所以不要在继承类中使用该重载的new和delete。若是基类专属的operator new并不是被设计用来对付派生的状况,能够将“内存申请量错误”的调用行为改成标准的operator new。像下例
void *Base::operator new(std::size_t size)throw(std::bad_alloc)
{
if(size!=sizeof(Base))
return ::operator new(size);
...
}
一样的,若类将 大小有误的分配行为转交::operator new执行,则必须将大小有误的删除行为转交::operator delete执行。若是要删除的对象派生自某个base class然后者欠缺virtual析构函数,C++传给operator delete的size_t数值可能不正确。
void Base::operator delete(void *rawMemory, std::size_t size)throw()
{
if(rawMemory==0)return;
if(size!=sizeof(Base))
{
::operator delete(rawMemory);
return;
}
归还rawMemory所指的内存;
return;
}
5、placement new和placement delete
以上所示为普通的,只带size_t参数的重载,可是若是 想要在指定内存位置上放置对象;或者想要在new时志记信息则须要带额外参数。
一、示例:
#include<iostream>
using namespace std;
class X
{
public:
X(int ii=0):i(ii)
{
cout<<"this="<<this<<endl;
}
~X()
{
cout<<"X::~X():"<<this<<endl;
}
void *operator new(size_t,void *loc)
{
return loc;
}
int i;
};
int main( )
{
int arr[10];
cout<<"arr="<<arr<<endl;
X *xp=new(arr)X(47);//X at location arr
cout<<xp->i<<endl;
xp->~X();//显示调用析构函数
return 0;
}
输出:
函数说明:
(1)调用时,关键字new后时参数表(没有size_t参数,它由编译器处理),参数表后面是正在建立的对象的类名。
(2)operator new()仅返回了传递给它的指针。所以调用者能够决定将对象放置在哪里,这是在该指针所指的那块内存上,做为new表达式一部分的构造函数被调用。
(3)由于不是在堆上分配的内存,所以不能用动态内存机制释放内存。能够显示的调用析构函数(在其它状况下不要显示的调用析构函数)。
二、对于new,若是第一个函数(operator new)调用成功,第二个函数(构造函数)却抛出异常,则系统会调用第一个函数对应的operator delete版本,若没有则系统什么都不会作,会发生内存泄露。所以若重载一个带额外参数的operator new,那么也要定义一个带相同额外参数的operator delete。若是但愿这些函数有着日常行为,只要令专属版本调用global版本便可。
可是,若是没有在构造函数里抛出异常,则placement delete不会被调用。delete一个指针,调用的是正常形式(没有额外参数)的delete。
class Widget
{
public:
...
static void* operator new(std::size_t size,std::ostream &logStream)
throw(std::bad_alloc);
static void operator delete(void*pMem,std::ostream&logStream) throw();
static void operator delete(void* pMem)throw();
...
};
Widget *pw=new(std::cerr)Widget;
delete pw;
三、对于上述示例,须要注意避免让类专属的news掩盖其它的news。例如,对于上式:Widget *pw=new Widget;将被掩盖。一样道理,派生类中的operator news会掩盖global版本和继承而得的operator new版本。能够定义一个基类,包含全部正常形式的new和delete。而后本身定义的类继承这个基类,而且在类中中using声明取得标准形式。
参考资料:
一、《C++编程思想》
二、《Effective C++》