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

news2025/1/18 20:07:07

源码分析 Why double free?

文章目录

  • 源码分析 Why double free?
    • What
    • Why
      • 1.浅拷贝 VS 深拷贝
        • 浅拷贝
        • 深拷贝
      • 2.push_back 和 emplace_back
        • push_back 源码
        • emplace_back 源码
      • Example
    • How
    • Reference
      • >>>>> 欢迎关注公众号【三戒纪元】 <<<<<

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

  • 浅拷贝与深拷贝的区别(详解)
  • C++ 深拷贝和浅拷贝

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/943869.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【爬虫小知识】如何利用爬虫爬网页——python爬虫

前言 网络时代的到来&#xff0c;给我们提供了海量的信息资源&#xff0c;但是&#xff0c;想要获取这些信息&#xff0c;手动一个一个网页进行查找&#xff0c;无疑是一项繁琐且效率低下的工作。这时&#xff0c;爬虫技术的出现&#xff0c;为我们提供了一种高效的方式去获取…

el-form的表单验证,有值却报红!

正确的写法是 el-form中的form用 :model绑定&#xff0c;各个输入项用 v-model绑定值 原因 显然&#xff0c;区别就是 v-model 和 :model的区别 V-mode v-model是一个语法糖&#xff0c;用于 “表单元素上” 实现双向数据绑定。包括数据绑定和事件监听。 <input v-model&q…

Docker技术--Docker简介和架构

1.Docker简介 (1).引入 我们之前学习了EXSI,对于虚拟化技术有所了解,但是我们发现类似于EXSI这样比较传统的虚拟化技术是存在着一定的缺陷:所占用的资源比较多,简单的说,就是你需要给每一个用户提供一个操作平台,这一个操作平台就会占用你的资源。这样资源的浪费是比较多的…

Ansys Zemax | 大功率激光系统的 STOP 分析(五)

大功率激光器广泛用于各种领域当中&#xff0c;例如激光切割、焊接、钻孔等应用中。由于镜头材料的体吸收或表面膜层带来的吸收效应&#xff0c;将导致在光学系统中由于激光能量吸收所产生的影响也显而易见&#xff0c;大功率激光器系统带来的激光能量加热会降低此类光学系统的…

问道管理:逾4600股飘红!汽车板块爆了,多股冲击涨停!

A股商场今天上午全体低开&#xff0c;但其后逐级上行&#xff0c;创业板指数上午收盘大涨超越3%&#xff0c;北向资金也完成净买入38亿元。 别的&#xff0c;A股商场半年报成绩发表如火如荼进行中&#xff0c;多家公司发表半年报后股价应声大涨&#xff0c;部分公司股价冲击涨停…

Docker从认识到实践再到底层原理(一)|技术架构

前言 那么这里博主先安利一些干货满满的专栏了&#xff01; 首先是博主的高质量博客的汇总&#xff0c;这个专栏里面的博客&#xff0c;都是博主最最用心写的一部分&#xff0c;干货满满&#xff0c;希望对大家有帮助。 高质量博客汇总 然后就是博主最近最花时间的一个专栏…

数字孪生:重塑政府决策与公共服务

在之前的文章中为大家分享了数字孪生在很多行业的应用场景&#xff0c;本文和大家一起探讨一下数字孪生在政务管理方面能有哪些应用&#xff0c;以及其对公共服务提供的积极影响。 1&#xff09;城市规划方面 数字孪生技术可用于模拟城市的发展和规划。政府可以建立城市的虚拟…

Promise构造函数,属性以及方法应用

&#xff08;一&#xff09;promise构造函数 <script type"text/javascript">const myPromise new Promise((resolve, reject) > {setTimeout(() > {resolve(foo)},300)})myPromise.then((value) > {console.log(value,value)}).catch((err) > {…

2023蓝帽杯初赛ctf部分题目

Web LovePHP 打开网站环境&#xff0c;发现显示出源码 来可以看到php版本是7.4.33 简单分析了下&#xff0c;主要是道反序列化的题其中发现get传入的参数里有_号是非法字符&#xff0c;如果直接传值传入my_secret.flag&#xff0c;会被php处理掉 绕过 _ 的方法 对于__可以…

蓝牙模块产品认证-国际市场准入准则之加拿大IC认证基础知识

