最后更新于 .

C/C++代码中,野指针问题历来已久,当然,大家都知道new/delete要成对出现:

A *p = new A();
delete p;
p = NULL;

然而现实中却并不是总是如此简单,考虑如下例子:

class A
{
public:
    C() {}
    virtual ~C() {}
};
class B
{
public:
    B() {
        m_pA = NULL;
    }
    virtual ~B() {}

    void SetA(A* p)
    {
        m_pA = p;
    }

private:
    A* m_pA;
};

A* pA = new A();
B* pB = new B();
pB->SetA(pA);

delete pA;
pA = NULL;
//此时B中的m_pA已经无效,但是m_pA仍然不等于NULL,所以用 != NULL来判断不会有任何作用

简单来说,即pA被赋值为NULL,对B中的m_pA没有产生影响,那么怎么才能产生影响呢? 我们有两个做法: 第一种,在A的析构函数里面去B.SetA(NULL),但是这个相当于A去操作了B的数据,这是不合理的。而且当外面的指针非常多的时候,也根本不可能实现。 第二种方法呢?是的,我们可以用二级指针。 考虑如下代码:

class A
{
public:
    C() {}
    virtual ~C() {}
};
class B
{
public:
    B() {
        m_ppA = NULL;
    }
    virtual ~B() {}

    void SetA(A** pp)
    {
        m_ppA = pp;
    }

private:
    A** m_ppA;
};

A** ppA = new (A*)();

(*ppA) = new A();
B* pB = new B();

pB->SetA(ppA);

delete (*ppA);
(*ppA) = NULL;

//这个时候,B中的m_ppA也会收到影响,即*m_ppA == NULL

这样确实可以解决野指针的问题,但是同时也引入了另一个问题,那就是ppA本身该什么时候释放呢?答案是:当最后一个引用ppA的类释放掉的时候。 最后一个,对,我们可以使用引用计数! OK,正式放出我们的代码,其中使用了引用计数来确定当最后一个类释放掉的时候,ppA指针的内存被析构:

/*=============================================================================
#
#     FileName: ptr_proxy.h
#         Desc: 这个类的作用,就是为了解决互指指针,不知道对方已经析构的问题
#
#       Author: dantezhu
#        Email: zny2008@gmail.com
#     HomePage: http://www.vimer.cn
#
#      Created: 2011-06-13 15:24:12
#      Version: 0.0.1
#      History:
#               0.0.1 | dantezhu | 2011-06-13 15:24:12 | initialization
#
=============================================================================*/

#ifndef __PTR_PROXY_H__
#define __PTR_PROXY_H__
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <iostream>
#include <memory>
#include <sstream>
#include <algorithm>
#include <string>
#include <vector>
#include <set>
#include <map>
using namespace std;

template <typename T>
class ptr_proxy
{
public:
    ptr_proxy(const T* pobj=NULL) : m_ppobj(NULL), m_pcount(NULL)
    {
        if (pobj == NULL)
        {
            return;
        }
        init(pobj);
    }

    ptr_proxy(const ptr_proxy& rhs) // 拷贝构造函数
    {
        m_ppobj = rhs.m_ppobj; // 指向同一块内存
        m_pcount = rhs.m_pcount; // 使用同一个计数值
        add_count();
    }

    virtual ~ptr_proxy()
    {
        dec_count();
    }

    /**
     * @brief   如果指向的对象被释放了,一定要调用这个函数让他知道
     */
    void set2null()
    {
        if (m_ppobj)
        {
            (*m_ppobj) = NULL;
        }
    }

    /**
     * @brief   赋值函数
     *
     * @param   rhs         源对象
     *
     * @return  自己的引用
     */
    ptr_proxy& operator=(const ptr_proxy& rhs)
    {
        if( m_ppobj == rhs.m_ppobj ) // 首先判断是否本来就指向同一内存块
            return *this; // 是则直接返回

        dec_count();

        m_ppobj = rhs.m_ppobj; // 指向同一块内存
        m_pcount = rhs.m_pcount; // 使用同一个计数值
        add_count();

        return *this; // 是则直接返回
    }

    ptr_proxy& operator=(const T* pobj)
    {
        if(m_ppobj && *m_ppobj == pobj) // 首先判断是否本来就指向同一内存块
            return *this; // 是则直接返回
        dec_count();

        init(pobj);

        return *this;
    }

    /**
     * @brief   获取内部关联的obj的指针
     *
     * @return
     */
    T* true_ptr()
    {
        if (m_ppobj)
        {
            return *m_ppobj;
        }
        else
        {
            return NULL;
        }
    }

