前几天,同事让帮忙看一段代码,问为什么程序报错了
free(): double free detected in tcache 2
Aborted (core dumped)
源代码如下:
#include
#include
#include
#include
#include
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;
}
一般double free
的问题都是释放指针内存导致的,double 就是多次释放了。
而代码中释放的时候,就是在主程序结束的时候,说明有多个指针指向了同一片内存,然后导致了多次释放的问题。
因为有多个指针指向同一片内存,这就是存在指针的复制。
而代码中存在复制的地方就是第27行和第28行,vector
的 push_back
和 emplace_back
。
为什么看起来没有问题的push_back,只拷贝了指针的地址?
这就是浅拷贝的发生。
只复制对象指针,即按位拷贝对象,如果拷贝基本类型,会拷贝基本类型的值;如果拷贝的是内存地址或引用类型,拷贝的是内存地址,并不复制对象本身内容,不开辟新内存,拷贝前后对象共同指向同一块内存。相当于share_ptr 中多个指针共享同一片内存。
深拷贝会创建一个新的对象,该对象与原对象各自拥有独立的内存。
深拷贝时会递归拷贝所有对象属性和数组元素,拷贝属性指向的动态分配内存。
深拷贝比浅拷贝速度慢,且内存开销较大。
如果类没有定义拷贝构造(Copy constructor)函数,编译器会隐式地(隐式表示如果不被使用则不生成)生成Copy constructor 函数,在Copy constructor函数中对成员变量执行类似于memcpy的按位复制。对于指针变量,仅仅复制其内存地址,并不会新开辟内存空间。因此,执行默认拷贝函数后,指针成员变量会指向同一块堆内存。
push_back和 emplace_back 都会发生拷贝构造(Copy constructor)的情况,当将元素 push_back 或 emplace_back 到 vector 中去的时候,vector 都要先创建1个该对象,然后通过拷贝构造函数将当前元素赋值给创建出来的对象。
让我们源码见。
该函数创建一个元素在 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对象通过拷贝构造函数传递给新对象。
#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 尾端
编写了一个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_back
和emplace_back
过程中发生了 Copy construct行为。
上述原因总结就是:
原代码中因为使用了 push_back
和emplace_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];
}