蓝牙模块产品认证-国际市场准入准则之加拿大IC认证基础知识 一&#xff1a;前言加拿大IC介绍 1.1&#xff1a;IC更名 2016年3月加拿大工业部(IC, Industry Canada)正式更名为加拿大创新、科学和经济发展 部(ISED, Innovation, Science and Economic Development Canada) ISED…

客观-【2 线性表】

关键字&#xff1a; 求一阶导数、建立有序单链表时间复杂度、静态链表的指针、链表查找数据x的序号

五、升压电路boost

开关导通时&#xff1a;输入电压对电感充电&#xff0c;形成回路&#xff0c;vi—>电感L—>开关管q&#xff1b; 开关断开时&#xff1a;输入的能量和电感能量一起向负载提供能量&#xff0c;形成回路&#xff0c;Vi—>L—>D—>C—>RL&#xff0c;因此输出电…

R语言APRIORI关联规则、K-MEANS均值聚类分析中药专利复方治疗用药规律网络可视化...

全文链接&#xff1a;http://tecdat.cn/?p30605 应用关联规则、聚类方法等数据挖掘技术分析治疗的中药专利复方组方配伍规律&#xff08;点击文末“阅读原文”获取完整代码数据&#xff09;。 方法检索治疗中药专利复方&#xff0c;排除外用中药及中西药物合用的复方。最近我们…

前端 -- 基础 VSCode 工具生成骨架标签新增代码 解释详解

目录 文档类型声明标签 Lang 语言种类 字符集 文档类型声明标签 <!DOCTYPE> 文档类型声明&#xff0c;作用就是告诉浏览器 当前的页面是 使用哪种 HTML 版本 来显示的网页 HTML 版本也很多呀 &#xff0c;比如 &#xff1a; HTML5 ,HTML4&#xff0c;XHTML 等…

Android 手游聚合SDK小知识(一)

前言 回头想想&#xff0c;在安卓游戏SDK这个领域&#xff0c;我也呆了4年了&#xff0c;从啥都不懂的小菜鸟&#xff0c;逐渐靠自己不断学习&#xff0c;对这个行业也算有了一些理解&#xff0c;趁着最近有空&#xff0c;我想了一下&#xff0c;还是把最近这几年对游戏SDK聚合…

.jar中没有主清单属性【已解决】

原因 对jar解压缩&#xff0c;可以看到有一个MANIFEST.MF文件&#xff0c;此文件就是jar运行时要查找的清单目录。 主清单数据&#xff0c;就是我们要运行的主类即程序入口&#xff0c;缺少主清单属性&#xff0c;就不知道从哪开始运行。 因此我们需要对项目进行配置&#xff…

华为数通方向HCIP-DataCom H12-821题库(单选题:161-180)

第161题 以下关于 URPF(Unicast Reverse Path Forwarding) 的描述&#xff0c; 正确的是哪一项 A、部署了严格模式的 URPF&#xff0c;也能够可以同时部署允许匹配缺省路由模式 B、如果部署松散模式的 URPF&#xff0c;默认情况下不需要匹配明细路由 C、如果部署松散模式的…

[java基础知识复习] Java基础知识总结分享一

写代码&#xff1a; 1&#xff0c;明确需求。我要做什么&#xff1f; 2&#xff0c;分析思路。我要怎么做&#xff1f;1,2,3。 3&#xff0c;确定步骤。每一个思路部分用到哪些语句&#xff0c;方法&#xff0c;和对象。 4&#xff0c;代码实现。用具体的java语言代码把思路…

蓝牙模块产品认证-国际市场准入准则之欧美CE认证基础知识

蓝牙模块产品认证-国际市场准入准则之欧美CE认证基础知识 一&#xff1a;前言欧盟市场准入介绍 1.1&#xff1a;CE适用范围 欧盟(EU : European Union)有27成员国、其中大家熟知的国家有法国、德国、意大利、奥地利匈牙利、荷兰、波兰、西班牙、葡萄牙、希腊、丹麦、瑞典、比利…

接地气的开源读书讨论会!KCC@广州首次活动圆满结束

前期推文链接&#xff1a;KCC广州开源读书会&广州开源建设讨论会 More 2023 年 8 月 20 日&#xff0c;KCC广州在暨南大学石牌校区成功举办了其首场读书会 & 开源讨论会。这标志着 KCC广州首次正式步入公众视野&#xff0c;开展开源交流活动。 开源社副执行长、KCC广州…