最近用MFC写校车查询预约系统的电脑端,当我们精心设计好了几个主要的类和一些接口之后,却发现了一个棘手的问题,由于我们将所有的班次和学生放在了STL容器里,这样主要是为了查找修改方便,不用每次都跟数据库打交道了。除此之外,每个班次类内部还保存着一个预约该班次的所有学生的集合,但是,这样就带来了一个问题——究竟在容器里存入对象的指针还是对象的副本呢?
为什么会存在这种问题呢,首先如果在容器中存入对象的副本,那么就意味着同一个对象就会有N多个副本,如何维护才能保证所有指向同一个学生的对象始终保持一致呢?好像只有在修改其中一个对象的时候遍历所有包含这个对象的容器,找到这个对象并进行修改才可以实现,这显然并不是我们的初衷,因为问题变得复杂化了。其次,如果存入指针呢,当然存入指针这种问题根本不会存在,只要合理的控制析构就可以解决问题,但是这又会带来另一个问题,我们为什么要使用容器来保存这一系列对象,其中之一原因无非是为了查找简单,可以直接调用<algorithm>里的find函数就ok了,而且便于管理,那么问题来了,存入的是指针怎么查找呢?比较地址吗?这显然也不是我们想要的!
一个折衷的办法是在学生类里面添加一个指针,这个指针指向的地址始终与对象本身地址保持一致,然后班次类内部的容器存入学生指针,外部的保存所有学生的容器存入对象,问题貌似解决了,但并非如此,垂悬指针出现了。注意到这样做会导致多个指针指向同一个对象,但是这些指针却无法知道自己本身所指向的内存区域是否还是有效的。
问题好像变得严重了,难道我们辛辛苦苦写出来的程序全部要改掉吗?快到24点了,正要我们准备先放下这个问题回去休息的时候,我突然想到是不是可以在容器中存入对象的引用呢?回答宿舍我马上进行了验证,结果在意料之中,但构造一个引用类型的vector时就会有编译报错——指向引用的指针非法,果然,容器不可以存入引用类型。那该怎么办呢,无意之中我想到了“智能指针”可能可以解决这个问题,但是STL提供的auto_ptr限制太多,而且不支持将其存入容器,甚至复制操作都是有风险的。不过带着试试看的想法,我还是写了如下的测试程序:
#include "stdafx.h"
#include <vector>
#include <algorithm>
#include <stdio.h>
#include <iostream>
using namespace std;
class CTest{
public:
int m_a;
int m_b;
CTest(int a, int b) :m_a(a), m_b(b){ }
bool operator==(CTest another){ return (m_a == another.m_a) && (m_b == another.m_b); }
};
class CSmartPtr{
public:
CSmartPtr(CTest * ptr) :m_ptr(ptr){ }
bool operator==(CSmartPtr another){ return (*m_ptr) == (*(another.m_ptr)); }
CTest & operator *(){ return *m_ptr; }
private:
CTest * m_ptr;
};
int main()
{
CTest * pCTest = new CTest(1, 2);//构造一个CTest对象,在其中一个容器里修改其成员变量的值
CSmartPtr *testptr1 = new CSmartPtr(pCTest);
CSmartPtr *testptr2 = new CSmartPtr(pCTest);
vector<CSmartPtr> a;
vector<CSmartPtr> b;
a.push_back(*testptr1);
b.push_back(*testptr2);
(*(*(a.begin()))).m_a = 3;
vector<CSmartPtr>::iterator iter = find(b.begin(), b.end(),*(a.begin()));
if (iter != b.end()){//说明在容器b里找到了存入到容器1中的元素
//也就是说对其中一个做了改变另一个也对应发生了改变
std::cout << (*(*iter)).m_a<<endl;
}
delete pCTest;
delete testptr1;
delete testptr2;
return 0;
}
运行,可以发现输出为3。这说明我们这一个容器里对其中对象的成员变量做的修改也反映到了另一个容器的对象上,也就是说他们是同一个对象,并且可以很方便的进行查找,而这正是我们想要的!但这个CSmartPtr类还无法帮助我们实现指针的管理,因为他没有对new出来的指针进行delete,是我们手动调用的。下面给出一个完整的智能指针类,这是C++Primer里的参考程序,我们对其进行了改进以满足我们的需要。
#include <stdexcept>
//泛型句柄类 参考:C plus plus Edition4 修改:智调度,绿色行
template <class T> class Handle {
public:
//构造函数,调用示例Handle<Students> handle(new Students(1,2));
Handle(T *p = 0) : ptr(p), use(new size_t(1)) { }
//重载解引用、指向操作符方便获取保存的指针内容
T& operator*();
T* operator->();
const T& operator*() const;
const T* operator->() const;
//重载==操作符,方便在容器中进行查找
bool operator==(Handle<T> another){
return ((*ptr) == (*(another.ptr)));
}
//赋值控制,提供常规的复制构造,并将使用计数加一
Handle(const Handle& h) : ptr(h.ptr), use(h.use)
{
++*use;
}
//重载=操作符,详见定义
Handle& operator=(const Handle&);
//析构函数,必要时释放内存
~Handle() { rem_ref(); }
private:
T* ptr; // 指向实体的指针
size_t *use; // 使用量计数
void rem_ref() // 计数减一,必要时释放内存
{
if (--*use == 0) { delete ptr; delete use; }
}
};
template <class T>
inline Handle<T>& Handle<T>::operator=(const Handle &rhs)
{
++*rhs.use; // 防止自身赋值,先加一
rem_ref(); // 减回来,这在一般情况下对左操作数来说看似没有任何变化,除非左操作数是它本身
ptr = rhs.ptr;
use = rhs.use;
return *this;
}
template <class T> inline T& Handle<T>::operator*()
{
if (ptr) return *ptr;
throw std::runtime_error
("dereference of unbound Handle");
}
template <class T> inline T* Handle<T>::operator->()
{
if (ptr) return ptr;
throw std::runtime_error
("access through unbound Handle");
}
template <class T> inline
const T& Handle<T>::operator*() const
{
if (ptr) return *ptr;
throw std::runtime_error("dereference of unbound Handle");
}
template <class T> inline
const T* Handle<T>::operator->() const
{
if (ptr) return ptr;
throw std::runtime_error("access through unbound Handle");
}
除了可以自己实现一个可以满足需要的智能指针类外,我们还可以使用boost库提供的一系列智能指针,这些类就相当强大了。boost库提供了以下六种智能指针供我们使用:
scoped_ptr <boost/scoped_ptr.hpp>、scoped_array <boost/scoped_array.hpp>和auto_ptr类似,同样不支持复制以及多个指针拥有同一对象的引用,后者是用来支持使用new []操作符初始化的指针。
shared_ptr <boost/shared_ptr.hpp>、shared_array <boost/shared_array.hpp>则是我们较为常用的,支持复制操作,也允许多个指针拥有同一对象,但他不是完美的,在使用时必须要杜绝直接复制shared_ptr里指针指向对象的这类操作,否则有可能导致程序崩溃,原因很简单,你不可能保证你用来copy保存在shared_ptr里对象的变量始终不会发生析构,一旦发生析构,行为将是未定义,如果非要如此,用引用吧!这种情况多发生在函数的形参里。另外,shared_ptr也不允许构造两个指向同一对象的智能指针,这样做依然会导致其中一个智能指针在另一个析构之前将对象析构。但是,我们有时候确实希望能从一个对象的this指针构造出另外一个智能指针来使用又希望不会出现刚才说到的问题,这种情况下你可以试着让自己的类继承自Boost提供的一个助手类enable_shared_from_this<YourClass> 然后在类中定义一个从this指针获取智能指针对象的函数,返回shared_from_this()即可。
为了消除循环引用带来的问题,Boost库还提供了一个智能指针weak_ptr<boost/weak_ptr.hpp>,这种智能指针一般与shanred_ptr一起工作,但他不会增加引用计数,它只充当一个默默的观察者的作用,必要时还可以通过lock()方法获取他所观察的shanred_ptr对象,同时weak_ptr还提供了expired()方法用于判断他所观察的shanred_ptr是否已被析构。
最后是一种插入式的智能指针intrusive_ptr<boost/intrusive_ptr.hpp>他并不直接提供对其指向对象的引用计数,需要用户自己来实现引用计数然后由intrusive_ptr指针对象来调用,这就很好的解决了由同一个指针实例化多个智能指针对象所带来的问题,但在实际中,这种智能指针并不常用。