图论看的头大…于是翻了翻抱佛脚必备书:《程序员面试宝典》,这书编的确实不怎么样,边边角角的题目有点多,有些题目的解答思路很不清晰,当做题库看看也就罢了。今天翻到一道标准容器复制含有指针成员的类导致重复解析的问题,专门回忆了下这方面的知识,在这里做个总结。
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会产生碎片,需要自定义分配器进行内存管理。