查看原文
其他

内存管理:new and delete

月踏 知知爸爸是码农 2022-06-13

前几篇文章看了malloc的实现,本文再来看下new和delete,每个C++程序员对它们应该都比较熟悉,在C++11的智能指针出现之前基本都是靠它们来直接管理内存,本文不讲它们最基本的用法,着重看下代码实现和一些平时使用可能注意不到的细节

一、new/delete expression

大家可能都知道在用new expression创建对象时,是先申请内存空间,然后在申请的空间上构造对象,delete expression做的事情正好相反,先析构对象,然后再释放空间,下面通过实际的例子来验证上面所说的过程,先写一段简单的C++示例代码:

class Foo {public: Foo(int a, int b) : a_(a), b_(b) {} ~Foo() {}
private: int a_; int b_;};
int main(int argc, char** argv) { auto *p = new Foo(100, 200);  delete p; return 0;}

使用正确命令编译上面代码之后,使用gdb调试执行,先用b main命令给main函数设断点,然后使用disassemble /m main来反汇编main函数,/m的作用是同时输出汇编代码对应的C++代码

下面是new expression对应的汇编代码,可以看到里面有两次函数调用,第一次是调用了operator new这个函数(使用c++filt _Znwm可以得到这个C++符号修饰前的函数名),第二次是调用了Foo的构造函数:

// auto *p = new Foo(100, 200);mov    $0x8,%edicallq  0x6a0 <_Znwm@plt>mov    %rax,%rbxmov    $0xc8,%edxmov    $0x64,%esimov    %rbx,%rdicallq  0x880 <Foo::Foo(int, int)>mov    %rbx,-0x18(%rbp)

下面是delete expression对应的汇编代码,里面同样有两次函数调用,第一次是调用了Foo的析构函数,第二次是调用了operator delete函数(使用c++filt _ZdlPvm可以得到这个C++符号修饰前的函数名):

// delete p;mov    -0x18(%rbp),%rbxtest   %rbx,%rbxje     0x816 <main(int, char**)+81>mov    %rbx,%rdicallq  0x8a4 <Foo::~Foo()>mov    $0x8,%esimov    %rbx,%rdicallq  0x6b0 <_ZdlPvm@plt>

通过上面的反汇编示例代码,可以验证我们这一节开始所说的new/delete expression的实际步骤是正确的

二、operator new/delete

在上面一节中,通过反汇编示例代码,可以看到分配和释放内存空间用的是operator new/delete函数,我们直接看C++标准库中它们的代码实现,下面先看一部分它们的定义(这两个函数有多个重载版本,不用都列出来):

// https://github.com/RTEMS/gnu-mirror-gcc/blob/master/libstdc++-v3/libsupc++/new_GLIBCXX_NODISCARD void* operator new(std::size_t) _GLIBCXX_THROW (std::bad_alloc) __attribute__((__externally_visible__));_GLIBCXX_NODISCARD void* operator new[](std::size_t) _GLIBCXX_THROW (std::bad_alloc) __attribute__((__externally_visible__));  void operator delete(void*) _GLIBCXX_USE_NOEXCEPT __attribute__((__externally_visible__));void operator delete[](void*) _GLIBCXX_USE_NOEXCEPT __attribute__((__externally_visible__));

其中operator new的一个实现如下:

// https://github.com/RTEMS/gnu-mirror-gcc/blob/master/libstdc%2B%2B-v3/libsupc%2B%2B/new_op.cc_GLIBCXX_WEAK_DEFINITION void *operator new (std::size_t sz) _GLIBCXX_THROW (std::bad_alloc) {  void *p; if (__builtin_expect (sz == 0, false))    sz = 1;  while ((p = malloc (sz)) == 0) {   new_handler handler = std::get_new_handler ();   if (! handler)     _GLIBCXX_THROW_OR_ABORT(bad_alloc());    handler ();  } return p;}

从上面代码可以看到operator new函数先处理了sz为0的特殊情况,实际的分配过程是通过malloc来实现的,分配成功则返回,分配失败则进入一个while循环,循环内部调用相应的handler来处理失败,其中new_handler的定义如下,它是一个函数指针:

// https://github.com/RTEMS/gnu-mirror-gcc/blob/master/libstdc++-v3/libsupc++/newnamespace std {typedef void (*new_handler)();new_handler set_new_handler(new_handler) throw();#if __cplusplus >= 201103Lnew_handler get_new_handler() noexcept;#endif} // namespace std

再看operator delete的一个实现,这个实现很简单,就是简单的调用了free函数:

// https://github.com/RTEMS/gnu-mirror-gcc/blob/master/libstdc%2B%2B-v3/libsupc%2B%2B/del_op.cc_GLIBCXX_WEAK_DEFINITION voidoperator delete(void* ptr) noexcept {  free(ptr);}

四、placement new/delete

placement new唯一的工作就是在内存上运行构造函数,同理,placement delete唯一的工作就是在内存上运行析构函数,直接看placement new/delete所调用的operator new/delete在标准库中的实现,operator new/delete实际上什么也没做:

// https://github.com/RTEMS/gnu-mirror-gcc/blob/master/libstdc%2B%2B-v3/libsupc%2B%2B/new// Default placement versions of operator new._GLIBCXX_NODISCARD inline void* operator new(std::size_t, void* __p) _GLIBCXX_USE_NOEXCEPT{ return __p; }_GLIBCXX_NODISCARD inline void* operator new[](std::size_t, void* __p) _GLIBCXX_USE_NOEXCEPT{ return __p; }
// Default placement versions of operator delete.inline void operator delete (void*, void*) _GLIBCXX_USE_NOEXCEPT { }inline void operator delete[](void*, void*) _GLIBCXX_USE_NOEXCEPT { }

