Cpp Tutorials-02a-内存管理(深入理解 new)

使用 new & delete

@tldr: C++中的new, operator new, placement new:

cplusplus.com :
operator new can be called explicitly as a regular function, but in C++, new is an operator with a very specific behavior: An expression with the new operator, first calls function operator new (i.e., this function) with the size of its type specifier as first argument, and if this is successful, it then automatically initializes or constructs the object (if needed). Finally, the expression evaluates as a pointer to the appropriate type.

new(new operator) 作用是申请内存和执行构造函数 A *a = new A();,分两步:

  1. operator new 只负责申请内存
  2. placement new 调用构造函数

区分 new、operator new、placement new:

  • A* pA = new A();
  • void *p = ::operator new(100);
  • T* pA = new(ptr) A();

new(new operator)

  • new(new operator): 调用 operator new 分配内存,然后调用构造函数
  • new 对应的删除操作是 delete operator, 与new相反,delete 调用析构函数并调用 operator delete 来释放内存
A* a = new A();
delete a;

通过反汇编可以看出A* = new A会被gcc解析成operator new(sizeof(A))A()两个步骤, delete a被解析为~A()operator delete(a)两个步骤。

➤ 使用 new operator 分配数组,分两种情况:

  • 对基本类型使用: char* buf = new char[N] 它将转换为对函数 operator new 的调用
  • 对 class 类型使用:A* ptr = new A[N] 分两个步骤:
    • 调用 operator new[] 函数完成内存分配
    • 在申请的空间上执行 N 次构造函数
  • delete operator 与上面类似,deletenew 配合使用,delete[]new[] 配合使用

operator new

  • operator new指对new的重载形式,它是一个函数,并不是运算符。只负责分配内存而不会调用构造, 对于operator new来说,分为全局重载和类重载:

    • 全局重载: void* ::operator new(size_t size)
    • 类中重载: void* A::operator new(size_t size), 注意operator new的参数是size_t, 返回是void指针
  • operator new()完成的操作一般只是分配内存,事实上系统默认的全局::operator new(size_t size)也只是调用malloc分配内存,并且返回一个void*指针;

  • 对应的删除operator delete: operator delete(buf); 只删除内存空间;

➤ 如何调用 operator new:

void *p = ::operator new(100); // 指定调用全局的operator new , 而不是类自己重载的版本
void *p = operator new(sizeof(int));

➤ operator new 有三种形式的函数重载:

  • (1)throwing: void* operator new (std::size_t size) throw (std::bad_alloc);
  • (2)nothrow: void* operator new (std::size_t size, const std::nothrow_t& nothrow_value) throw();
  • (3)placement: void* operator new (std::size_t size, void* ptr) throw();

A* a = new A;
这句代码里的new(new operator),先是调用了throwing版本的 operator new 分配内存, 然后调用构造;

A* a = new(std::nothrow) A;
new operator 先调用 nothrow 版本的operator new, 然后调用构造;

placement版本的operator new,它也是对operator new的一个重载,定义于<new>中, 它多接收一个ptr参数,但它只是简单地返回ptr, 内部什么都没有做, 当使用 placement new expression 的时候会调用这个版本的operator new

重载::operator new

Effective C++ 第三版第 50 条列举了定制 new/delete 的几点理由

  • 检测代码中的内存错误
  • 优化性能
  • 获得内存使用的统计数据
  1. 不改变operator new的默认参数重载: 用这种方式的重载,使用方不需要包含任何特殊的头文件,也就是说不需要看见这两个函数声明。“性能优化”通常用这种方式。
void* operator new(std::size_t sz) {
std::printf("global op new called, size = %zu\n",sz);
return std::malloc(sz);
}
void operator delete(void* ptr) noexcept {
std::puts("global op delete called");
std::free(ptr);
}
int main() {
// 以下调用自定义的operator new/delete:
int* p1 = new int;
delete p1;

int* p2 = new int[10];
delete[] p2;
}
  1. 增加新的参数的operator new, 为了跟踪内存分配的错误
