c++内存问题整理与智能指针使用
公司里小组组织c++知识的分享会,正好我手上碰到过几个purify的内存泄露问题,就借这里总结一下c++的内存问题。 借鉴陈硕总结的分类,c++大致的内存问题有以下几个方面: 1.缓冲区溢出 在使用自己编写的缓冲区或者使用不安全的函数时,会遇到类似数组越界的缓冲区溢出问题,Linux内核的解决办法是栈随机化,金丝雀的检测,具体的攻击手段和例子,可以参考我另一篇的buffer lab
公司里小组组织c++知识的分享会,正好我手上碰到过几个purify的内存泄露问题,就借这里总结一下c++的内存问题。
借鉴陈硕总结的分类,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
更多推荐
所有评论(0)