既然placement new/delete所调用的operator new/delete什么也没做,那么placement new/delete有什么实际用处呢?实际上它们的作用很大,因为可以在指定的地址空间构造对象,这种灵活性可以在一定程度上提高程序性能,下面举个标准库的例子来说明

vector的实际可用空间一般比已用空间大,我们这里简单理解为是两倍关系,push_back函数在push对象的时候,如果发现还有可用空间,就会在这部分可用空间上直接用placement new来构造对象,下面是C++标准库关键的代码实现和出处,我精简了代码,关键的地方加了注释解释:

// 1. 先看push_back的关键代码// https://github.com/RTEMS/gnu-mirror-gcc/blob/master/libstdc++-v3/include/bits/stl_vector.hvoid push_back(const value_type& __x) { // 判断是不是还有可用空间, // 还有的话就调用_Alloc_traits::construct来构造对象 if (this->_M_impl._M_finish != this->_M_impl._M_end_of_storage) { 。。。    _Alloc_traits::construct(this->_M_impl, this->_M_impl._M_finish, __x);    。。。  } else { 其它方法处理 }}
// 2. 再看_Alloc_traits::construct的代码实现// https://github.com/RTEMS/gnu-mirror-gcc/blob/master/libstdc++-v3/include/ext/alloc_traits.htemplate<typename _Ptr, typename... _Args>static std::__enable_if_t<__is_custom_pointer<_Ptr>::value>construct(_Alloc& __a, _Ptr __p, _Args&&... __args) {  // 由调用了另一个函数,其中_Base_type是一个typedef:  // typedef std::allocator_traits<_Alloc> _Base_type; _Base_type::construct(__a, std::__to_address(__p), std::forward<_Args>(__args)...);}
// 3. 再看_Base_type::construct的实现// https://github.com/RTEMS/gnu-mirror-gcc/blob/master/libstdc++-v3/include/bits/alloc_traits.htemplate<typename _Tp, typename... _Args>static auto construct(_Alloc& __a, _Tp* __p, _Args&&... __args) {  // 这里又调了另一个函数_S_construct _S_construct(__a, __p, std::forward<_Args>(__args)...); }
template<typename _Tp, typename... _Args>static auto _S_construct(_Alloc&, _Tp* __p, _Args&&... __args) {  // 终于追踪到了目标代码,调用了placement new来构造对象! ::new((void*)__p) _Tp(std::forward<_Args>(__args)...); }

通过上面的实际代码的追踪,我们可以看到vector在push_back一个新元素的时候,在内部空间还足够的情况下,会选择使用placement new来在指定地址上直接构造对象,这样就省去了内存分配的开销。

因为可以在指定的地址构造对象,这样我们也可以用new expression在栈上构造对象了,这在某些特殊场景中可能也会有用,大家自己发挥想象吧^^

五、为什么有时候需要重载operator new/delete

对于new/delete expression底层调用的operator new/delete函数,为什么有时候我们需要重载它们呢?我觉得大概有下面几个原因

1. 为特定的类定制operator new/delete实现。以本文最开始的C++ Foo类代码示例为例,可以在类的内部添加下面两个operator new/delete自定义函数:

// 下面函数内部的。。。表示添加的定制化的功能class Foo { static void *operator new(size_t size) {    。。。 void *p = ::operator new(size); 。。。 return p; }
static void operator delete(void *ptr) { 。。。 ::operator delete(ptr); 。。。 }}

2. 可以添加感兴趣的log信息、debug信息等。比如可以在重载版本里面记录时间、调用次数等log信息,也可以为申请的size额外在头尾各增加若干字节的空间,用来存储某个特定的magic number,这样如果程序发生crash,可以通过check这些magic number是否发生变化来在一定程度上排除一些内存使用错误。

3. 标准库的内存管理主要用于一般目的,它对于大块申请、小块申请、大小混合申请、长时间执行的应用、多线程的应用、单线程的应用、数据库的应用等等等等无数的应用场景都要有还算不错的性能,所以在某些单一情景比如都是大块申请、程序确定是单线程等等情况未必是最佳实现,在这种情况下,大家自然可以根据实际情况实现一个更高性能的版本来替代标准库的内存管理,这时候直接重写operator new/delete,里面不用再调用C标准库的malloc,换成自己的内存管理算法即可。

六、summary and reference

本文从标准库代码实现的角度讲了new/delete expression所对应的底层内存分配operator new/delete的实现,但是new/delete expression除了有内存分配/释放的工作之外,还要调用类的构造/析构函数,我想这部分代码应该是编译器生成的,本来想在GCC代码中找一下这部分的生成代码是在哪,但是木有找到。。。大家如果知道的话,可以留言或者私信告诉我,感谢~

本文主要参考了cppreference和标准库的实现:

  • https://en.cppreference.com/w/cpp/language/new

  • https://en.cppreference.com/w/cpp/language/delete

  • https://en.cppreference.com/w/cpp/memory/new/operator_new

  • https://en.cppreference.com/w/cpp/memory/new/operator_delete

  • https://github.com/RTEMS/gnu-mirror-gcc/blob/master/libstdc++-v3/libsupc++/new

  • https://github.com/RTEMS/gnu-mirror-gcc/blob/master/libstdc%2B%2B-v3/libsupc%2B%2B/new_op.cc

  • https://github.com/RTEMS/gnu-mirror-gcc/blob/master/libstdc%2B%2B-v3/libsupc%2B%2B/del_op.cc



您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存