前一小节《容器与继承》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,价格,折扣生效数目,以及折扣率都相等时,才能算作相等,所以这样做很容易误导类的使用者。这里采取的办法是定义一个比较函数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调用获得。