c++内存问题整理与智能指针使用

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/lijun538/article/details/50857236

  公司里小组组织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的内存问题主要分为以下几类。
  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_ptr‍s 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
展开阅读全文

没有更多推荐了,返回首页