读者如果觉得我文章还不错的,文章可以转发,但是必须保留原出处和原作者署名。
今天我们主要学习知识点:
1.new的调用流程。
2.我们重载了new之后能干啥。
3.placement new干啥的。
4.set_new_handler是什么。
/****************************************************************************
**
** Copyright (C) 2019 [email protected]
** All rights reserved.
**
****************************************************************************/
/*
测试对象的new、delete,在VS2017更容易观察
*/
#ifndef obj_new_delete_h
#define obj_new_delete_h
#include
#include
#include
using std::cout;
using std::endl;
namespace obj_new_delete
{
class Obj
{
public:
Obj():mCount(0) { cout << "Obj ctor" << endl; }
~Obj() { cout << "~Obj dtor" << endl; }
private:
int mCount;
};
void test_new_obj()
{
Obj *obj = new Obj();
delete obj;
}
}
#endif // obj_new_delete_h
我们在Obj *obj = new Obj();处下个断点,再打开反汇编窗口,我摘取主要的代码。
markdown的汇编代码不能高亮,看的很难受啊,还是前往公众号可以获取更好的阅读体验。
; Obj *obj = new Obj();
00DE2C47 push 4
00DE2C49 call operator new (0DE141Ah)
00DE2C4E add esp,4
00DE2C51 mov dword ptr [ebp-0ECh],eax
00DE2C57 mov dword ptr [ebp-4],0
00DE2C5E cmp dword ptr [ebp-0ECh],0
00DE2C65 je obj_new_delete::test_new_obj+7Ah (0DE2C7Ah)
00DE2C67 mov ecx,dword ptr [ebp-0ECh]
; 调用对象的构造函数
00DE2C6D call obj_new_delete::Obj::Obj (0DE1456h)
我在::operator new的汇编代码处,点击菜单“转到源代码”,就可以还原为C++代码,这个代码的源文件叫做new_scalar.cpp:
void* __CRTDECL operator new(size_t const size)
{
for (;;)
{
// 这个有个小技巧,控制变量的作用域,没必要让其他对象见到这个变量的作用域
if (void* const block = malloc(size))
{
return block;
}
// _callnew内部会调用new_handler,
// 返回值为0表示new_handler类型函数为null,这样就不会调用new_handler类型函数,
// 抛出两个其中一个的异常,大小异常,内存分配异常。
// 否则就会调用设置的new_handler类型函数
if (_callnewh(size) == 0)
{
if (size == SIZE_MAX)
{
__scrt_throw_std_bad_array_new_length();
}
else
{
__scrt_throw_std_bad_alloc();
}
}
// The new handler was successful; try to allocate again...
}
}
到这里,我们基本上能够知道operator new调用过程。
从代码上看operator new做了两件事:
- 获取到新的内存。
- 调用对象的构造函数(从汇编代码看,这一步是编译器插入的,但是很多书上把这一步归为operator new。)
// 重载global new
void * operator new(size_t const size)
{
return malloc(size);
}
// 重载global delete
void operator delete(void *head)
{
return free(head);
}
class Obj
{
public:
Obj():mCount(0) { cout << "Obj ctor" << endl; }
~Obj() { cout << "~Obj dtor" << endl; }
// 重载局部 new
void *operator new (size_t const size)
{
return static_cast<Obj *>(::operator new(size));
}
// 重载局部delete
void operator delete(void *head)
{
return ::operator delete(head);
}
private:
int mCount;
};
其实重载operator new的代码很简单。
重载局部operator new,只需要将operator new作为类的普通成员变量就可以
重载全局operator new,只要在全局位置声明::operator new的实现函数
- 可以用来检测运用上的错误
- 可以提高效率,节省不必要的内存,提高回收和分配的速度(比如针对某一对象的内存池)
- 可以收集对内存使用的数据统计。
tips:
1.malloc(0)会返回一个正常的地址,但是这个地址不能存储任何东西。
在用户指定的内存上构建对象,这个过程不会申请新的内存,只会调用对象的构造函数即可。
看代码:
void test_placement_new()
{
char *buff = new char[sizeof(Obj)];
Obj *ojb = new(buff)Obj();
}
老套路,在VS 2017下转到反汇编的窗口。
; char *buff = new char[sizeof(Obj)];
00892558 push 4
0089255A call operator new[] (0891118h)
0089255F add esp,4
; eax中保存了new char返回的首地址
00892562 mov dword ptr [ebp-0E0h],eax
00892568 mov eax,dword ptr [ebp-0E0h]
0089256E mov dword ptr [buff],eax
; Obj *ojb = new(buff)Obj();
; 将eax中的首地址作为参数传递进去
00892571 mov eax,dword ptr [buff]
00892574 push eax
; Obj对象的大小
00892575 push 4
00892577 call operator new (0891550h)
0089257C add esp,8
0089257F mov dword ptr [ebp-0ECh],eax
00892585 mov ecx,dword ptr [ebp-0ECh]
; 调用Obj的构造函数
0089258B call obj_new_delete::Obj::Obj (0891456h)
00892590 mov dword ptr [ojb],eax
再调用operator new,我单步调试进去发现调用的operator new函数原型如下:
_Ret_notnull_ _Post_writable_byte_size_(_Size) _Post_satisfies_(return == _Where)
inline void* __CRTDECL operator new(size_t _Size, _Writable_bytes_(_Size) void* _Where) noexcept
{
(void)_Size;
return _Where;
}
- 函数原型为:inline void* __CRTDECL operator new(size_t _Size, void* _Where) noexcept,返回用户传入的地址。
- 调用类的构造函数(从汇编角度看,构造函数代码是编译器插入的,但是很多书上把这一步归为placement new)。
我举个例子说明:
你向vector中push了2个元素,此时vector的实际内存是为4个的。这是你再向vector中push一个元素,因为vector还有2个未用的空间,所以不需要申请内存。这样就可以在原来那块已经分配好的内存中调用元素的构造函数就可以了。而这里就恰恰用到了placement new了。
我摘抄SGI STL 3.0版本的里面的源代码供大家参考下
template <class T1, class T2>
inline void construct(T1* p, const T2& value) {
new (p) T1(value);
}
// vector push_back函数
void push_back(const T& x) {
if (finish != end_of_storage) {
construct(finish, x);
++finish;
}
else
insert_aux(end(), x);
}
class Obj
{
public:
Obj():mCount(0) { cout << "Obj ctor" << endl; }
~Obj() { cout << "~Obj dtor" << endl; }
// 重载内置版本placement new
void *operator new(size_t size, void *address)
{
cout << "void *operator new(size_t size, void *address) version"
<< endl;
return address;
}
// 重载内置版本placement delete
void operator delete(void *buff, void *address)
{
return ::operator delete(buff, address);
}
// 重载placement new
void *operator new(size_t size, void *address, long extra)
{
cout << "void *operator new(size_t size, void *address, long extra) "
" version"
<< " extra:" << extra <<endl;
return address;
}
// 重载placement delete
void operator delete(void *buff, void *address, long extra)
{
return ::operator delete(buff, address);
}
private:
int mCount;
};
void test_placement_new()
{
char *buff = new char[sizeof(Obj)];
// Obj *ojb = new(std::cerr)Obj();
Obj *obj = new(buff)Obj();
obj->~Obj();
Obj *obj_1 = new(buff, 123456)Obj();
obj_1->~Obj();
delete buff;
// 打印结果
// void *operator new(size_t size, void *address) version
// Obj ctor
// ~Obj ctor
// void *operator new(size_t size, void *address, long extra) version extra:123456
// Obj ctor
// ~Obj ctor
}
1.如果你重载了placement new,那么一定要重载对应的placement delete。因为在对象的构造函数抛出异常时,C++运行时系统需要找到对应的placement delete去释放内存。否则可能有内存泄露的风险。
2. 重载了placement new会隐藏正常operator new导致编译语法错误。如何避免后面我有机会写出完整的代码。
3. 使用了placement new,需要对内存的释放要注意,并不能直接delete。因为这块内存并不是你来申请的,你应该直接调用你自己对象的析构函数。
首先它是个位于std命名空间的全局函数,参数类型为函数指针 void (*new_hander)()
当用户使用operator new无法返回正确的内存地址,这时C++编译器就会调用一个客户指定的错误处理函数。这个错误处理函数就是通过set_new_handler来指定的。
- new_handler让更多内存可以被使用。
- 如果不能获取到更多有用的内存抛出异常或者直接终止程序运行。(必须要这样做,因为new包含是死循环操作)
写一个实例代码:
void new_exception_handler()
{
cout << "memory cout..." << endl;
abort();
}
void test_new_obj()
{
// 测试new_set_handler
std::new_handler old_handler = std::set_new_handler(new_exception_handler);
while (true) {
new int[100000000ul];
}
cout << "end..." << endl;
// memory cout...
}
看到operator new的源代码,会发现原理很简单,本质上是对malloc的封装。从汇编代码看,operator new其实返回malloc返回的内存,而构造函数的代码其实编译器插入的。当调用operator new发生异常时,C++运行时系统会负责回收内存。
operator new重载作用非常的大,其中最重要的也是最常见的就是内存池实现。
placement new在某一些场景还是非常有用的。如果重载了placement new,那么必须也需要重载placement delete。
set_new_handler就是设置一个全局的回调函数,在operator new异常情况下就会调用我们设置的new_handler类型函数。