主要介绍了C++的内存管理行为,即内存分配和内存归还(operator new和operator delete),同时了解new_handler和set_new_handler。
多线程里: 内存分配在heap上,多线程环境中经常会发生竞态条件,如果没有使用合理的同步机制而采用无锁算法或者精心防止并发访问,通常容易导致对heap的破坏。
stl里并不通过new和delete管理内存,而是有自己的分配器和回收器
operator new和operator delete适用于单一对象,Arrays一般使用operator new []和operator delete []
- set_new_handler可以指定内存分配无法满足时候的操作
- nothrow new 具有局限性,仅保证new操作不抛出异常,不保证同语句其他操作不抛出异常。
namespace std{
// 其实new_handler就是一个函数指针的名字,返回void,参数列表为空
typedef void (*new_handler)();
// 不抛出异常,设置全局 global new_handler
new_handler set_new_handler(new_handler p) throw();
};
当operator new无法满足操作时,会抛出异常,但是在这之前会调用错误处理函数new_handler(会一直调用new_handler,直到成功分配内存、返回异常、程序中断等)
实验:
void outOfMemery() {
printf("Out of Memeryn");
}
int main() {
set_new_handler(outOfMemery);
try {
int i = 0;
while (true) {
printf("------------------------%dn", i);
int *p = new int[1000000000L];
i++;
}
} catch (bad_alloc) {
printf("OVERn");
}
}
输出:
------------------------0 ------------------------1 ------------------------2 ------------------------3 ------------------------4 ------------------------5 ------------------------6 ------------------------7 Out of Memery Out of Memery Out of Memery Out of Memery Out of Memery ... 后面一直输出 Out of Memery
本实验中的new_handler只是打印消息,没做任何有效处理,因此会一直调用下去.
例子
这里使用了自定义的new_handler
void f() {
printf("testn");
exit(-1);
}
int main() {
set_new_handler(f);
try {
while (true) {
int *base = new int[100000000L];
}
} catch (bad_alloc) {
printf("BAD_ALLOCn");
}
}
set_new_handler(0)表示没有new_handler函数,会直接抛出异常
int main() {
set_new_handler(nullptr);
try {
while (true) {
int *base = new int[100000000L];
}
} catch (bad_alloc) {
printf("BAD_ALLOCn");
}
}
// BAD_ALLOC
new_handler应有的的行为
operator new 无法满足内存需求时,会不断调用new_handler,直到找到足够内存、抛出异常、退出,一般应该设计new_handler如下:
- 让更多的内存可被使用,以便下次operator new可以成功
- 安装另一个new_handler, 调用其他的new_handler(通过set_new_handler)
- 卸载new_handler, 即set_new_handler(nullptr),这样下次调用operator new就会直接抛出异常。
- 不返回,结束程序, 调用exit或者abort
C++中并没有原生支持类的专属new_handler,但是我们可以自己实现,函数原型如下:
class Widget {
private:
static new_handler currentHandler;
public:
static new_handler set_new_handler(new_handler p) throw();
// size_t 是默认参数,由编译器在编译器自动替换
static void *operator new(size_t sz) throw(bad_alloc);
};
new_handler Widget::currentHandler = nullptr;
Widget是一个例子类,将其专有的new_handler声明为static,保证所有的Widget类的new_handler都是共享的.
set_new_handler
参考std的实现模式,在类里的set_new_handler的工作如下:
- 将类的currentHandler设置为指定handler
- 返回之前的老handler(可以由global set_new_handler来用作恢复global new_handler处理,后面会提到)
new_handler Widget::set_new_handler(new_handler p) throw() {
new_handler oldHandler = currentHandler;
currentHandler = p;
return oldHandler;
}
global set_new_handler的工作原理和这个一样
NewHandlerHolder
使用RAII思想构建一个管理类NewHandlerHolder,使用构造函数保存set_new_handler返回的old new_handler,在离开代码块自动恢复之(利用析构函数)
class NewHandlerHolder {
public:
explicit NewHandlerHolder(new_handler nh) : handlerCache(nh) {}
~NewHandlerHolder() {
std::set_new_handler(handlerCache);
}
private:
new_handler handlerCache;
NewHandlerHolder(const NewHandlerHolder &) = delete;
NewHandlerHolder &operator=(const NewHandlerHolder &) = delete;
};
operator new
Widget::operator new的工作如下:
- 调用std::set_new_handler,将global new_handler设置为Widget::currentHandler
- 调用::operator new(即global operator new)来分配内存.
- 分配失败:调用global new_handler(已经被安装为Widget::new_handler),如果最终无法满足内存分配,会抛出bad_alloc异常,Widget::operator new必须恢复之前的global new_handler,然后再传播异常.
- 分配成功:返回一个足够Widget对象使用的内存,Widget::operator new会返回一个指针,指向其内存.然后恢复之前的global new_handler
- 为了方便的管理new_handler(需要恢复操作),使用构造函数保存set_new_handler返回的old new_handler,在离开代码块自动恢复之(利用析构函数)
void *Widget::operator new(size_t sz) throw(class std::bad_alloc) {
NewHandlerHolder holder(std::set_new_handler(currentHandler));
return ::operator new(sz);
// 离开代码块,holder析构,会自动重置new_handler
}
使用上面的类
void outOfMemery() {
printf("Out of Memeryn");
// abort();
}
int main() {
// 为Widget设置专属new_handler
Widget::set_new_handler(outOfMemery);
Widget *widget = new Widget; // 如果失败了会调用outOfMemery
string *str = new string; // 如果失败了会调用 global new_handler
Widget::set_new_handler(0);
Widget *widget1 = new Widget; // 如果失败会返回bad_alloc
}
使用模板来实现new_handler定制化
上面的实现方法没用泛用性,要对每个类都实现set_new_handler和operator_new方法,使用模板技术可以使之泛化
templateclass NewHandlerSupport { private: static std::new_handler currentHandler; public: static std::new_handler set_new_handler(std::new_handler nh) throw(); static void *operator new(std::size_t sz) throw(std::bad_alloc); }; template std::new_handler NewHandlerSupport ::currentHandler = nullptr; template std::new_handler NewHandlerSupport ::set_new_handler(std::new_handler nh) throw() { std::new_handler oldHandler = currentHandler; currentHandler = nh; return oldHandler; } template void *NewHandlerSupport ::operator new(std::size_t sz) throw(std::bad_alloc) { NewHandlerHolder holder(std::set_new_handler(currentHandler)); return ::operator new(sz); }
使用:令Widget继承自NewHandlerSupport
class Widget : public NewHandlerSupport{ private: // 类的成员,不必声明 new_handler public: // 不必再声明 operator new 和 set_new_handler };
这里用到的技术:
- 虽然是模板,但是其实并没有使用到T.因为这里其实要使用的模板的一个特性:自动为每一个T生成化一份实例,所以每个不同的类都会有其自己的静态对象.
- 怪异的循环模板模式(CRTP): class Widget : public NewHandlerSupport
Widget *p = new (std::nothrow) Widget;
其作用:
- 分配内存失败,会直接返回nullptr指针,不会抛出异常
- 分配成功,执行下一步操作
但是存在问题:虽然在operator new中不会抛出异常,但是同语句的Widget的构造函数中可能会抛出异常. 所以这个技术其实一般没被使用.
new和delete的合理替换时机 替换时机什么时候替换默认的operator new和operator delete是合理的呢?常见的场景如下:
检测运用上的错误
overruns:在分配的内存区后写数据
underruns:在分配的内存区前写数据
通过自定义operator new和operator delete可以解决这个问题:operator new可以预先分配超额内存,在额外空间放置byte patterns(即签名sign). operator delete在释放内存前检查这个空间,如果发生了overrun或者underrun,那么就可以做出对应的处理.
强化性能
现实中的内存分配需求比较多样:大块内存、小块内存、混合型内存,还有各种不同的生命周期,最后会造成内存区被分割成很多碎片。
因此可以对于内存分配进行定制化操作,可以大幅度提升性能,并且减少不必要的内存消耗。
搜集使用的统计数据
自定义的new可以帮助记录在分配内存中的关键信息,例如:分配的区块大小、是FIFO还是LIFO,运用形态等
一个协助检查overrun和underrun的operator new,这里用到了static_cast和reinterpret_cast,使用了int字符sign作为检查符,然后使用了static_cast将内存前部分存为int,使用了static_cast
static const int sign = 0xDEADBEEF;
typedef unsigned char Byte; // 一个字节
void *operator new(size_t sz) throw(bad_alloc) {
size_t realSize = sz + 2 * sizeof(int);
void *memory = malloc(realSize);
if (!memory) throw bad_alloc();
*static_cast(memory) = sign;
*reinterpret_cast(static_cast(memory) + realSize - sizeof(int)) = sign;
return static_cast(memory) + sizeof(int);
}
缺点
- 没有实现内存分配失败后反复调用new_handler,直到内存分配成功,
- 没有内存对齐
内存对齐
按系统来分,有些系统没有内存对齐会发生错误,有些系统内存对齐会提高性能:如X86里面,doubles可以被对齐于任意byte边界,但是如果是8-byte对齐,其访问速度会快很多。
C++要求operator new返回的指针有适当的对齐,malloc返回的指针就是对齐的,但是上面的偏移过的operator new并不保证.
合理替换new & delete的时机:
- 为了检测运用错误
- 为了搜集动态内存分配时的统计信息
- 增加分配和归还的速度:例如定制化内存池
- 降低默认内存管理器的额外空间开销:默认的内存分配器会在每个分配区上带来额外的开销
- 弥补默认分配器中的内存对齐
- 将相关对象集中成簇:某些数据通常被连续使用,可以将其分配在连续空间,降低内存缺页的开销。那么就可以将其创建在另一个heap上,这样就可以被成簇的集中在少量的内存页上。placement new和placement delete可以解决这个问题.
- 获得定制行为