    /**
     * @brief   获取内部关联的obj的指针
     *
     * @return
     */
    T* operator*()
    {
        return true_ptr();
    }

    /**
     * @brief   获取内部关联的obj的个数
     *
     * @return  个数
     */
    int count()
    {
        if (m_pcount != NULL)
        {
            return *m_pcount;
        }
        return 0;
    }

    /**
     * @brief   判断智能指针是否为空
     *
     * @return
     */
    bool is_null()
    {
        if (m_ppobj == NULL || (*m_ppobj) == NULL)
        {
            return true;
        }
        return false;
    }

protected:
    void init(const T* pobj)
    {
        m_ppobj = new (T*)();
        *m_ppobj = (T*)pobj;
        m_pcount = new int(); // 初始化计数值为 1
        *m_pcount = 1;
    }

    void add_count()
    {
        if (m_pcount == NULL)
        {
            return;
        }
        (*m_pcount) ++;
    }

    /**
     * @brief   计数减1
     */
    void dec_count()
    {
        if (m_pcount == NULL || m_ppobj == NULL)
        {
            return;
        }

        (*m_pcount)--; // 计数值减 1 ,因为该指针不再指向原来内存块了
        if( *m_pcount <= 0 ) // 已经没有别的指针指向原来内存块了
        {
            //我们不去主动析构对象
            //free_sptr(*m_ppobj);//把对象析构
            if (m_ppobj != NULL)
            {
                delete m_ppobj;
                m_ppobj = NULL;
            }

            if (m_pcount != NULL)
            {
                delete m_pcount;
                m_pcount = NULL;
            }
        }
    }

protected:
    T** m_ppobj;
    int* m_pcount;
};

template <typename T>
class IPtrProxy
{
public:
    IPtrProxy() {
        m_ptr_proxy = (T*)this;
    }
    virtual ~IPtrProxy() {
        m_ptr_proxy.set2null();
    }

    ptr_proxy<T>& get_ptr_proxy()
    {
        return m_ptr_proxy;
    }

protected:
    ptr_proxy<T> m_ptr_proxy;
};

#endif

我们来写段测试代码测试一下:

#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <iostream>
#include <memory>
#include <sstream>
#include <algorithm>
#include <string>
#include <vector>
#include <set>
#include <map>

#include "ptr_proxy.h"

using namespace std;

class A : public IPtrProxy<A>
{
public:
    A() {}
    virtual ~A() {}
};

class B : public IPtrProxy<B>
{
public:
    B() {}
    virtual ~B() {}

    void print()
    {
        printf("this is print\n");
    }

    void SetAPtr(const ptr_proxy<A>& pptr)
    {
        m_ptr_a = pptr;
    }

    void check()
    {
        if (m_ptr_a.is_null())
        {
            printf("is null\n");
        }
        else
        {
            printf("is not null\n");
        }
    }

    ptr_proxy<A> m_ptr_a;
};

int main(int argc, char **argv)
{
    A* a = new A();
    B* b = new B();
    b->SetAPtr(a->get_ptr_proxy());
    delete a;
    b->check();

    b->get_ptr_proxy().true_ptr()->print();
    delete b;
    return 0;
}

输出为:

is null
this is print

这个类最有效的使用场景是当出现大量互指指针时,那么指向对象的指针有效性判断就尤其重要,而这个类可以完美解决这个问题。 可能想的比较深的朋友会问,既然引用计数都已经用上了,那么为什么不直接通过引用计数来析构呢? 其实这几天我也在尝试,C++是否能引入完美的引用计数进行对象管理,而最终卡在一个地方,即: 如果,在类的构造函数里面,需要将引用计数对象构造出来,那么引用计数就会出现问题,如:

class A
{
public:
    A() {
        Count t(this);
    }
    virtual ~A() {}
};
Count c = new A();

这个时候就会出现问题,除非把Count构造的计数对象放到一个对象池中管理,但是又会增加对象查找的成本,所以最终放弃了这个想法。 另外一点就是,C/C++的指针在很多情况下是最方便的,过度的封装很可能会弄巧成拙,所以适度就好。

OK,惯例代码还是放到googlecode上: http://code.google.com/p/vimercode/source/browse/#svn%2Ftrunk%2Fptr_proxy 目前代码使用中没有发现明显问题,欢迎大家交流~

Pingbacks

Pingbacks已打开。

Trackbacks

引用地址

