前一小节《容器与继承》http://blog.csdn.net/thefutureisour/article/details/7744790提到过:
对于容器,假设定义为基类类型,那么则不能通过容器訪问派生类新增的成员;假设定义为派生类类型,一般不能用它承载基类的对象,即使利用类型转化强行承载,则基类对象能够訪问没有意义的派生类成员,这样做是非常危急的。对这个问题的解决的方法,是使用容器保存基类的指针。
在C++中,这类问题有一种通用的解决的方法,称为句柄类。它大体上完毕双方面的工作:
1.管理指针。这与智能指针的功能类似
2.实现多态。利用动态绑定,是得指针既能够指向基类,也能够指向派生类。
句柄类的设计须要重点考虑两个因素:
1.怎样管理指针
2.是否屏蔽它所管理的基类和派生类的接口。这意味着,假设我们充分了解继承成层次的接口,那么就能直接使用它们;要么我们将这些接口封装起来,使用句柄类自身的接口。
以下通过一个比較复杂的样例来说明这个问题:
这个样例的大体思路,是使用一个容器(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调用获得。