幸运的是,出于某些原因,C++的标准库中至少引入了一种类型的智能指针,虽然在使用上有局限性,但是它刚好可以解决我们的这个难题,这就是标准库中唯一的一个智能指针::std::auto_ptr<>。
它将指针包装成了类,并且重载了反引用(dereference)运算符operator *和成员选择运算符operator ->,以模仿指针的行为。关于auto_ptr<>的具体细节,参阅《The C++ Standard Library》(中译本:C++标准库)。
例如以下代码,
#include < cstring >
#include < memory >
#include < iostream >
class string
{
public:
string(const char* cstr) { _data=new char [ strlen(cstr)+1 ]; strcpy(_data, cstr); }
~string() { delete [] _data; }
const char* c_str() const { return _data; }
private:
char* _data;
};
void foo()
{
::std::auto_ptr < string > str ( new string( " hello " ) );
::std::cout << str->c_str() << ::std::endl;
}
由于str是函数的局部对象,因此在函数退出点生存期结束,此时auto_ptr<string>的析构函数调用,自动销毁内部指针维护的string对象(先前在构造函数中通过new表达式分配而来的),并进而执行string的析构函数,释放为实际的字符串动态申请的内存。在string中也可能管理其他类型的资源,如用于多线程环境下的同步资源。下图说明了上面的过程。
进入函数foo 退出函数
| A
V |
auto_ptr<string>::auto<string>() auto_ptr<string>::~auto_ptr<string>()
| A
V |
string::string() string::~string()
| A
V |
_data=new char[] delete [] _data
| A
V |
使用资源 ---------------------> 释放资源
现在我们拥有了最简单的垃圾回收机制(我隐瞒了一点,在string中,你仍然需要自己编码控制对象的动态创建和销毁,但是这种情况下的准则极其简单,就是在构造函数中分配资源,在析构函数中释放资源,就好像飞机驾驶员必须在起飞后和降落前检查起落架一样。),即使在foo函数中发生了异常,str的生存期也会结束,C++保证自然退出时发生的一切在异常发生时一样会有效。
auto_ptr<>只是智能指针的一种,它的复制行为提供了所有权转移的语义,即智能指针在复制时将对内部维护的实际指针的所有权进行了转移,例如
auto_ptr < string > str1( new string( < str1 > ) );
cout << str1->c_str();
auto_ptr < string > str2(str1); // str1内部指针不再指向原来的对象
cout << str2->c_str();
cout << str1->c_str(); // 未定义,str1内部指针不再有效
某些时候,需要共享同一个对象,此时auto_ptr就不敷使用,由于某些历史的原因,C++的标准库中并没有提供其他形式的智能指针,走投无路了吗?
3.2 另一种智能指针
但是我们可以自己制作另一种形式的智能指针,也就是具有值复制语义的,并且共享值的智能指针。
需要同一个类的多个对象同时拥有一个对象的拷贝时,我们可以使用引用计数(Reference Counting/Using Counting)来实现,曾经这是一个C++中为了提高效率与COW(copy on write,改写时复制)技术一起被广泛使用的技术,后来证明在多线程应用中,COW为了保证行为的正确反而导致了效率降低(Herb Shutter的在C++ Report杂志中的Guru专栏以及整理后出版的《More Exceptional C++》中专门讨论了这个问题)。
然而对于我们目前的问题,引用计数本身并不会有太大的问题,因为没有牵涉到复制问题,为了保证多线程环境下的正确,并不需要过多的效率牺牲,但是为了简化问题,这里忽略了对于多线程安全的考虑。
首先我们仿造auto_ptr设计了一个类模板(出自Herb Shutter的《More Execptional C++》),
template < typename T >
class shared_ptr
{
private:
class implement // 实现类,引用计数
{
public:
implement(T* pp):p(pp),refs(1){}
~implement(){delete p;}
T* p; // 实际指针
size_t refs; // 引用计数
};
implement* _impl;
public:
explicit shared_ptr(T* p)
: _impl(new implement(p)){}
~shared_ptr()
{
decrease(); // 计数递减
}
shared_ptr(const shared_ptr& rhs)
: _impl(rhs._impl)
{
increase(); // 计数递增
}
shared_ptr& operator=(const shared_ptr& rhs)
{
if (_impl != rhs._impl) // 避免自赋值
{
decrease(); // 计数递减,不再共享原对象
_impl=rhs._impl; // 共享新的对象
increase(); // 计数递增,维护正确的引用计数值
}
return *this;
}
T* operator->() const
{
return _impl->p;
}
T& operator*() const
{
return *(_impl->p);
}
private:
void decrease()
{
if (--(_impl->refs)==0)
{ // 不再被共享,销毁对象
delete _impl;
}
}
void increase()
{
++(_impl->refs);
}
};
这个类模板是如此的简单,所以都不需要对代码进行太多地说明。这里仅仅给出一个简单的使用实例,足以说明shared_ptr<>作为简单的垃圾回收器的替代品。
void foo1(shared_ptr < int >& val)
{
shared_ptr < int > temp(val);
*temp=300;
}
void foo2(shared_ptr < int >& val)
{
val=shared_ptr < int > ( new int(200) );
}
int main()
{
shared_ptr < int > val(new int(100));
cout<<"val="<<*val;
foo1(val);
cout<<"val="<<*val;
foo2(val);
cout<<"val="<<*val;
}
在main()函数中,先调用foo1(val),函数中使用了一个局部对象temp,它和val共享同一份数据,并修改了实际值,函数返回后,val拥有的值同样也发生了变化,而实际上val本身并没有修改过。
然后调用了foo2(val),函数中使用了一个无名的临时对象创建了一个新值,使用赋值表达式修改了val,同时val和临时对象拥有同一个值,函数返回时,val仍然拥有这正确的值。
最后,在整个过程中,除了在使用shared_ptr < int >的构造函数时使用了new表达式创建新之外,并没有任何删除指针的动作,但是所有的内存管理均正确无误,这就是得益于shared_ptr<>的精巧的设计。
拥有了auto_ptr<>和shared_ptr<>两大利器以后,应该足以应付大多数情况下的垃圾回收了,如果你需要更复杂语义(主要是指复制时的语义)的智能指针,可以参考boost的源代码,其中设计了多种类型的智能指针。