MoreEffectiveC++Item35 条款28 Smart Pointers(智能指针)

一、我们来介绍一下智能指针

  智能指针(如标准库的auto_ptr,shared_ptr,weak_ptr,boost的scoped_ptr等)主要用于动态内存的管理,同时提供给用户与内置指针一样的使用方法

  本条款主要涉及智能指针在构造与析构,复制和赋值,解引等方面的注意点,于类似的实现,并非智能指针详细实现.

1.当智能指针作用于构造和析够函数时

2.智能指针的构造、赋值、和析够

3.智能指针的解引

智能指针的概念可能如下

template<class T>
class auto_ptr {
public:
  auto_ptr(T *ptr = 0): pointee(ptr) {}
  ~auto_ptr() { delete pointee; }
  ...
private:
  T *pointee;
};

 

那么我们现在考虑如下情况(拷贝构造和赋值)

 auto_ptr<TreeNode> ptn1(new TreeNode);
 auto_ptr<TreeNode> ptn2 = ptn1;      // 调用拷贝构造函数
                                      //会发生什么情况?
 auto_ptr<TreeNode> ptn3;
 ptn3 = ptn2;                         // 调用 operator=;
                                      // 会发生什么情况?

 

 

当我们调用拷贝构造函数时,我们会发现会有两个auto_ptr指向同一个 pointee.然而当释放是会造成一个对象被释放两次,这是一个不可预估的错误.

为了避免上述的情况我们禁止了auto_ptr的拷贝构造和赋值.即当auto_ptr被拷贝和赋值时,我们转交其所有权

template<class T>
class auto_ptr {
public:
  ...
  auto_ptr(auto_ptr<T>& rhs);        // 拷贝构造函数
  auto_ptr<T>&                       // 赋值
  operator=(auto_ptr<T>& rhs);       // 操作符
  ...
};
template<class T>
auto_ptr<T>::auto_ptr(auto_ptr<T>& rhs)
{
  pointee = rhs.pointee;             // 把*pointee的所有权
                                     // 传递到 *this
  rhs.pointee = 0;                   // rhs不再拥有
}                                    // 任何东西
template<class T>
auto_ptr<T>& auto_ptr<T>::operator=(auto_ptr<T>& rhs)
{
  if (this == &rhs)                  // 如果这个对象自我赋值
    return *this;                    // 什么也不要做
  delete pointee;                    // 删除现在拥有的对象
  pointee = rhs.pointee;             // 把*pointee的所有权
  rhs.pointee = 0;                   // 从 rhs 传递到 *this
  return *this;
}

 

这里有一个注意事项

// 这个函数通常会导致灾难发生
void printTreeNode(ostream& s, auto_ptr<TreeNode> p)
{ s << *p; }
int main()
{
  auto_ptr<TreeNode> ptn(new TreeNode);
  ...
  printTreeNode(cout, ptn);          //通过传值方式传递auto_ptr
  ...
}

 

当printTreeNode的参数p被初始化时(调用auto_ptr的拷贝构造函数),ptn指向对象的所有权被传递到给了p。当printTreeNode结束执行后,p离开了作用域,它的析构函数删除它指向的对象(就是原来ptr指向的对象)。然而ptr已不再指向任何对象(它的dumb pointer是null),所以调用printTreeNode以后任何试图使用它的操作都将产生未定义的行为.

Pass-by-reference是一个解决上述问题的一个较好的方法

// 这个函数的行为更直观一些
void printTreeNode(ostream& s,
                   const auto_ptr<TreeNode>& p)
{ s << *p; }

 

在函数里,p是一个引用,而不是一个对象,所以不会调用拷贝构造函数初始化p。当ptn被传递到上面这个printTreeNode时,它还保留着所指对象的所有权,调用printTreeNode以后还可以安全地使用ptn。所以通过const引用传递auto_ptr可以避免传值所产生的风险.


 

auto_ptr的析构的概念应该是这样

template<class T>
SmartPtr<T>::~SmartPtr()
{
  if (*this owns *pointee) {
    delete pointee;
  }
}

 


 

下面来介绍一下auto_ptr的解引

