当operator new无法满足某一内存分配需求时,会抛出异常(当然可以使用nothrow版本),当operator new抛出异常以反映一个未获满足的内存需求之前,它会先调用一个错误处理函数new-handler(当然必须要先调用set_new_handler。
例如:
#includevoid outOfMem() { std::cerr << "Unable to satifsy request for memoryn"; std::abort(); } int main() { std::set_new_handler(outOfMem); int *pBigDataArray = new int[100000000L]; }
更多信息请查阅参考文献1的条款49:了解new-handler的行为(Understand the behavior of the new-handler)P240。
2.了解new和delete的合理替换时机Understand when it makes sense to replace new and delete.
替换标准库的operator new、operator delete常见的理由:
a. 将new所得内存delete时失败会导致内存泄漏(memory leaks);
b. 在new所得内存进行多次delete会导致不确定行为;
c. 由于各种编程错误导致数据”overruns“(写入点在分配区块之后)或”underruns“(写入点在分配区块起点之前)。【如果自定义一个operator new,便可超额分配内存,以额外空间(位于客户所得区块之前或后)放置特定的byte patterns】。operator delete便得以检查上述签名是否原封不动,若否就表示在分配区的某个时间点发生了overrunns或underruns,operator delete可以log事实以及发生问题的指针(个人理解,可以在operator delete时进行校验超额空间放置的特定的byte patterns是否发生了改变)。
编译器(标准库)提供的operator new和operator delete采取的是中庸之道,需要对每个任务都适度友好,但不对特定任何人有最佳表现。自定义的operator new和operator delete性能胜过缺省版本,定制版的速度比较快,有时候会快很多,而且所需内存比较少,最高可节省50%。对于有些程序而言将标准库的operator new和operator delete替换为自定义的版本是获得重大效能提升的办法之一。
2.3 为了收集使用上的统计数据在使用自定义的operator new和operator delete之前应该了解开发的软件如何使用动态内存。比如,分配区块的大小分布如何?寿命分布如何?它们倾向于以FIFO(先进先出)次序或LIFO(后进先出)次序或随机次序来分配和归还?它们的运用形态是否随时间改变,即软件在不同的执行阶段有不同的分配/归还形态吗?任何时刻所需的最大动态分配量是多少?使用自定义operator new和operator delete便于轻松搜集这些信息。
#pragma once #include#include #include #include static const int kSignature = 0xDEADBEEF; void* operator new(size_t size) { if(void* mem = malloc(size + 2 * sizeof(int))) { *((int*)mem) = kSignature; *((int*)((unsigned char*)mem + sizeof(int) + sizeof(unsigned char)*size)) = kSignature; std::cout << "malloc successfully" << std::endl; return (unsigned char*)mem + sizeof(int); } else { throw std::bad_alloc(); } } void operator delete(void *mem) noexcept { std::cout << "Entering self-define operator delete...n"; if(mem) { // 当然有其它方法校验overuns,比如在delete时根据申请内存的大小进行偏移进行主动校验 if((*(int*)((unsigned char*)mem - sizeof(int)) == kSignature)) { free((unsigned char*)mem - sizeof(int)); std::cout << "free successfully" << std::endl; } else{ std::cerr << "memory underruns!!!" << std::endl; } } } auto test_operator_new_main() -> void { std::cout << "testing operator new & operator delete for overruns&underruns..." << std::endl; int *ptr = new int; *((unsigned char*)ptr - 2) = 'P'; delete ptr; std::cout << "------------------------------" << std::endl; }
编译器cmake -G "Visual Studio 15 2017" -A x64 ..的结果(此运行结果符合设计预期):
testing operator new & operator delete for overruns&underruns... malloc successfully Entering self-define operator delete... memory underruns!!! ------------------------------
编译器cmake -G "MinGW Makefiles" ..的结果(此运行结果不符合设计预期):
testing operator new & operator delete for overruns&underruns... malloc successfully
由结果可知在不同编译器下结果有差异,"MinGW Makefiles"时应该时调用标准库的operator delete,并未使用自定义的版本。
2.4 为了检测运用错误如2.3节描述。
2.5 为了收集动态分配内存之使用统计信息如2.3节描述。
2.6 为了增加分配和归还的速度(标准库提供的)泛用型分配器往往(虽然并不总是)比自定义分配器慢,特别是当自定义分配器专门针对某特定类型对象而设计时。
2.7 为了降低缺省内存管理器带来的空间额外开销泛用型内存管理器往往(虽然并不总是)不仅比自定义分配器慢,而且往往还使用更多内存,因为它们常常在每一个分配区块上有一些额外开销。
2.8 为了弥补缺省分配器中的非最佳齐位在x86架构上double如果是8-byte对齐则访问速度最快,但是编译器自带的operator new并不保证动态分配的double采取8字节对齐。此时,将缺省的operator new替换为一个8字节版本可使得程序效率大幅提升。
2.9 为了将相关对象成簇集中如果有些数据往往一起使用,而你有希望处理这些数据时缺页异常的频率降至最低,那么创建一个heap就有意义,使用new和delete的placement版本就可以使得这些数据被成簇集中在尽可能少的内存页上。
2.10 为了获得非传统行为有时候希望operator new和operator delete做编译器默认版没做的事情。例如自定义的operator delete将归还的内容覆盖为0。
3.编写new和delete时需固守常规 3.1 处理0 bytes的内存申请C++规定,即使客户要求0 bytes,operator new也得返回一个合法指针,因此重载operator new时应当使其有能力处理0 byte申请。
在应对0 bytes的内存申请之前需要先介绍EBO,Empty Base Optimization空白基类最优化。即基类如果是空类,则其子类的大小是子类成员的大小。
#include#include #include namespace test_ebo { class Empty{}; class Empty2{}; class Derived1 : public Empty { }; class Derived2 : public Empty { private: char c_; }; class Derived3 : public Empty { private: int i_; }; class MultipleDerived : public Derived1 { }; class DerivedMultiBase : public Empty, public Empty2 { }; auto main() -> int { std::cout << "testing test_ebo......n" << std::endl; std::cout << "sizeof(Empty): " << sizeof(Empty) << std::endl; // 1 std::cout << "sizeof(Derived1): " << sizeof(Derived1) << std::endl; // 1 std::cout << "sizeof(Derived2): " << sizeof(Derived2) << std::endl; // 1 std::cout << "sizeof(Derived3): " << sizeof(Derived3) << std::endl; // 4 std::cout << "sizeof(MultipleDerived): " << sizeof(MultipleDerived) << std::endl; // 1 std::cout << "sizeof(DerivedMultiBase): " << sizeof(DerivedMultiBase) << std::endl; // 1 std::cout << typeid(MultipleDerived).name() << std::endl; std::cout << typeid(DerivedMultiBase).name() << std::endl; return 0; } }
cmake -G "MinGW Makefiles" ..编译,输出:
sizeof(Empty): 1 sizeof(Derived1): 1 sizeof(Derived2): 1 sizeof(Derived3): 4 sizeof(MultipleDerived): 1 sizeof(DerivedMultiBase): 1 N8test_ebo15MultipleDerivedE N8test_ebo16DerivedMultiBaseE
处理申请0 bytes的示例:
void* operator new(std::size_t size) {
if(0 == size) {
size = 1; // 将0-byte视为1-byte申请
}
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();
}
}
3.2注意operator new会被子类继承
class Base {
public:
static void* operator new(size_t size);
};
class Derived: public Base {}; // 假设Derived未声明operator new
Derived* p = new Derived; // 此处调用的是Base::operator new
如果Base专属的operator new并非设计用于Base的子类(实际上往往如此),那么应该在申请子类对象内存的时候使用标准库的operator new,像下面这样:
void* Base::operator new(size_t size) {
if(size != sizeof(Base)) { // 如果大小不是基类大小则使用标准库的operator new
return ::operator new(size);
}
// ...... // 否则在这处理
}
3.3更多operator new细节请移步Reference及上手实验
Reference
- Scott Meyers. Effective C++ 改善程序与设计的55个具体做法(第三版), 电子工业出版社,2011.