void* operator new(size_t size, const char* file, int line);  // 其返回的指针必须能被普通的 ::operator delete(void*) 释放
void operator delete(void* p, const char* file, int line); // 这个函数只在析构函数抛异常的情况下才会被调用

// new (__FILE, __LINE__)会调用构造函数吗?
Foo* p = new (__FILE, __LINE__) Foo;

重载class::operator new

struct X {
static void* operator new(std::size_t sz){
std::cout << "custom new for size " << sz << '\n';
return ::operator new(sz);
}
static void* operator new[](std::size_t sz){
std::cout << "custom new for size " << sz << '\n';
return ::operator new(sz);
}
};
int main() {
// 以下会调用类成员的operator new/delete
X* p1 = new X;
delete p1;
X* p2 = new X[10];
delete[] p2;
}

重载时的优先顺序

在使用 new运算符分配类类型的对象时(如果该类重载了operator new),将调用该类的operator new。
在使用 new运算符分配内置类型的对象、未重载operator new函数的类类型的对象、任何类型的数组时,将调用全局operator new 函数。

new_handler

operator new失败, 会调用new_handler, 如果new_handler不存在则抛出一个std::bad_alloc异常,
std::set_new_handler可以为当前operator new指定一个new_handler

typedef void (*p_new_handler)();
std::set_new_handler(p_new_handler);
int* pBigDataArray = new int[1000000000000L];

如何设计一个良好的new_handler ? 《Effective C++》建议以下几种做法(选1即可):

  1. 让更多的内存可以被使用(也就是清理内存,让出更多的空间给这里的内存分配操作)
  2. 安装另一个new_handler(当这个new_handler无法处理当前分配失败的情况时,我们可以装在另外一个new_handler试图处理这种情况)
  3. 卸载new_handler(如果当前的new_handler确实无法处理当前错误,那么就将当前的new_handler卸载,例如nullptr,让new抛出bad::alloc的异常)
  4. 直接抛出bad::alloc的异常
  5. 调用abort()或exit()直接终止程序

placement new expression

char* ptr = new char[sizeof(T)]; // 分配内存
T* tptr = new(ptr) T("hello"); // 在已分配内存进行构造
tptr->~T(); // 析构
delete[] ptr;

第二行的new(ptr) T("hello")会调用operator new的placement形式

参考

new 和 delete 的内部实现

new 表达式

源码位置 gcc/libstdc++-v3/libsupc++/new_op.cc

_GLIBCXX_WEAK_DEFINITION void *
operator new (std::size_t sz) _GLIBCXX_THROW (std::bad_alloc)
{
void *p;

/* malloc (0) is unpredictable; avoid it. */
if (sz == 0)
sz = 1;

while (__builtin_expect ((p = malloc (sz)) == 0, false))
{
new_handler handler = std::get_new_handler ();
if (! handler)
_GLIBCXX_THROW_OR_ABORT(bad_alloc());
handler ();
}

return p;
}

operator new

➤ 全局 operator new 和 operator delete 源码:

void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
// try to allocate size bytes
void *p;
while ((p = malloc(size)) == 0) //申请空间
if (_callnewh(size) == 0) //若申请失败则调用处理函数
{
// report no memory
static const std::bad_alloc nomem;
_RAISE(nomem); // #define _RAISE(x) ::std:: _Throw(x) 抛出nomem的异常
}
return (p);
}

void operator delete(void* pUserData)
{
_CrtMemBlockHeader* pHead;

RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));
if (pUserData == NULL)
return;
_mlock(_HEAP_LOCK); /* block other threads */
__TRY

/* get a pointer to memory block header */
pHead = pHdr(pUserData);

/* verify block type */
_ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));

_free_dbg(pUserData, pHead->nBlockUse);

__FINALLY
_munlock(_HEAP_LOCK); /* release other threads */
__END_TRY_FINALLY

return;
}

可以看出 operaotr new 和 malloc 的不同:如果分配失败,前者抛异常,后者返回null;