智能指针解引的核心部分为operator*和operator-> 函数,实现可能像这样

template<class T>
T& SmartPtr<T>::operator*() const
{
    perform "smart pointer" processing;
    return *pointee;
}

 

如果程序采用了lazy fetching策略,就有可能需要为pointers变换出一个新对象.需要注意的是,operator*返回的是引用,如果返回对象,可能会产生由于SmartPtr指向的是T的派生类对象而非T类对象而造成的切割问题

对于使用引用计数的shared_ptr,问题还未停止,它允许多个智能指针共享相同对象,但前提是这些指针所指向的对象相同.由于operator*和operator->返回所指对象的引用和指针,这可能导致其所指对象被更改,但原则上共享同一块内存的其他智能指针却要求所指对象保持不变.因此有必要在调用operator*和operator->的时候开辟一块新内存,使调用operator*和operator->的智能指针指向这块新内存以防止共享内存被篡改,像这样:

template<class T>
T& SmartPtr<T>::operator*() const{
    if(number of reference!=1){
        pointee=new T(*pointee);
        --reference number of the old object;
        set the reference number of the new object to 1; 
    }
    return *pointee;
}

 

测试智能指针是否为NULL

目前为止我们讨论的函数能让我们建立、释放、拷贝、赋值、dereference灵巧指针。但是有一件我们做不到的事情是“发现灵巧指针为NULL”:

SmartPtr<TreeNode> ptn;
...
if (ptn == 0) ...                    // error!
if (ptn) ...                         // error!
if (!ptn) ...                        // error!

 

在智能指针类里加入一个isNull成员函数是一件很容易的事,但是没有解决当测试NULL时智能指针的行为与dumb pointer不相似的问题。另一种方法是提供隐式类型转换操作符,允许编译上述的测试。一般应用于这种目的的类型转换是void* :因为灵巧指针被隐式地转换为void*指针

template<class T>
class SmartPtr {
public:
  ...
  operator void*();                  // 如果智能指针为null,
  ...                                // 返回0, 否则返回
};                                   // 非0。
SmartPtr<TreeNode> ptn;
...
if (ptn == 0) ...                    // 现在正确
if (ptn) ...                         // 也正确
if (!ptn) ...                        // 正确

 

有一种两全之策可以提供合理的测试null值的语法形式,同时把不同类型的灵巧指针之间进行比较的可能性降到最低。这就是在灵巧指针类中重载operator!,当且仅当灵巧指针是一个空指针时,operator!返回true:

template<class T>
class SmartPtr {
public:
  ...
  bool operator!() const;            // 当且仅当灵巧指针是
  ...                                // 空值,返回true。
};
//用户调用时
SmartPtr<TreeNode> ptn;
...
if (!ptn) {                          // 正确
  ...                                // ptn 是空值
}
else {
  ...                                // ptn不是空值
}

 

但是

if (ptn == 0) ...                    // 仍然错误
if (ptn) ...                         // 也是错误的

幸好程序员不会经常这样编写代码。有趣的是,iostream库的实现除了提供void*隐式的类型转换,也有operator!函数,不过这两个函数通常测试的流状态有些不同。(在C++类库标准中(参见Effective C++ 条款49和本书条款M35),void*隐式的类型转换已经被bool类型的转换所替代,operator bool总是返回与operator!相反的值)


 

把智能指针转变成dumb指针

有时要兼容并未使用智能指针的程序库,就要允许智能指针到内置指针的转换,直接的思路还是隐式转换操作符:

template<class T>
class DBPtr {
public:
    ...
    operator T*() { return pointee; }
    ...
};

 

但是正如多次强调的,隐式转换操作符很容易被滥用,它使得客户可以轻易获得内置指针,从而绕过智能指针的控制,像这样:

class Tuple{...};
void processTuple(DBPtr<Tuple>& pt)
{
    Tuple *rawTuplePtr = pt; // 得到内置指针
    use rawTuplePtr to modify the tuple
}

 

这样的也会被编译通过

DBPtr<Tuple> pt=new Tuple;
delete pt;//通过,执行隐式类型转换

 

以上的操作会造成相同的对象呗被释放两次,会造成不可预估的错误.如果在"引用计数"情况下那将造成计数方面的错误