评论

  1. yjjlyyj

    yjjlyyj on #

    sf~

    Reply

  2. fanhe

    fanhe on #

    还是觉得c++加个垃圾回收机制比较好, c++0x也还没有, 只是加了个类似返回引用的东东.

    Reply

  3. clar

    clar on #

    应用场景是什么,
    共享化的资源为什么不借助单体模式?

    Reply

  4. clar

    clar on #

    何时我们需要智能指针?

    有三种典型的情况适合使用智能指针:

    * 资源所有权的共享
    * 要编写异常安全的代码时
    * 避免常见的错误,如资源泄漏

    共享所有权是指两个或多个对象需要同时使用第三个对象的情况。这第三个对象应该如何(或者说何时)被释放?为了确保释放的时机是正确的,每个使用这个共享资源的对象必须互相知道对方,才能准确掌握资源的释放时间。从设计或维护的观点来看,这种耦合是不可行的。更好的方法是让这些资源所有者将资源的生存期管理责任委派给一个智能指针。当没有共享者存在时,智能指针就可以安全地释放这个资源了。

    Reply

    1. Dante

      Dante on #

      嗯,这个的主要场景是用在了bayonet的状态机项目中,很多地方都有用到,举个最简单的例子:
      在一个容器里面有一堆指针,在某个逻辑中指针被delete掉了,但是我不想立即去在容器中erase,因为这样会增加容器的查找效率。我希望能每隔一段时间去对容器遍历一次进行资源回收。
      这种情况下就可以用到文中的指针代理,真实的指针已经为null了,我只要遍历一下容器中的指针代理,把那些为空的对象删除掉就行了。
      类似的还有很多,比如互指指针,单体无法解决这个问题,还有观察者模式,被观察者析构时通知观察者,等等。

      Reply

  5. Huang Yun

    Huang Yun on #

    一般做法是new多分配一点空间存放引用计数,不过这样实现的话必须要求每个类自己管理,也不方便。试试boost::shared_ptr

    Reply

  6. saalihmao

    saalihmao on #

    为何不考虑boost::shared_ptr?

    Reply

  7. saalihmao

    saalihmao on #

    boost::shared_ptr + boost::weak_ptr还可以解决循环引用的问题。

    用cpp终究躲不开template的

    Reply

    1. Dante

      Dante on #

      嗯,其实文中的使用场景本身不会出现循环引用的问题,因为释放是主动触发的。

      至于为什么不选用boost,目前在腾讯还是很少有项目使用boost的,毕竟boost还没有stl那么成熟。
      而且一个功能满足自己要求就好,不一定非要追求那些新的东西。

      Reply

      1. saalihmao

        saalihmao on #

        其实也不需要整个boost,boost::shared_ptr已经完全包含在tr1里面了。版本稍稍新一点的编译器(例如gcc 4.0.0以上,正常的svr的linux包含的gcc版本都不太可能低于4.0.0吧...)就支持的...
        std::tr1::shared_ptr和boost::shared_ptr完全一样,tr1也是标准库的一部分,而且也肯定是未来的cpp标准的一部分。
        使用这些著名库的好处是完善...虽然功能确实是够了,但是这些库的代码实际上要安全得多,考虑的情况也要多得多... 例如你的实现里面ptr_proxy构造函数应该区分默认和基于指针创建的情况,后者应该是explict,前者应该throw ()不分配内存保证无异常抛出,例如所有的分配空间的地方都应该有内存捕获保证已分配的空间被释放,例如应该考虑支持有些类的destructor不是public的情况,例如应该考虑支持stl的Allocator的自定义资源分配...等等,c++确实是恶心,所以库很重要啊,一直自己造轮子是不行的...

        Reply

  8. egmkang

    egmkang on #

    http://www.cnblogs.com/egmkang/archive/2011/09/26/2189548.html
    这是我写的文章,也可以用来避免野指针,需要自己delete资源.

    Reply

  9. Sparkfire

    Sparkfire on #

    还有一个万能的方法:自己new的东西就自己负责彻底的delete and set null。记得stdc里有个函数判断内存指针是否有效......忘记了。

    Reply

  10. 夜风

    夜风 on #

    这个是赋值函数,而不是构造函数

    /**
    * @brief copy构造函数
    *
    * @param rhs 被拷贝对象
    *
    * @return 自己的引用
    */
    ptr_proxy&amp; operator=(const ptr_proxy&amp; rhs)

    Reply

    1. Dante

      Dante on #

      不好意思,注释copy的时候忘记改了。

      Reply

发表评论