c++内存问题整理与智能指针使用
2016-03-14 14:49
585 查看
公司里小组组织c++知识的分享会,正好我手上碰到过几个purify的内存泄露问题,就借这里总结一下c++的内存问题。
借鉴陈硕总结的分类,c++大致的内存问题有以下几个方面:
1.缓冲区溢出
在使用自己编写的缓冲区或者使用不安全的函数时,会遇到类似数组越界的缓冲区溢出问题,Linux内核的解决办法是栈随机化,金丝雀的检测,具体的攻击手段和例子,可以参考我另一篇的buffer lab实验。在自己写程序的时候,最重要的一点是记录或者限制缓冲区的长度,使用
在purify检测的时候,使用的就是类似金丝雀的机制,在缓冲区前后插入特殊的数值,如果其数值被修改了,就是溢出。
2.空悬指针/野指针
指针在所指的内存空间被释放后,没有置为NULL,指针的生存期还没有结束,这时候指针所指的区域是个随机值。并且可以继续使用!!!
下面使用代码进行验证,环境为win7,64位,Dev-c++5.11,gcc(c11)。
上面代码的运行结果:
可以看出第一次是随机值,第二次竟然正常更新了。
在正常工程使用中,指针delete完之后置NULL,以及不要返回局部指针变量!!!如果两个指针指向同一内存,其中一个置NULL,依然解决不了空悬指针的问题。如下:
重置p对q没有任何作用!
关于局部变量的示例代码:
将
3.重复释放
非常经典的double free问题,运行如下程序:
运行结果会产生运行时错误,调试状态下会有SIGTRAP信号。
解决办法也是指针置NULL。拓展一下,指针置NULL之后还可以释放吗?答案是可以。这里需要研究一下new和delete。
new和delete其实就是对malloc和free的封装。
对于简单数据,直接调用operator new分配内存。但是可以使用new_handler来处理new失败的情况。
另一处不同,malloc失败返回NULL,而new抛出异常。
对于复杂数据结构,例如对象,先调用operator new分配内存后,在调用其构造函数。
delete对于简单数据,直接调用free。
对于复杂的数据结构,先析构,后delete。
free的部分代码如下(glibc):
函数会对指针进行NULL的检测,为空直接返回,因此delete完之后置NULL可以避免重复释放问题。
4.内存泄露
这一问题可以通过智能指针解决,下面再细讲。
5.不配对的new[]/delete
new和delete在声明使用的时候需要配对使用。
当我们释放一个指向数组的指针时,它指示编译器指针指向一个对象数组的第一个元素,元素按逆序销毁。
承接上面对new和delete的研究,在调用new[]和delete[]的时候。
对于简单数据,new[]计算好大小后调用operator new。
对于复杂数据类型,先分配内存,写入数组大小,然后调用N次构造。
对于简单数据类型,delete[]和delete效果一样。
对于复杂的数据类型,delete[]先析构,后释放空间。
注意!!!以下代码正常运行,编译器没有警告。
在《c++primer》中提到,上面的行为是未定义的。可能会崩溃,可以是行为异常。比较一劳永逸的办法是使用vector代替数组。
6.内存碎片
常用的解决办法是实现自己的memory pool,这里不详细讨论了,因为首先现在的malloc有所优化,第二这个问题有时候影响不大。
上面参照了《linux多线程服务端》的分类,现在讲解一下purify的问题,purify的内存问题主要分为以下几类。
这里不详细解释了,英文应该很好懂。
讲一下在工作过程中碰到的两个实例。
第一个是UMR,未初始化内存读的问题,很多时候,这个问题并不算问题,不具有准确性。
例如下面的代码就会有UMR问题,这里是因为结构体的字节填充,其中smth的field2会因为4字节的地址对齐的需要,被填充三个字节。
而我遇到的UMR代码如下:
这个函数乍看没有任何问题,函数的绑定和函数操作而已,如果做类似调用,就会有UMR问题。
问题出在编译器自己合成的构造函数和拷贝构造上,如果你不希望编译器合成,或者不清楚合成的代码效果,请明确的构造出来。这里的解决办法就是构造出空的构造函数和拷贝构造函数即可。
接下来的一个例子也与拷贝构造函数和拷贝赋值运算符有关。
首先明确一下两个函数出现的地方,拷贝赋值是同类对象赋值时出现。拷贝构造是在使用=定义变量时、将一个对象作为实参传递给一个非引用类型的形参、以及从一个返回类型为非引用类型的函数返回一个对象时使用。
如果自己没有定义,编译器会为我们合成一个。实例代码如下:
这里在使用拷贝赋值和拷贝构造函数时,编译器会为我们合成拷贝构造和拷贝赋值函数。类似下面的代码:
这个代码有什么问题?看一下面的图片就知道了。
对于指针型的成员变量,合成的拷贝赋值和拷贝构造只是复制了指针本身,而不是指向的对象,这叫做浅拷贝,当其中的s1或者s2析构释放的时候,它所指向的内存空间就被释放掉了,另一个指针就变成了野指针,会出现double free。同时也存在其中一个指针更改值造成另一个对象值也更改的现象。
应急的解决办法是自己完成拷贝构造和拷贝赋值。代码如下:
上面提到应急两个字,言外之意,应该有更好的解决办法,相信各位应该也能想到了,智能指针。
简单来说,智能指针在上图的S1和S2与内存空间之间加了一个代理层,一个新的对象,让s1和s2所指的对象永久有效,先命名为proxy,同时把两个指针都变成对象,sp1,sp2。proxy有两个成员,指针和计数器。sp1析构后,计数器减一,计数为0时,销毁proxy指针指向的对象。
空悬指针野指针可以用shared_ptr/weak_ptr解决,对于重复释放可以选择unique_ptr与scoped_ptr解决。
其中shared_ptr、weak_ptr、scoped_ptr为boost库模板。
c11吸收了shared_ptr、weak_ptr并使用具有移动语意的unique_ptr代替scoped_ptr,它们声明在memory头文件中。
这里简单介绍一下用法,代码如下,更多的查看手册:
shared_ptr运行多个指针指向同一个对象,这就可以解决上面的浅拷贝问题。
注意shared_ptr有一个非常有用的特性,删除器,可以使析构动作在构造时被捕捉。
结果如下:
一个unique_ptr只能指向一个给定对象,不支持普通的拷贝和赋值,但是可以使用函数release或者reset转移指针所有权,scoped_ptr则不允许,两者异同:
A auto_ptr is a pointer with copy and with move semantics and ownership (=auto-delete).
A unique_ptr is a auto_ptr without copy but with move semantics.
A scoped_ptr is a auto_ptr without copy and without move semantics.
auto_ptrs are allways a bad choice – that is obvious.
Whenever you want to explicitely have move semantics, use a unique_ptr.
Whenever you want to explicitely disallow move semantics,use a scoped_ptr.
最后介绍一下weak_ptr,shared_ptr是强引用,拿铁丝绑着对象,而weak_ptr是棉线挂着(陈硕的比喻),weak_ptr不控制所指对象的生命周期,对象的释放和weak_ptr无关,这种弱引用可以拿来打破shared_ptr的循环引用问题,两个shared_ptr互相引用,会造成对象无法释放。
weak_ptr起到一个检测的作用!!!
weak_ptr还可以用于弱回调,把shared_ptr绑定到function里,会延长对象的生命周期,如果想实现对象活着就调用,否则忽略的效果,可以使用weak_ptr。
最后对weak_ptr和shared_ptr做一个简单的分析。
unique_ptr使用元素,指针和删除器。
shared_ptr在基类的基础上加上删除器参数。下面是示意性的摘录。
最后给一个玩具型的参考实现,来自c++primer答案参考。
借鉴陈硕总结的分类,c++大致的内存问题有以下几个方面:
1.缓冲区溢出
在使用自己编写的缓冲区或者使用不安全的函数时,会遇到类似数组越界的缓冲区溢出问题,Linux内核的解决办法是栈随机化,金丝雀的检测,具体的攻击手段和例子,可以参考我另一篇的buffer lab实验。在自己写程序的时候,最重要的一点是记录或者限制缓冲区的长度,使用
vector<char>这样的容器,
strncpy这样更安全的函数。
在purify检测的时候,使用的就是类似金丝雀的机制,在缓冲区前后插入特殊的数值,如果其数值被修改了,就是溢出。
2.空悬指针/野指针
指针在所指的内存空间被释放后,没有置为NULL,指针的生存期还没有结束,这时候指针所指的区域是个随机值。并且可以继续使用!!!
下面使用代码进行验证,环境为win7,64位,Dev-c++5.11,gcc(c11)。
void testNullPtr(){ int *p=new int(10); delete p; cout<<*p<<endl; *p=20; cout<<*p<<endl; }
上面代码的运行结果:
可以看出第一次是随机值,第二次竟然正常更新了。
在正常工程使用中,指针delete完之后置NULL,以及不要返回局部指针变量!!!如果两个指针指向同一内存,其中一个置NULL,依然解决不了空悬指针的问题。如下:
重置p对q没有任何作用!
int *p(new int(42)); auto q=p; delete p; p=nullptr;
关于局部变量的示例代码:
char *itoa(int n){ char buf[43]; sprintf(buf,"%d",n); return buf; }
将
buf声明为static即可。
3.重复释放
非常经典的double free问题,运行如下程序:
void testDoubleFree(){ int *p=new int(10); delete p; delete p; }
运行结果会产生运行时错误,调试状态下会有SIGTRAP信号。
解决办法也是指针置NULL。拓展一下,指针置NULL之后还可以释放吗?答案是可以。这里需要研究一下new和delete。
new和delete其实就是对malloc和free的封装。
对于简单数据,直接调用operator new分配内存。但是可以使用new_handler来处理new失败的情况。
另一处不同,malloc失败返回NULL,而new抛出异常。
对于复杂数据结构,例如对象,先调用operator new分配内存后,在调用其构造函数。
delete对于简单数据,直接调用free。
对于复杂的数据结构,先析构,后delete。
free的部分代码如下(glibc):
函数会对指针进行NULL的检测,为空直接返回,因此delete完之后置NULL可以避免重复释放问题。
if (ptr == NULL) { catomic_increment (&calls[idx_free]); return; }
4.内存泄露
这一问题可以通过智能指针解决,下面再细讲。
5.不配对的new[]/delete
new和delete在声明使用的时候需要配对使用。
int *p=new int(10); delete p; int *pa=new int[10]; delete []pa;
当我们释放一个指向数组的指针时,它指示编译器指针指向一个对象数组的第一个元素,元素按逆序销毁。
承接上面对new和delete的研究,在调用new[]和delete[]的时候。
对于简单数据,new[]计算好大小后调用operator new。
对于复杂数据类型,先分配内存,写入数组大小,然后调用N次构造。
对于简单数据类型,delete[]和delete效果一样。
对于复杂的数据类型,delete[]先析构,后释放空间。
注意!!!以下代码正常运行,编译器没有警告。
class Obj{ public: Obj(){} }; void FitNewDelete(){ Obj *p=new Obj[3]; delete p; }
在《c++primer》中提到,上面的行为是未定义的。可能会崩溃,可以是行为异常。比较一劳永逸的办法是使用vector代替数组。
6.内存碎片
常用的解决办法是实现自己的memory pool,这里不详细讨论了,因为首先现在的malloc有所优化,第二这个问题有时候影响不大。
上面参照了《linux多线程服务端》的分类,现在讲解一下purify的问题,purify的内存问题主要分为以下几类。
这里不详细解释了,英文应该很好懂。
讲一下在工作过程中碰到的两个实例。
第一个是UMR,未初始化内存读的问题,很多时候,这个问题并不算问题,不具有准确性。
例如下面的代码就会有UMR问题,这里是因为结构体的字节填充,其中smth的field2会因为4字节的地址对齐的需要,被填充三个字节。
struct something { int field1; char field2; }; /* ... */ struct something smth, smth2; smth.field1 = 1; smth.field2 = 'A'; smth2 = smth;
而我遇到的UMR代码如下:
struct test: public std::binary_function<x, y, bool> { bool operator() (const x& thisInfo, y otherEntityType) const { return (thisInfo.entityType == otherEntityType); } };
这个函数乍看没有任何问题,函数的绑定和函数操作而已,如果做类似调用,就会有UMR问题。
this->ritTypeInfo = std::find_if(RITS_begin, RITS_end, bind2nd(test(), entityType));
问题出在编译器自己合成的构造函数和拷贝构造上,如果你不希望编译器合成,或者不清楚合成的代码效果,请明确的构造出来。这里的解决办法就是构造出空的构造函数和拷贝构造函数即可。
接下来的一个例子也与拷贝构造函数和拷贝赋值运算符有关。
首先明确一下两个函数出现的地方,拷贝赋值是同类对象赋值时出现。拷贝构造是在使用=定义变量时、将一个对象作为实参传递给一个非引用类型的形参、以及从一个返回类型为非引用类型的函数返回一个对象时使用。
如果自己没有定义,编译器会为我们合成一个。实例代码如下:
class CheckApp{}; class check{ CheckApp *pCheckApp; public: check(){ pCheckApp=new CheckApp(); } ~check(){ if(pCheckApp!=NULL){ delete pCheckApp; pCheckApp=NULL; } } };
这里在使用拷贝赋值和拷贝构造函数时,编译器会为我们合成拷贝构造和拷贝赋值函数。类似下面的代码:
check::check(const check& rhs) :pCheckApp(rhs.pCheckApp){} check& check::operator=(const check& rhs){ if(this!=&rhs){ pCheckApp=rhs.pCheckApp; } return *this; }
这个代码有什么问题?看一下面的图片就知道了。
对于指针型的成员变量,合成的拷贝赋值和拷贝构造只是复制了指针本身,而不是指向的对象,这叫做浅拷贝,当其中的s1或者s2析构释放的时候,它所指向的内存空间就被释放掉了,另一个指针就变成了野指针,会出现double free。同时也存在其中一个指针更改值造成另一个对象值也更改的现象。
应急的解决办法是自己完成拷贝构造和拷贝赋值。代码如下:
//先完成拷贝构造,下面赋值要用 check::check(const check& rhs){ pCheckApp=new CheckApp(); if(pCheckApp!=NULL){ *pCheckApp=*(rhs.pCheckApp); } } //进行深拷贝,按值拷贝 check& check::operator=(const check &rhs){ //这个判断是防止自赋值 a=a if(this!=&rhs){ //使用构造局部变量,进行成员交换 //局部变量出作用域会自动释放 //防止new失败,简化设计 check tmp(rhs); CheckApp *tmpCheckApp=pCheckApp; pCheckApp=tmp.pCheckApp; tmp.pCheckApp=tmpCheckApp; } return *this; }
上面提到应急两个字,言外之意,应该有更好的解决办法,相信各位应该也能想到了,智能指针。
简单来说,智能指针在上图的S1和S2与内存空间之间加了一个代理层,一个新的对象,让s1和s2所指的对象永久有效,先命名为proxy,同时把两个指针都变成对象,sp1,sp2。proxy有两个成员,指针和计数器。sp1析构后,计数器减一,计数为0时,销毁proxy指针指向的对象。
空悬指针野指针可以用shared_ptr/weak_ptr解决,对于重复释放可以选择unique_ptr与scoped_ptr解决。
其中shared_ptr、weak_ptr、scoped_ptr为boost库模板。
c11吸收了shared_ptr、weak_ptr并使用具有移动语意的unique_ptr代替scoped_ptr,它们声明在memory头文件中。
这里简单介绍一下用法,代码如下,更多的查看手册:
shared_ptr运行多个指针指向同一个对象,这就可以解决上面的浅拷贝问题。
注意shared_ptr有一个非常有用的特性,删除器,可以使析构动作在构造时被捕捉。
template<class T> struct endPtr{ //主要用于非动态分配的对象 //用于不具有良好的析构函数的对象 //deleter是个泛型类型,需要operator() void operator()(T* p){ delete [] p; cout<<"now delete"<<endl; } }; void testSharePtr(){ //shared_ptr<int> q(new int(10)); 不建议 shared_ptr<int> q=make_shared<int> (42); //c11 构造 auto p=make_shared<int> (40); //使用 cout<<"p:"<<*p<<" use: "<<p.use_count()<<endl; //引用数 cout<<"q:"<<q.use_count()<<endl; //判断 cout<<"is unique?: "<<q.unique()<<endl; p=q; cout<<"p:"<<p.use_count()<<endl; cout<<"q:"<<q.use_count()<<endl; int *tmp=new int[100]; //定义自己的删除器 shared_ptr<int> r(tmp,endPtr<int>()); }
结果如下:
一个unique_ptr只能指向一个给定对象,不支持普通的拷贝和赋值,但是可以使用函数release或者reset转移指针所有权,scoped_ptr则不允许,两者异同:
A auto_ptr is a pointer with copy and with move semantics and ownership (=auto-delete).
A unique_ptr is a auto_ptr without copy but with move semantics.
A scoped_ptr is a auto_ptr without copy and without move semantics.
auto_ptrs are allways a bad choice – that is obvious.
Whenever you want to explicitely have move semantics, use a unique_ptr.
Whenever you want to explicitely disallow move semantics,use a scoped_ptr.
最后介绍一下weak_ptr,shared_ptr是强引用,拿铁丝绑着对象,而weak_ptr是棉线挂着(陈硕的比喻),weak_ptr不控制所指对象的生命周期,对象的释放和weak_ptr无关,这种弱引用可以拿来打破shared_ptr的循环引用问题,两个shared_ptr互相引用,会造成对象无法释放。
weak_ptr起到一个检测的作用!!!
auto p=make_shared<int> (42); //不改变引用计数 weak_ptr<int> wp(p); //由于对象可能不存在,使用lock函数,如果有的话,返回shared_ptr if(shared_ptr<int> np=wp.lock()){ }
weak_ptr还可以用于弱回调,把shared_ptr绑定到function里,会延长对象的生命周期,如果想实现对象活着就调用,否则忽略的效果,可以使用weak_ptr。
最后对weak_ptr和shared_ptr做一个简单的分析。
unique_ptr使用元素,指针和删除器。
// unique_ptr内部片段 template <typename _Tp, typename _Dp = default_delete<_Tp> > class unique_ptr { // use SFINAE to determine whether _Del::pointer exists class _Pointer { template<typename _Up> static typename _Up::pointer __test(typename _Up::pointer*); template<typename _Up> static _Tp* __test(...); typedef typename remove_reference<_Dp>::type _Del; public: typedef decltype(__test<_Del>(0)) type; }; typedef std::tuple<typename _Pointer::type, _Dp> __tuple_type; __tuple_type _M_t; public: typedef typename _Pointer::type pointer; typedef _Tp element_type; typedef _Dp deleter_type; };
shared_ptr在基类的基础上加上删除器参数。下面是示意性的摘录。
template<typename _Tp> class shared_ptr : public __shared_ptr<_Tp> { public: //其中一个构造函数 template<typename _Tp1, typename _Deleter> shared_ptr(_Tp1* __p, _Deleter __d) : __shared_ptr<_Tp>(__p, __d) { } }; template<typename _Tp, _Lock_policy _Lp> class __shared_ptr { public: typedef _Tp element_type; protected: friend class __weak_ptr<_Tp, _Lp>; private: void* _M_get_deleter(const std::type_info& __ti) const noexcept { return _M_refcount._M_get_deleter(__ti); } _Tp* _M_ptr; // Contained pointer. __shared_count<_Lp> _M_refcount; // Reference counter. };
最后给一个玩具型的参考实现,来自c++primer答案参考。
/*************************************************************************** * @file shared_pointer.hpp * @author Yue Wang * @date 04 Feb 2014 * Jul 2015 * Oct 2015 * @remark This code is for the exercises from C++ Primer 5th Edition * @note ***************************************************************************/ #pragma once #include <functional> #include "delete.hpp" namespace cp5 { template<typename T> class SharedPointer; template<typename T> auto swap(SharedPointer<T>& lhs, SharedPointer<T>& rhs) { using std::swap; swap(lhs.ptr, rhs.ptr); swap(lhs.ref_count, rhs.ref_count); swap(lhs.deleter, rhs.deleter); } template<typename T> class SharedPointer { public: // // Default Ctor // SharedPointer() : ptr{ nullptr }, ref_count{ new std::size_t(1) }, deleter{ cp5::Delete{} } { } // // Ctor that takes raw pointer // explicit SharedPointer(T* raw_ptr) : ptr{ raw_ptr }, ref_count{ new std::size_t(1) }, deleter{ cp5::Delete{} } { } // // Copy Ctor // SharedPointer(SharedPointer const& other) : ptr{ other.ptr }, ref_count{ other.ref_count }, deleter{ other.deleter } { ++*ref_count; } // // Move Ctor // SharedPointer(SharedPointer && other) noexcept : ptr{ other.ptr }, ref_count{ other.ref_count }, deleter{ std::move(other.deleter) } { other.ptr = nullptr; other.ref_count = nullptr; } // // Copy assignment // SharedPointer& operator=(SharedPointer const& rhs) { //increment first to ensure safty for self-assignment ++*rhs.ref_count; decrement_and_destroy(); ptr = rhs.ptr, ref_count = rhs.ref_count, deleter = rhs.deleter; return *this; } // // Move assignment // SharedPointer& operator=(SharedPointer && rhs) noexcept { cp5::swap(*this, rhs); rhs.decrement_and_destroy(); return *this; } // // Conversion operator // operator bool() const { return ptr ? true : false; } // // Dereference // T& operator* () const { return *ptr; } // // Arrow // T* operator->() const { return &*ptr; } // // Use count // auto use_count() const { return *ref_count; } // // Get underlying pointer // auto get() const { return ptr; } // // Check if the unique user // auto unique() const { return 1 == *refCount; } // // Swap // auto swap(SharedPointer& rhs) { ::swap(*this, rhs); } // // Free the object pointed to, if unique // auto reset() { decrement_and_destroy(); } // // Reset with the new raw pointer // auto reset(T* pointer) { if (ptr != pointer) { decrement_n_destroy(); ptr = pointer; ref_count = new std::size_t(1); } } // // Reset with raw pointer and deleter // auto reset(T *pointer, const std::function<void(T*)>& d) { reset(pointer); deleter = d; } // // Dtor // ~SharedPointer() { decrement_and_destroy(); } private: T* ptr; std::size_t* ref_count; std::function<void(T*)> deleter; auto decrement_and_destroy() { if (ptr && 0 == --*ref_count) delete ref_count, deleter(ptr); else if (!ptr) delete ref_count; ref_count = nullptr; ptr = nullptr; } }; }//namespace
相关文章推荐
- C++为什么要引入静态成员函数
- 【C++ STL学习之四】Map类模板
- C++ 内存管理
- C++在C语言基础之上增强的几个实用特性总结
- C++_C风格字符串
- 调试基础 eclipse C++ 断点调试
- C++_运算符的优先级
- C++ Set常用用法
- c++内存模型
- c++ assert
- C语言实现动态顺序表
- C语言实现静态顺序表
- C++类外直接访问私有成员
- 51Nod--1384全排列
- C++中cos,sin,asin,acos这些三角函数操作的是弧度,而非角度,
- C-C++字符输出时遇到字符'\n','\0'的区别
- 容器基础知识
- 【Editor】Visual Studio常用快捷键
- C++ vector 的使用出现的问题解决的记录
- C++ new失败情况与处理