智能指针产生的原因
由于 C++ 语言没有自动内存回收机制,程序员每次 new 出来的内存都要手动 delete。但是 new 完之后做了一些事情,到最后旧经常会忘记delete。例如在一个类构造函数 new 在析构的时候才会释放。程序员在写 析构的时候不一定还记得 delete。
因此在 Poco c++ 中提供了一种引用计数机制实现的智能指针模板类。仔细阅读下面你就就可以自己实现一个 c++ 引用计数方式的智能指针。
poco::AutoPtr 使用的例子
以下为使用的例子。其中 xxx 为某个 class。
Poco::AutoPtr ptr = new xxx;
Poco::AutoPtr ptr2( new xxx);
Poco::AutoPtr ptr3 = ptr2;
在以上的代码中我们有两次 new 但是都不需要 delete.因为 AutoPtr 会在没有任何 AutoPtr 指针指向它的时候帮我们析构掉。
如果仅仅是简单的析构时候 delete 明显是会出错的。例如上面的例子中。ptr2 ,ptr3 是指向同一块内存的。如果 ptr3 在使用 而ptr2 析构的时候释放,那么会造成很严重的后果可能是崩溃。
这说明了什么?
说明了 Poco::AutoPtr 的智能指针不是简单的在析构的时候 delete 的。
应该是在最后一个指向 xxx 对象的 AutoPtr 消亡时候才 delete掉。也就是说需要一个引用计数机制,来记录了有几个智能指针指向 xxx 类型的对象。
下面介绍两个概念
引用计数:
1.无论何时一个引用被销毁或者重写,它所引用的对象的引用计数都会减少。
2.无论何时一个引用被创建或者被拷贝,它的引用的对象的引用计数都会增加。
3.初始时的引用计数是 1。
4.当一个对象的引用为 0 时,这个对象资源被销毁。
5.在多线程情况下增加和减少必须是原子操作。
对象拥有权:
1.任何对象都可能拥有一个或多个所有者。
2.只要一个对象至少还拥有一个所有者,它就会继续存在。
3.如果一个对象没有所有者,那么可以被销毁。
Poco::AutoPtr 的实现就是基于一个引用计数的思想。
因为需要 AutoPtr 指向的类拥有引用计数的一个功能。为了方便起见。我们先实现一个基类。后面的类如果要用到智能指针的,只要继承此引用计数类即可获得计数功能。
下面 是RefCountedObj 类代码。来源于 poco 只是为了让代码简洁,把计数器 _counter 类型换成 uint32_t。poco 里是一个 AutomicCounter 类支持多线程的原子 ++ ,–。这里为了理清整体实现,先不考虑多线程。
/**
* Copyright (c) 2015, xxx(版权声明)
*
* @file RefContedObj.h
* @brief 一个引用计数类
*
* 提供了 duplicate() 来增加引用计数。release() 来减少引用计数。
* 每次 release() 时候判断引用是否为 0 如果为0 那么 delete 资源。
* 析构函数为 protected 不允许 外部 delete 或者栈中使用。
*
* @author: yuhaiyang
* @date: 2015年4月24日
*
* 修订说明:初始版本
*/
#ifndef REFCOUNTEDOBJECT_H_
#define REFCOUNTEDOBJECT_H_
#include <stdint.h>
class RefCountedObject
{
public:
RefCountedObject();
/// Creates the RefCountedObject.
/// The initial reference count is one.
void duplicate() const;
/// Increments the object's reference count.
void release() const throw();
/// Decrements the object's reference count
/// and deletes the object if the count
/// reaches zero.
///< 用来释放 this 的方法。
int referenceCount() const;
/// Returns the reference count.
protected:
/// Destroys the RefCountedObject.
///< 此处声明为 protected 的意义是在栈内存不能使用。只能通过new 使用。
///< 不能使用 delete 释放,只有本类的 release() 或者派生类 release 能够delete。
virtual ~RefCountedObject()
{
}
private:
RefCountedObject(const RefCountedObject&);
RefCountedObject& operator = (const RefCountedObject&);
///< 引用计数, 在多线程注意互斥。
mutable uint32_t _counter;
};
//
// inlines
//
inline RefCountedObject::RefCountedObject()
:_counter(1)
{
}
inline int RefCountedObject::referenceCount() const
{
return _counter;
}
inline void RefCountedObject::duplicate() const
{
++_counter;
}
inline void RefCountedObject::release() const throw()
{
try
{
if (--_counter == 0) delete this;
}
catch (...)
{
}
}
#endif /* REFCOUNTEDOBJECT_H_ */
这个类提供了 Poco::AutoPtr 需要的 duplicate(), release(),这两个方法。
duplicate() 每次调用 _counter 都会加1 。release() 方法每次调用 _counter 都会减 1,并且如果 _counter是0 的话,就会调用 delete this ;就可以析构自己。
每次有新的只能指针指向这个对象的时候只要调用此对象duplicate()就可以了。
这里将类的析构声明为protected 的作用是不允许能自动析构的。只能调用 release() 先检测 _counter 是否为0 再来确定是否delete ,所以是安全的。 Poco::AutoPtr 每次析构也是调用 release()方法由对象自己的引用计数来判断是否释放。
有了引用计数类,也清楚了该怎么使用了,下面我们要开始分析 Poco::AutoPtr。
Poco::AutoPtr.
从上图可以看出来,想要使用智能指针的类,不需要每次自己实现一个引用计数类,都只要继承于 RefCountedObject 类就可以了。
AutoPtr构造和赋值:
1.当 AutoPtr 从 T* ,拷贝构造或者赋值的时候 AutoPtr 获取对象的拥有权。引用计数不变。
1)当从一个 T* 构造AutoPtr,AutoPtr拥有 T 的拥有权,引用计数不变。
2)当赋值一个 T* 到AutoPtr,AutoPtr拥有T的拥有权,引用计数不变。
2.当 AutoPtr 从 AutoPtr 构造或者赋值的时候这两个 AutoPtr 共同拥有对象的拥有权,引用计数增加。
1)当从另外一个AutoPtr构造AutoPtr,这两个AutoPtr共享T的拥有权,引用计数增加。
2)当赋值另一个AutoPtr到AutoPtr,这两个AutoPtr共享T的拥有权,引用计数增加。
上面1.1 要求我们重载AutoPtr 的从 T* 的构造函数
/**
* @brief :从一个 T* 构造一个 AutoPtr对象。
* @parm [in] 从 T* 的原生指针构造一个 AutoPtr 对象。
*/
AutoPtr( T* ptr ):_ptr( ptr )
{
}
1.2 要求我们重载从 T* 的 operator= 函数
/**
*@brief 使用 T* 来赋值给 AutoPtr .
*/
AutoPtr& operator= ( T* other )
{
return assign( other );
}
2.1 要求重载从 AutoPtr 对象构造一个AutoPtr 对象。注意这里有两个只能指针指向对象,所以要调用计数类的 duplicate()
/**
* @brief 从一个 AutoPtr对象 构造一个新的 AutoPtr 对象。
* @parm: [in] ptr 从
*/
AutoPtr( const AutoPtr& ptr ):_ptr( ptr._ptr )
{
if( _ptr )
{
//引用计数 ++
_ptr->duplicate();
}
}
2.2 要求重载从一个 AutoPtr 赋值到另一个 AutoPtr 类的。需要注意的是左值之前指向一个对象的话那么要先 release()。
/**
*@brief 使用 T* 来赋值给 AutoPtr .
*/
AutoPtr& operator= ( T* other )
{
return assign( other );
}
AutoPtr& assign( T* other )
{
///< 判断是否是本身。
if( _ptr != other )
{
///< 释放之前的内存。
if( _ptr )
{
_ptr->release();
}
_ptr = other;
}
return *this;
}
AutoPtr 支持的操作和语义:
1.Poco::AutoPtr支持相关的操作:==,!=,<,<=,>,>=。
2.当进行解引用操作:*,->,如果指针为空,抛出NullpointerException异常。
3.Poco::AutoPtr支持完全的值操作相关语义:默认构造函数,拷贝构造函数,赋值,它也可以被用在集合:std::vector等中。
4.使用AutoPtr::isNull()或者AutoPtr::operator !()测试是否指针为空。
下面是实现部分,只有基本功能,但是对于想搞清楚原理足够了。
/**
* Copyright (c) 2015,xxx(版权声明)
*
* @file AutoPtr.h
* @brief AutoPtr 的简单实现。
*
* 一个基于引用计数的只能指针,会自动释放资源。
*
* @author: yuhaiyang
* @date: 2015年4月24日
*
* 修订说明:初始版本
*/
#ifndef AUTOPTR_H_
#define AUTOPTR_H_
#include <stdio.h>
namespace My
{
class NullPointer : public std::exception
{
public:
NullPointer()
{
}
virtual const char* what( void ) const throw()
{
return "this Pointer is NULL";
}
///< 析构函数必须声明为不 抛出异常。
~NullPointer() throw()
{
}
};
/*
* @brief 智能指针模板类。
*
* 使用引用计数机制的智能指针。
* 想要使用引用计数机制的类必须是 RefCountedObject 类的派生类。
* 因为需要提供 duplicate() 方法,和 release() 方法。
*/
template <class T>
class AutoPtr
{
public:
/**
* @brief 默认的构造函数没有实际的值。
*/
AutoPtr( ):_ptr( NULL )
{
}
/**
* @brief :从一个 T* 构造一个 AutoPtr对象。
* @parm [in] 从 T* 的原生指针构造一个 AutoPtr 对象。
*/
AutoPtr( T* ptr ):_ptr( ptr )
{
}
/**
* @brief 从一个 AutoPtr对象 构造一个新的 AutoPtr 对象。
* @parm: [in] ptr 从
*/
AutoPtr( const AutoPtr& ptr ):_ptr( ptr._ptr )
{
if( _ptr )
{
//引用计数 ++
_ptr->duplicate();
}
}
/**
* @brief 析构的时候调用 _ptr->release() 计数减减。
*/
virtual ~AutoPtr( )
{
if( _ptr )
{
_ptr->release();
}
}
/**
* @brief 获取 ptr 对象。
*/
T* get( void )
{
return _ptr;
}
/ operator 重载 ///
/**
* @brief 取值操作
*/
T& operator*() const
{
if( _ptr )
{
return *_ptr;
}else
{
throw NullPointer();
}
}
/**
* @brief -> 的重载
*/
T* operator ->()
{
if( _ptr )
{
return _ptr;
}else
{
throw NullPointer();
}
}
/**
*@brief 使用 T* 来赋值给 AutoPtr .
*/
AutoPtr& operator= ( T* other )
{
return assign( other );
}
AutoPtr& assign( T* other )
{
///< 判断是否是本身。
if( _ptr != other )
{
///< 释放之前的内存。
if( _ptr )
{
_ptr->release();
}
_ptr = other;
}
return *this;
}
/**
* @brief 使用 AutoPtr 来赋值 AutoPtr。
*/
AutoPtr& operator=( AutoPtr &other )
{
return ( assign( other ) );
}
AutoPtr& assign( AutoPtr& other )
{
///< 判断是不是本身
if( this != &other )
{
if( _ptr )
{
_ptr->release();
}
///< 把当前类的 _ptr 指向 other._ptr 。
_ptr = other._ptr;
if( _ptr )
{
//引用计数增加。
_ptr->duplicate();
}
}
return *this;
}
/**
* @brief 关系运算符 ==
*/
bool operator == ( const AutoPtr& other ) const
{
return ( _ptr == other._ptr );
}
bool operator == ( T* ptr ) const
{
return ( _ptr == ptr );
}
/**
* @brief 关系运算符 !=
*/
bool operator != ( const AutoPtr& other) const
{
return ( _ptr != other._ptr );
}
bool operator != ( const T* ptr) const
{
return ( _ptr != ptr );
}
/**
* @brief 关系运算符小于,
*
* @warning 注意 AutoPtr 进行排序是按照地址进行的。这里设计不是很合理。
* 应该调用用户的 class T 重载的 operator< 而不单纯是地址比较。
*/
bool operator < ( const AutoPtr& other ) const
{
return ( _ptr < other._ptr );
}
bool operator < ( const T* ptr ) const
{
return ( _ptr < ptr );
}
/**
* @brief 关系运算符大于
*/
bool operator >( const AutoPtr& other ) const
{
return ( _ptr > other._ptr );
}
bool operator >( const T* ptr ) const
{
return ( _ptr > ptr );
}
/**
* @brief 强制类型转换操作符 (type)xxx。
*/
operator T* ()
{
return _ptr;
}
/**
* @brief 判断是否为空 。_ptr 是 NULL 返回 true,否则返回 false。
*/
bool operator ! () const
{
return _ptr == 0;
}
bool isNull() const
{
return _ptr == 0;
}
private:
T *_ptr;
};
} /* namespace My */
#endif /* AUTOPTR_H_ */
总结:
1.最好不要 先使用原始指针 new 然后赋值给 AutoPtr 指针。
例如:
C* p = new C;
AutoPtr ptr = p;
因为 p 是一个风险它不遵循 AutoPtr 的规矩。
更好的做法
AutoPtr ptr (new C);
2.智能指针的比较是指向的地址的值比较。
3.如果这个类只允许 new 出来的,那么析构声明为 protected。
4.由于 AutoPtr 指针是一个对象,所以 if( AutoPtr ) 一直都是真。判断指针内容是否为空使用 if( !ptr ) 这个取反方法的重载,或者使用 if( ptr.isNull ) 这个方法来判断 。