C++ | 源码分析 Why double free?

源码分析 Why double free?

What

前几天,同事让帮忙看一段代码,问为什么程序报错了

free(): double free detected in tcache 2
Aborted (core dumped)

源代码如下:

#include <algorithm>
#include <iostream>
#include <vector>
#include<string.h>
#include <unistd.h>
class s_data {
public:
    int a;
    int b;
    int *p;
    s_data() {
        a = 1;
        b = 2;
        p = new int;
        p[0] = 3;
    }
    ~s_data() {
        delete p;
    }
};

int main() {
    std::cout << sizeof(s_data) << " " << sizeof(int) << " " << sizeof(int*) << std::endl;
    s_data t1;
    s_data t2;
    std::vector<s_data> vv;
    vv.push_back(t1);
    vv.emplace_back(t2);
    std::cout << vv[0].a << " " << vv[0].b << " " << vv[0].p[0] << std::endl;
    std::cout << vv[1].a << " " << vv[1].b << " " << vv[1].p[0] << std::endl;
}

Why

一般double free的问题都是释放指针内存导致的,double 就是多次释放了。

而代码中释放的时候,就是在主程序结束的时候,说明有多个指针指向了同一片内存,然后导致了多次释放的问题。

因为有多个指针指向同一片内存,这就是存在指针的复制

而代码中存在复制的地方就是第27行和第28行,vector push_backemplace_back

为什么看起来没有问题的push_back,只拷贝了指针的地址?

这就是浅拷贝的发生。

1.浅拷贝 VS 深拷贝

浅拷贝

只复制对象指针,即按位拷贝对象,如果拷贝基本类型,会拷贝基本类型的值;如果拷贝的是内存地址或引用类型,拷贝的是内存地址,并不复制对象本身内容,不开辟新内存,拷贝前后对象共同指向同一块内存。相当于share_ptr 中多个指针共享同一片内存。

深拷贝

深拷贝会创建一个新的对象,该对象与原对象各自拥有独立的内存

深拷贝时会递归拷贝所有对象属性和数组元素,拷贝属性指向的动态分配内存。

深拷贝比浅拷贝速度慢,且内存开销较大。

如果类没有定义拷贝构造(Copy constructor)函数,编译器会隐式地(隐式表示如果不被使用则不生成)生成Copy constructor 函数,在Copy constructor函数中对成员变量执行类似于memcpy的按位复制。对于指针变量,仅仅复制其内存地址,并不会新开辟内存空间。因此,执行默认拷贝函数后,指针成员变量会指向同一块堆内存。

2.push_back 和 emplace_back

push_back和 emplace_back 都会发生拷贝构造(Copy constructor)的情况,当将元素 push_back 或 emplace_back 到 vector 中去的时候,vector 都要先创建1个该对象,然后通过拷贝构造函数将当前元素赋值给创建出来的对象。

让我们源码见。

push_back 源码

该函数创建一个元素在 vector 的末尾并分配给定的数据

void push_back(const value_type &__x) {
    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);
        ++this->_M_impl._M_finish; // 更新当前容器内元素数量
    } else
        // 如果满了,那么就重新申请空间,然后拷贝数据,接着插入新数据 __x
        _M_realloc_insert(end(), __x);
}

// 如果 C++ 版本为 C++11 及以上(也就是从 C++11 开始新加了这个方法),使用 emplace_back() 代替
#if __cplusplus >= 201103L
void push_back(value_type &&__x) {
    emplace_back(std::move(__x));
}
#endif
// __x 要添加的数据。
// 这是典型的堆栈操作。 
  void
  push_back(const value_type& __x)
  {
// 首先判断容器是否有剩余空间,如果不够,则先增加空间,然后尾部构造元素
if (this->_M_impl._M_finish != this->_M_impl._M_end_of_storage)
  {
    // 空间不够就扩容
    _GLIBCXX_ASAN_ANNOTATE_GROW(1);
    _Alloc_traits::construct(this->_M_impl, this->_M_impl._M_finish,
                 __x);
    ++this->_M_impl._M_finish; // 调整水位高度
    _GLIBCXX_ASAN_ANNOTATE_GREW(1);
  }
else // 如果有则构造新的元素,然后尾部插入新的元素
  _M_realloc_insert(end(), __x);
  }

