前一小節《容器與繼承》http://blog.csdn.net/thefutureisour/article/details/7744790提到過:函數
對於容器,假設定義爲基類類型,那麼則不能經過容器訪問派生類新增的成員;假設定義爲派生類類型,通常不能用它承載基類的對象,即便利用類型轉化強行承載,則基類對象可以訪問沒有意義的派生類成員,這樣作是很是危急的。對這個問題的解決的方法,是使用容器保存基類的指針。this
在C++中,這類問題有一種通用的解決的方法,稱爲句柄類。它大致上完畢雙方面的工做:.net
1.管理指針。這與智能指針的功能相似設計
2.實現多態。利用動態綁定,是得指針既可以指向基類,也可以指向派生類。指針
句柄類的設計需要重點考慮兩個因素:code
1.怎樣管理指針對象
2.是否屏蔽它所管理的基類和派生類的接口。這意味着,假設咱們充分了解繼承成層次的接口,那麼就能直接使用它們;要麼咱們將這些接口封裝起來,使用句柄類自身的接口。blog
如下經過一個比較複雜的樣例來講明這個問題:繼承
這個樣例的大致思路,是使用一個容器(multiset)來模擬一個購物車,裏面裝了不少書,有的書是原價銷售的,有的書是打折銷售的,並且打折銷售也分爲兩種策略:買的多了纔打折;買的少纔打折,超出部分原價銷售。最後能夠方便的計算購買各類不一樣類型的書,在不一樣的打折條件下一共花了多少錢。接口
首先,是定義不一樣打折策略的書籍,它們時句柄類要管理的繼承層次:
//不使用折扣策略的基類 class Item_base { public: //構造函數 Item_base(const std::string &book = "",double sales_price = 0.0): isbn(book),price(sales_price){ } //返回isbn號 std::string book()const { return isbn; } //基類不需要折扣策略 virtual double net_price(std::size_t n)const { return n * price; } //析構函數 virtual ~Item_base(){}; virtual Item_base* clone()const { return new Item_base(*this); } private: std::string isbn; protected: double price; }; //保存摺扣率和購買數量的類 //它有兩個派生類,實現兩種折扣模式 class Disc_item:public Item_base { public: //默認構造函數 Disc_item(const std::string& book = "", double sales_price = 0.0,std::size_t qty = 0,double disc_rate = 0.0): Item_base(book,sales_price),quantity(qty),discount(disc_rate){} //純虛函數:防止用戶建立這個類的對象 double net_price(std::size_t)const = 0; //將買多少書與折扣率綁定起來 std::pair<std::size_t,double>discount_policy()const { return std::make_pair(quantity,discount); } //受保護成員供派生類繼承 protected: //實現折扣策略的購買量 std::size_t quantity; //折扣率 double discount; }; //批量購買折扣策略:大於必定的數量纔有折扣 class Bulk_item:public Disc_item { public: //構造函數 Bulk_item(const std::string& book = "",double sales_price = 0.0,std::size_t qty = 0,double disc_rate = 0.0): Disc_item(book,sales_price,qty,disc_rate){ } ~Bulk_item(){} double net_price(std::size_t)const; Bulk_item* clone()const { return new Bulk_item(*this); } }; //批量購買折扣策略:小於必定數量纔給折扣,大於的部分照原價處理 class Lds_item:public Disc_item { public: Lds_item(const std::string& book = "",double sales_price = 0.0,std::size_t qty = 0,double disc_rate = 0.0): Disc_item(book,sales_price,qty,disc_rate){ } double net_price(std::size_t cnt)const { if(cnt <= quantity) return cnt * (1 - discount) * price; else return cnt * price - quantity * discount * price; } Lds_item* clone()const { return new Lds_item(*this); } };
double Bulk_item::net_price(std::size_t cnt)const { if(cnt >= quantity) return cnt * (1 - discount) * price; else return cnt * price; }
當中基類是不打折的。基類的直接派生類添加了兩個成員,各自是購買多少書纔會打折的數量(或者是超過多少之後就不打折了的數量,這取決於它的派生類),以及折扣幅度。咱們把這個類定義爲了虛基類。經過將它的net_price定義爲純虛函數來完畢。定義爲虛基類的目的是因爲這個類並無實際的意義,咱們不想建立它的對象,而它的派生類,則詳細定義了兩種不一樣的打折策略。在基類和派生類中,都定義了clone函數來返回一個自身的副本,在句柄類初始化時,會用獲得它們。這裏有一點需要注意:普通狀況下,虛函數在繼承體系中的聲明應該是一樣的,但是有一種例外狀況:基類中的虛函數返回的是指向某一基類(並不必定是這個基類)的指針或者引用,那麼派生類中的虛函數可以返回基類虛函數返回的那個基類的派生類(或者是它的指針或者引用)。
而後,咱們定義一個句柄類裏管理這個繼承層次中的基類或者派生類對象:
class Sales_item { public: //默認構造函數 //指針置0,不與不論什麼對象關聯,計數器初始化爲1 Sales_item():p(0),use(new std::size_t(1)){} //接受Item_base對象的構造函數 Sales_item(const Item_base &item):p(item.clone()),use(new std::size_t(1)){} //複製控制函數:管理計數器和指針 Sales_item(const Sales_item &i):p(i.p),use(i.use){++*use;} //析構函數 ~Sales_item(){decr_use();} //賦值操做符聲明 Sales_item& operator=(const Sales_item&); //重載成員訪問操做符 const Item_base *operator->()const { if(p) //返回指向Item_base或其派生類的指針 return p; else throw std::logic_error(" unbound Sales_item"); } //重載解引操符 const Item_base &operator*()const { if(p) //返回Item_base或其派生類的對象 return *p; else throw std::logic_error(" unbound Sales_item"); } private: //指向基類的指針,也可以用來指向派生類 Item_base *p; //指向引用計數 std::size_t *use; //析構函數調用這個函數,用來刪除指針 void decr_use() { if(--*use == 0) { delete p; delete use; } } };
Sales_item& Sales_item::operator=(const Sales_item &rhs) { //引用計數+1 ++*rhs.use; //刪除原來的指針 decr_use(); //將指針指向右操做數 p = rhs.p; //複製右操做數的引用計數 use = rhs.use; //返回左操做數的引用 return *this; }
句柄類有兩個數據成員,各自是指向引用計數的指針和指向基類(或者是其派生類的指針)。還重載瞭解引操做符以及箭頭操做符用來訪問繼承層次中的對象。它的構造函數有3個:第一個是默認構造函數,建立一個引用計數爲1,指針爲空的對象;第三個是複製構造函數,讓指針指向實參指針所指向的對象,且引用計數+1;第二個構造函數的形參是一個基類的對象的引用,但是實參有多是基類對象也多是派生類對象,怎麼肯定呢?這裏經過基類和派生類中clone函數來肯定:函數返回的是什麼類型,就是什麼類型。
有了前面的鋪墊,咱們就可以編寫真正的購物車類了:
//關聯容器的對象必須定義<操做 inline bool compare(const Sales_item &lhs,const Sales_item &rhs) { return lhs->book() < rhs->book(); } class Basket { //指向函數的指針 typedef bool (*Comp)(const Sales_item&,const Sales_item&); public: typedef std::multiset<Sales_item,Comp> set_type; typedef set_type::size_type size_type; typedef set_type::const_iterator const_iter; //默認構造函數,將比較函數肯定爲compare Basket():items(compare){} //定義的操做: //爲容器加入一個對象 void add_item(const Sales_item &item) { items.insert(item); } //返回購物籃中返回ISBN的記錄數 size_type size(const Sales_item &i)const { return items.count(i); } //返回購物籃中所有物品的價格 double total()const; private: //關聯容器來儲存每一筆交易,經過指向函數的指針Comp指明容器元素的比較 std::multiset<Sales_item,Comp> items; };
double Basket::total()const { //儲存執行時的總價錢 double sum = 0.0; //upper_bound用以跳過所有一樣的isbn for(const_iter iter = items.begin();iter != items.end();iter= items.upper_bound(*iter)) { sum += (*iter)->net_price(items.count(*iter)); } return sum; }
購物車是使用multiset實現的,這意味着,一樣isbn的書籍是連續存放的。
對於關聯容器,必須支持<操做,可是定義<操做並很差,因爲咱們的<是經過isbn序號推斷的,而「==」,也改用isbn推斷;可是按常理,僅僅有isbn,價格,折扣生效數目,以及折扣率都相等時,才幹算做相等,因此這樣作很是easy誤導類的使用者。這裏採取的辦法是定義一個比較函數compare,把它定義成內聯函數,因爲每次向容器插入元素時,都要用到它。而將這個比較函數與容器關聯起來的過程很是的「鬆散」,或者說,耦合度很是低:
multiset<Sales_item,Comp> items;意味着咱們創建一個名爲items的關聯容器,容器的類型是Sales_item的。而且容器經過Comp指向的函數來推斷容器元素的大小。這意味着,在容器的構造函數中,經過將指向函數的指針初始化給不一樣的函數,就能實現不一樣的推斷操做。
這個類定義了3個函數,分別用來向購物車中添加新的書籍以及返回某個ISBN書的數量以及計算總的價格。當中total函數值得細緻說明一下:
首先是循環的遍歷並不是使用iter++來完畢的,而是使用iter = items.upper_bound(*iter)。對於multiset,upper_bound返回的是指向某一個鍵的最後一個元素的下一個位置,這樣就可以一次處理同一本書。固然,這裏的有一個前提,就是對於同一本書,它的折扣策略、折扣率以及達到折扣所知足的數量是一致的。
其次,循環體中的函數寫的很簡潔:iter解引得到的是Sales_item對象,利用定義的箭頭操做符可以訪問基類或者派生類的net_price函數,這個函數的派生類版本號需要一個代表有多少本書纔打折的實參,這個實參經過調用關聯容器的count調用得到。