圖論看的頭大…於是翻了翻抱佛腳必備書:《程序員面試寶典》,這書編的確實不怎么樣,邊邊角角的題目有點多,有些題目的解答思路很不清晰,當做題庫看看也就罷了。今天翻到一道標准容器復制含有指針成員的類導致重復解析的問題,專門回憶了下這方面的知識,在這里做個總結。
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會產生碎片,需要自定義分配器進行內存管理。