#if __cplusplus >= 201103L
  void
  push_back(value_type&& __x)
  { emplace_back(std::move(__x)); } // C++ 11后 push_back就是 emplace_back
  • 当容器空间不够时:

    容器就开始扩容,扩容大小为 m a x ( 旧长度 × 2 , ( 旧长度 + n 个新增元素 ) × 2 ) max(旧长度 \times 2,(旧长度 + n个新增元素) \times 2) max(旧长度×2(旧长度+n个新增元素)×2)

    使用 _Alloc_traits::construct创建一个对象,看下源码

          template<typename _Tp, typename... _Args>
    	static auto construct(_Alloc& __a, _Tp* __p, _Args&&... __args)
    	noexcept(noexcept(_S_construct(__a, __p,
    				       std::forward<_Args>(__args)...)))
    	-> decltype(_S_construct(__a, __p, std::forward<_Args>(__args)...))
    	{ _S_construct(__a, __p, std::forward<_Args>(__args)...); }
    
    

    主要函数是:_S_construct(__a, __p, std::forward<_Args>(__args)...);

    其中std::forward<_Args>(__args)...)函数的作用是__t对象转换为 目标__Tp对象

      template<typename _Tp>
        constexpr _Tp&&
        forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
        {
          static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"
    		    " substituting _Tp is an lvalue reference type");
          // 将 __T 转换为 _Tp对象
          return static_cast<_Tp&&>(__t);
        }
    

    然后构造出的对象传入 _S_construct构造函数,该构造函数作用很清晰,将forward构造出来的对象,在__p位置上创建出1个新的对象,然后通过_Tp拷贝构造函数传递给 该位置上新的_Tp对象。

      template<typename _Tp, typename... _Args>
    static
    _Require<__and_<__not_<__has_construct<_Tp, _Args...>>,
                   is_constructible<_Tp, _Args...>>>
    _S_construct(_Alloc&, _Tp* __p, _Args&&... __args)
    noexcept(std::is_nothrow_constructible<_Tp, _Args...>::value)
    { ::new((void*)__p) _Tp(std::forward<_Args>(__args)...); } // 就是拷贝构造函数!
    
  • 当容器空间充足时:

    此时在vector数据尾部插入 __x_M_realloc_insert(end(), __x);

    #if __cplusplus >= 201103L
      template<typename _Tp, typename _Alloc>
        template<typename... _Args>
          void
          vector<_Tp, _Alloc>::
          _M_realloc_insert(iterator __position, _Args&&... __args)
    #else
      template<typename _Tp, typename _Alloc>
        void
        vector<_Tp, _Alloc>::
        _M_realloc_insert(iterator __position, const _Tp& __x)
    #endif
        {
          const size_type __len =
    	_M_check_len(size_type(1), "vector::_M_realloc_insert");
          pointer __old_start = this->_M_impl._M_start;
          pointer __old_finish = this->_M_impl._M_finish;
          const size_type __elems_before = __position - begin();
          pointer __new_start(this->_M_allocate(__len));
          pointer __new_finish(__new_start);
          __try
    	{
    	  _Alloc_traits::construct(this->_M_impl, // 调用 construct 拷贝构造函数
    				   __new_start + __elems_before,
    #if __cplusplus >= 201103L
    				   std::forward<_Args>(__args)...); // 通过 forward 转换参数为目标对象
    #else
    				   __x);
    #endif
    	  __new_finish = pointer();
    
    	... // 省略很多行
        
    	std::_Destroy(__old_start, __old_finish, _M_get_Tp_allocator());
          _GLIBCXX_ASAN_ANNOTATE_REINIT;
          _M_deallocate(__old_start,
    		    this->_M_impl._M_end_of_storage - __old_start);
          this->_M_impl._M_start = __new_start;
          this->_M_impl._M_finish = __new_finish;
          this->_M_impl._M_end_of_storage = __new_start + __len;
        }
    

    这里看源码,还是通过_Alloc_traits::construct拷贝构造函数创建新对象

结论:

源码看到这里,相比大家都能看出来,push_back(xxx)所做的工作就是在vector 的适当地方,创建1个新对象,然后将 XXX对象通过拷贝构造函数传递给新对象。

emplace_back 源码
#if __cplusplus >= 201103L
  template<typename _Tp, typename _Alloc>
    template<typename... _Args>
#if __cplusplus > 201402L
      typename vector<_Tp, _Alloc>::reference
#else
      void
