管理C++类中的指针成员

图论看的头大…于是翻了翻抱佛脚必备书:《程序员面试宝典》,这书编的确实不怎么样,边边角角的题目有点多,有些题目的解答思路很不清晰,当做题库看看也就罢了。今天翻到一道标准容器复制含有指针成员的类导致重复解析的问题,专门回忆了下这方面的知识,在这里做个总结。

C++最讽刺的地方就是“用指针实现了面向对象”这点,所以C++压根不是什么面向对象,说是面向指针更恰当一点。内存管理这块一直是C++最复杂的地方之一,也是很多人讨厌C++的最大原因之一(我猜C风格字符串是另外一个原因之一)。复制(或使用隐含复制的操作)含有指针成员的类,必须对指针成员做一些特殊的处理,主要方法包括以下几种:

①值型类,在复制指针成员时,重新分配内存,复制对象。使得不同的对象之间完全无耦合,配合C++11引入的右值引用,高效又省事。

②使用C++11中引入的新技术,包括shared_ptr,unique_ptr和weak_ptr(#include <memory>)。

③自己定义智能指针类,实现②中的功能。

我个人最喜欢还是用①,这么干最简单,如果加上垃圾回收器,基本就成了java。但是对于大规模数据运算的类,还是不要这么干为妙。方法②是比较方便的方法,前提是对这三个smart pointer有比较好的理解,而且你个人喜欢这个风格的书写。(其实我看着感觉不是很爽…)

其中unique_ptr被设计出来是用来取代auto_ptr的,shared_ptr就是一个使用计数类,配合weak_ptr来使用。详细的介绍可以直接看微软的文档,或者这个翻译的版本:http://kheresy.wordpress.com/2012/03/05/c11_smartpointer_p2/

如果讨厌stl的智能指针类,最后的方法就是自己实现这个玩意。C++prime 4th在13.5.1和15.8.1两节介绍了两个方法来实现使用计数,总结一下:

 1 class U_Ptr;
 2 class HasPtr
 3 {
 4 public:
 5     HasPtr(int *p,int i):ptr(new U_Ptr(p)),val(i){}
 6     HasPtr(const HasPtr &orig):
 7         ptr(orig.ptr),val(orig.val)
 8     {
 9         ++ptr->use;
10     }
11     HasPtr& operator=(const HasPtr&);
12     ~HasPtr(){if (--ptr->use==0)delete ptr;}
13     int *get_ptr()const{return ptr->ip;}
14     int get_int()const{return val;}
15     void set_ptr(int *p){ptr->ip=p;}
16     void set_int(int i){val=i;}
17     int get_ptr_val()const{return *ptr->ip;}
18     int set_ptr_val(int i)const{*ptr->ip=i;}
19 private:
20     U_Ptr *ptr;
21     int val;
22 };
23 class U_Ptr
24 {
25     friend class HasPtr;
26     int *ip;
27     size_t use;
28     U_Ptr(int *p):ip(p),use(1){}
29     ~U_Ptr(){delete ip;}
30 };
1 HasPtr& HasPtr::operator=(const HasPtr& other)
2 {
3     ++other.ptr->use;
4     if(--ptr->use==0)delete ptr;
5     ptr=other.ptr;
6     val=other.val;
7     return *this;
8 }

这里使用了一个计数类,用来实际存放本来应该在HasPtr中保存的指针,然后又使用了友元。这种设计风格会破坏掉类的封装,也就是所谓侵入式智能指针,故一般不推荐。

 1 #include <exception>
 2 #include <iostream>
 3 class BasePtr;
 4 class HandlePtr
 5 {
 6 public:
 7     HandlePtr():ptr(nullptr),use(new size_t(1)){}
 8     HandlePtr(const HandlePtr& i)
 9         :ptr(i.ptr),use(i.use)
10     {
11         ++*use;
12     }
13     HandlePtr(HandlePtr&& i)
14         :ptr(i.ptr),use(i.use)
15     {
16     }
17     HandlePtr(const BasePtr&);
18     ~HandlePtr(){decr_use();}
19     HandlePtr& operator=(const HandlePtr&);
20     const BasePtr* operator->()const
21     {
22         if(ptr)return ptr;
23         else throw std::logic_error("unbound HandlePtr");
24     }
25     const BasePtr& operator*()const
26     {
27         if(ptr)return *ptr;
28         else throw std::logic_error("unbound HandlePtr");
29     }
30 private:
31     BasePtr* ptr;
32     size_t *use;
33     void decr_use()
34     {
35         if(--*use==0){delete ptr;delete use;}
36     }
37 };
38 class BasePtr 
39 {
40 public:
41     virtual BasePtr* clone()const
42     {
43         return new BasePtr(*this);
44     } 
45 };
1 HandlePtr::HandlePtr(const BasePtr& other)
2     :ptr(other.clone()),use(new size_t(1)){}

上面则是更好的,也是比较常见的指针管理方式:使用句柄类。句柄类中包含指向管理的类和其子类的指针,句柄类重载了箭头和解引用操作符,使其指向实际管理的指针,完成动态绑定。句柄类的可以直接由基类引用初始化, 但是由于基类引用可能指向子类,所以必须定义虚克隆来返回实际的类型。

以上两个方法的核心思想都是“引用计数”,而shared_ptr也是这个原理。令人蛋疼的是,boost引入了大量智能指针,仅仅是掌握这些智能指针的用法就够头疼的,好在shared_ptr几乎可以解决所有的问题,所以它得到了最广泛的应用,掌握shared_ptr基本上可以解决大部分memory leak的问题(如果不需要考虑引用计数,可以使用unique_ptr)。

我试着用shared_ptr完成了书中sales_item的例子,如下:

 1 #include <string>
 2 #include <ostream>
 3 class Basket;
 4 class Item_base{
 5 public:
 6     Item_base(const std::string& book="",
 7                 double sales_price=0.0):
 8     isbn(book),price(sales_price){}
 9     std::string book()const
10     {
11         return isbn;
12     }
13     virtual double net_price(std::size_t n)const
14     {
15         return n*price;
16     }
17     virtual ~Item_base(){}
18 private:
19     std::string isbn;
20 protected:
21     double price;
22 };
23 
24 class Bulk_item:public Item_base{
25 public:
26     double net_price(std::size_t n)const;
27 private:
28     std::size_t min_qty;
29     double discount;
30 };
31 void print_total(std::ostream &os,const Item_base &item,std::size_t n);
#include "common.h"
#include <memory>
#include <set>
using namespace std;
double Bulk_item::net_price(std::size_t cnt)const
{
    if(cnt>=min_qty)
        return cnt*(1-discount)*price;
    else
        return cnt*price;
}
void print_total(ostream &os,const Item_base &item,size_t n)
{
    os<<"ISBN:"<<item.book()
        <<"\tnumber sold:"<<n<<"\ttotal price:"
        <<item.net_price(n)<<endl;
}
inline bool
    compare(const shared_ptr<Item_base>& lb1,const shared_ptr<Item_base>& lb2)
{
    return lb1->book() < lb2->book();
}
class Basket{
    typedef bool (*Comp)(const shared_ptr<Item_base>&,const shared_ptr<Item_base>&);
    multiset<shared_ptr<Item_base>,Comp> items;
public:
    typedef multiset<shared_ptr<Item_base>,Comp> set_type;
    typedef set_type::size_type size_type;
    typedef set_type::const_iterator const_iter;
    Basket():items(compare){}
    void add_item(const shared_ptr<Item_base> &pItem)
    {
        items.insert(pItem); 
    }
    size_type size(const shared_ptr<Item_base> &i)
    {
        return items.count(i);
    }
    double total()const
    {
        double sum=0.0;
        for(auto iter=items.begin();iter!=items.end()
            ;iter=items.upper_bound(*iter))
        {sum+=(*iter)->net_price(items.count(*iter));}
        return sum;
    }
};

大部分情况下,shared_ptr的使用比较简单,当做一个自己书写的句柄类使用即可。但是shared_ptr有一些陷阱,比如不能传递数组(但是可以用vector代替,或者指定删除器);不要在函数实参中初始化;最好不要把this指针传给shared_ptr,如果需要返回this,可以考虑使用继承std::enable_shared_from_this,然后返回shared_from_this()(但是这么做之前必须已经有一个正常产生的shared_ptr来存放这个返回的指针);在可能出现循环引用时,使用weak_ptr打断这种循环;如果是命名对象,最好不要使用new而使用make_shared;另外大量使用shared_ptr会产生碎片,需要自定义分配器进行内存管理。

posted @ 2012-09-12 21:39  生无所息  阅读(3690)  评论(0编辑  收藏  举报