即使实现了隐式转换操作符,但它还是不能做到提供和内置指针完全一样的行为,因为编译器禁止连续隐式调用自定义的隐式类型转换,像这样的使用会失败:

class TupleAccessors {
public:
    TupleAccessors(const Tuple *pt); // Tuple到TupleAccessor的转换
    ... 
};
TupleAccessors merge(const TupleAccessor& ta1,const TupleAccessors& ta2);
DBPtr<Tuple> pt1, pt2;
...
merge(pt1,pt2);//调用会出错

 

解决方法是使用普通成员函数进行显式转换以代替隐式转换操作符,像这样

class DBPtr {
public:
    ...
    T* toPrimary() { return pointee; }
    ...
};

 

智能指针于"与继承有关"的类型转换

两个类之间有继承关系,但以这两个类为参数具现化的类模板却没有继承关系,由于智能指针是类模板,因此智能指针的包装会屏蔽内置指针的继承关系,例如对于以下继承层次:

class MusicProduct {
public:
    MusicProduct(const string& title);
    virtual void play() const = 0;
    virtual void displayTitle() const = 0;
    ...
};
class Cassette: public MusicProduct {
public:
    Cassette(const string& title);
    virtual void play() const;
    virtual void displayTitle() const;
    ...
};
class CD: public MusicProduct {
public:
    CD(const string& title);
    virtual void play() const;
    virtual void displayTitle() const;
    ...
}
void displayAndPlay(const MusicProduct* pmp, int numTimes)
{
    for (int i = 1; i <= numTimes; ++i) {
    pmp->displayTitle();
    pmp->play();
}

 继承关系如下

//由于各个类的继承关系,可以利用指针的多态实现面向对象编程,像这样:
Cassette *funMusic = new Cassette("Alapalooza");
CD *nightmareMusic = new CD("Disco Hits of the 70s");
displayAndPlay(funMusic, 10);
displayAndPlay(nightmareMusic, 0);
//但当指针经过封装成为智能指针之后,正如开始所说,以下代码将无法通过编译
void displayAndPlay(const SmartPtr<MusicProduct>& pmp,int numTimes);
SmartPtr<Cassette> funMusic(new Cassette("Alapalooza"));
SmartPtr<CD> nightmareMusic(new CD("Disco Hits of the 70s"));
displayAndPlay(funMusic, 10); // 错误!
displayAndPlay(nightmareMusic, 0); // 错误!

 

这是由于MusicProduct,Cassette,CD之间有继承关系,但智能指针SmartPtr<MusicProduct>,SmartPtr<Cassette>,SmartPtr<CD>之间却没有内在的继承关系

解决方法是为每一个智能指针类定义一个隐式类型转换操作符,像这样:

class SmartPtr<Cassette> {
public:
    operator SmartPtr<MusicProduct>()
    { return SmartPtr<MusicProduct>(pointee); }
    ...
private:
    Cassette *pointee;
};
class SmartPtr<CD> {
public:
    operator SmartPtr<MusicProduct>()
    { return SmartPtr<MusicProduct>(pointee); }
    ...
private:
    CD *pointee;
};

种方法可以解决类型转换的问题,但是却治标不治本:一方面,必须为每一个智能指针实例定义隐式类型转换操作符,这无疑与模板的初衷背道相驰;另一方面,类的继承层次可能很庞大,采用以上方式,继承层次的最底层类的负担将会非常大——必须为对象直接或间接继承的每一个基类提供隐式类型转换操作符.

"将nonvirtual member function声明为templates"是C++后来接入的一个性质,使用它可以从根本上解决饮食类型转换的问题,像这样:

template<class T> 
class SmartPtr { 
public:
    SmartPtr(T* realPtr = 0);
    T* operator->() const;
    T& operator*() const;
    template<class newType> // 模板成员函数
    operator SmartPtr<newType>() 
    {
        return SmartPtr<newType>(pointee);
    }
    ...
};

这个成员函数模板将智能指针之间的隐式类型转换交由底层内置指针来完成,保证了指针转换的"原生态":如果底层指针能够转换,那么包装后的智能指针也能够进行转换.唯一的缺点是它是通过指针之间的隐式类型转换来实现指针的多态,也就是说,它实际上并不能区分对象之间的继承层次,假如扩充MusicProduct的继承体系,加上一个新的CasSingle class,像这样

template<class T>
class SmartPtr { ... }; 
void displayAndPlay(const SmartPtr<MusicProduct>& pmp,
int howMany);
void displayAndPlay(const SmartPtr<Cassette>& pc,
int howMany);
SmartPtr<CasSingle> dumbMusic(new CasSingle("Achy Breaky Heart"));
displayAndPlay(dumbMusic, 1);//错误,隐式类型转换函数的调用具有二义性,因其不知道具体该调用哪一个模板成员函数

 

 正如之前所言,使用隐式类型转换操作符实现的指针多态并不能区分对象的继承层次,也就是说将SmartPtr<CasSingle>转为SmartPtr<Cassette>&和转为SmartPtr<MusicProduct>&具有同样的优先级,因此造成二义性.而内置指针却能做到这一点,它优先将CasSingle绑定到Cassette&,因为CaSingle直接继承自Cassette.此外,以上策略还有移植性不高的缺点:有些编译器可能并不支持member templates.


 

智能指针于const

 回顾一下const

CD goodCD("Flood");
const CD *p; // p 是一个non-const 指针,指向 const CD 对象
CD * const p = &goodCD; // p 是一个const 指针,指向non-const CD 对象;因为 p 是const,它必须在定义时就被初始化
const CD * const p = &goodCD; // p 是一个const 指针,指向一个 const CD 对象

但对于智能指针,只有一个地方可以放置const,因此cosnt只能施行于指针之上,而不能施行于指针所指对象之上,像这样

const SmartPtr<CD> p=&goodCD;

 

 若使其const修饰对象

SmartPtr<const CD> p=&goodCD;

现在我们可以建立const和non-const对象和指针的四种不同组合:

SmartPtr<CD> p;                          // non-const 对象
                                         // non-const 指针
SmartPtr<const CD> p;                    // const 对象,
                                         // non-const 指针
const SmartPtr<CD> p = &goodCD;          // non-const 对象
                                         // const指针
const SmartPtr<const CD> p = &goodCD;    // const 对象
                                         // const 指针

 

 但是美中不足的是,使用dumb指针我们能够用non-const指针初始化const指针,我们也能用指向non-cosnt对象的指针初始化指向const对象的指针;就像进行赋值一样。例如:

CD *pCD = new CD("Famous Movie Themes");
const CD * pConstCD = pCD;               // 正确

//但是如果我们试图把这种方法用在灵巧指针上
SmartPtr<CD> pCD = new CD("Famous Movie Themes");
SmartPtr<const CD> pConstCD = pCD;       // 错误

使用之前的隐式类型转换技术可以顺带解决这个问题,但又有所区别,带const的类型转换是单向的:从non-const到const的转换是安全的,但是从const到non-const则不是安全的。而且用const指针能做的事情,用non-const指针也能做,但是用non-const指针还能做其它一些事情(例如,赋值操作)。同样,用指向const对象的指针能做的任何事情,用指向non-const对象的指针也能做到,但是用指向non-const对象的指针能够完成一些指向const对象的指针所不能完成的事情(例如,赋值操作)

template<class T> // 指向const 对象的
class SmartPtrToConst {
protected:
    union {
        const T* constPointee; // 提供给SmartPtrToConst 访问
        T* pointee; // 提供给SmartPtr 访问
    };
};
template<class T> 
class SmartPtr: public SmartPtrToConst<T> {
public:
    template<class constType>
    operator SmartPtrToConst<constType>();
    ... //没有额外数据成员
};

 

 SmartPtrToConst使用了union,这样constPointee和pointee共享同一块内存SmartPtrToConst使用constPointee,SmartPtr使用pointee.

 总结:智能指针代价高但是收益也很大,与其强大的功能相比在多数情况下还是值得的.还有一点,智能指针无论如何也不能完全替代内置指针.

 

posted @ 2017-06-25 22:54  WangZijian  阅读(267)  评论(0)    收藏  举报