#endif
      vector<_Tp, _Alloc>::
      emplace_back(_Args&&... __args)
      {
	if (this->_M_impl._M_finish != this->_M_impl._M_end_of_storage)
	  {
	    _GLIBCXX_ASAN_ANNOTATE_GROW(1);
	    _Alloc_traits::construct(this->_M_impl, this->_M_impl._M_finish,
				     std::forward<_Args>(__args)...);
	    ++this->_M_impl._M_finish;
	    _GLIBCXX_ASAN_ANNOTATE_GREW(1);
	  }
	else
	  _M_realloc_insert(end(), std::forward<_Args>(__args)...);
#if __cplusplus > 201402L
	return back(); // C++ 14 增加了返回尾元素迭代器
#endif
      }
#endif


// 左值完美转发
  template<typename _Tp>
    constexpr _Tp&&
    forward(typename std::remove_reference<_Tp>::type& __t) noexcept
    { return static_cast<_Tp&&>(__t); }

// 右值完美转发
  template<typename _Tp>
    constexpr _Tp&&
    forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
    {
      static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"
		    " substituting _Tp is an lvalue reference type");
      return static_cast<_Tp&&>(__t);
    }

对于C++11来说,emplace_back整个过程与 push_back如出一辙,均是通过拷贝构造函数将对象复制创建到vector 尾端

Example

编写了一个String对象,通过具体例子看push_back 和 emplace_back 过程

String 类:

class String {
public:
  String() { 
    std::cout << "Construct!" << std::endl;
    std::cout << "        ctor " << randy << std::endl;
    }

  String(std::string arg):randy(arg) { 
    std::cout << "Construct!" << std::endl; 
    std::cout << "        ctor " << randy << std::endl;
    }

  String(const String& input) {
    randy = input.randy;
    std::cout << "Copy!" << std::endl;
    std::cout << "        C " << randy << std::endl;
  }

  String& operator=(String& input) {
    randy = input.randy;
    std::cout << "Copy operator!" << std::endl;
    std::cout << "        C= " << randy << std::endl;
    return *this;
  }

  String(String&& input) : randy(input.randy) {
    std::cout << "Move Copy!" << std::endl;
    input.randy = "";
    std::cout << "        C && " << randy << std::endl;
  }

  String& operator=(String&& input) {
    randy = input.randy;
    input.randy = "";
    std::cout << "Move Copy operator!" << std::endl;
    std::cout << "        C && = " << randy << std::endl;
    return *this;
  }

  ~String() {
    std::cout << "Destruct!" << std::endl;
    std::cout << "        ~ " << randy << std::endl;
  }

public:
  std::string randy{"orton"};
};

创建vector ,并 push_back 和 emplace_back 元素

int main() {
  {
    std::vector<String> array;
    array.reserve(3);
      
    String ss("11");
    array.push_back(ss);

    String ss2("22");
    array.emplace_back(ss2);
	
	String ss3("33");
    String ss4(std::move(ss3));
  }
}

结果:

Construct!       # String ss("11");
        ctor 11
Copy!           # array.push_back(ss);
        C 11
Construct!      # String ss2("22");
        ctor 22
Copy!           # array.emplace_back(ss2);
        C 22
Construct!      # String ss3("33");
        ctor 33
Move Copy!      # String ss4(std::move(ss3));
        C && 33
Destruct!       # ss4 destruct
        ~ 33
Destruct!       # ss3 destruct
        ~ 
Destruct!       # array 中 22 destruct
        ~ 22
Destruct!       # array 中 11 destruct
        ~ 11
Destruct!       # ss2 destruct
        ~ 22
Destruct!       # ss destruct
        ~ 11

测试代码也能看出 push_backemplace_back过程中发生了 Copy construct行为。

How

上述原因总结就是:

原代码中因为使用了 push_backemplace_back 插入元素,C++会调用s_data的拷贝构造函数将元素插入到尾部,但是s_data类并没有手动写出深拷贝的拷贝构造函数,于是编译器自动生成隐式的拷贝构造函数,该拷贝构造函数为浅拷贝,只拷贝了 s_data类中的指针地址,未拷贝其指向的内存,导致原来被插入的元素及vector中的元素共享同一片内存,2者析构的时候会重复释放同一片内存,造成了 double free`。

解决办法:

s_data设计拷贝构造函数,函数内进行深拷贝就能解决问题

s_data(const s_data& input) {
  this->a = input.a;
  this->b = input.b;
  if (p == nullptr) {
    p = new int();
  }
  *p = input.p[0];
}

Reference


>>>>> 欢迎关注公众号【三戒纪元】 <<<<<

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值