● 基类和派生类
一、定义基类
1.基类成员函数
除了构造函数之外,任意非static成员函数都可以是虚函数。保留字virtual只在类内部的成员函数声明中出现,不能用在类定义体外部出现的函数定义上。基类通常应将派生类需要重定义的任意函数定义为虚函数。
2.访问控制和继承
public和protected成员都可以被派生类访问,而private成员不能被派生类访问。
3.protected成员
像private成员一样,protected成员不能被类的用户访问。
像public成员一样,protected成员可被该类的派生类访问。
派生类只能通过派生类对象访问其基类的protected成员,派生类对其基类类型对象的protected成员没有特殊访问权限。
二、派生类
1.定义派生类
class classname : access-lable base-class
这里access-label是public、protected或private,base-class是已定义的类的名字。
访问权限如下:
2.派生类和虚函数
派生类一般会重定义所继承的虚函数。如果派生类没有重定义某个虚函数,则使用基类中定义的版本。
一旦函数在基类中声明为虚函数,它就一直为虚函数,派生类无法改变该函数为虚函数这一事实。派生类重定义虚函数时,可以使用virtual保留字,但不是必须这样做。
3.派生类对象
派生类对象由多个部分组成:派生类本身定义的(非static)成员加上由基类(非static)成员组成的子对象。C++语言不要求编译器将对象的基类部分和派生部分连续排列。
4.virtual与其他成员函数
C++中的函数默认不使用动态绑定。要触发动态绑定,必须满足两个条件:第一,只有指定为虚函数的成员函数才能进行动态绑定,成员函数默认为非虚函数,非虚函数不进行动态绑定;第二,必须通过基类类型的引用或指针进行函数调用。
在C++中,通过基类的引用(或指针)调用虚函数时,发生动态绑定。引用(或指针)即可以指向基类对象也可以指向派生类对象,这一事实是动态绑定的关键。用引用(或指针)调用的虚函数在运行时确定,被调用的函数是引用(或指针)所指对象的实际类型所定义的。
基类类型引用和指针的关键点在于静态类型(在编译时可知的引用类型或指针类型)和动态类型(指针或引用所绑定的对象的类型,这是仅在运行时可知的)可能不同。
引用和指针的静态类型和动态类型可以不同,这是C++用以支持多态性的基石。
如果调用非虚函数,则无论实际对象是什么类型,都执行基类类型所定义的函数。如果调用虚函数,则直到运行时才能确定调用哪个函数,运行的虚函数是引用所绑定的或指针所指向的对象所属类型定义的版本。
5.覆盖虚函数机制
使用作用域操作符可以覆盖虚函数机制。
double d = basep->Item::net_price(42);
这段代码强制将net_price调用确定为Item_base中定义的版本,该调用将在编译时确定。
6.派生类可以恢复继承成员的访问级别,但不能使访问级别比基类中原来指定的更严格或宽松。
class Base{
public:
std::size_t size() const {return n;}
protected:
std::size_t n;
};
class Derived: private Base{}
size在Base中为public,但在Derived中为private。为了使size在Derived中成为public,可以在Derived的public部分增加一个using声明。
class Derived: private Base{
public:
using Base::size:
protected:
using Base::n;
};
7.默认继承保护级别
使用class保留字定义的派生类默认具有private继承,而用struct保留字定义的类默认具有public继承。
● 友元关系与继承
像其他类一样,基类或派生类可以使其他类或函数称为友元,友元可以访问类的private和protected数据。
友元关系不能继承。基类的友元对派生类的成员没有特殊访问权限。如果基类被授予友元关系,则只有基类具有特殊访问权限,该基类的派生类不能访问授予友元关系的类。
如果基类定义了static成员,则整个继承层次中只有一个这样的成员。无论从基类派生出多少个派生类,每个static成员只有一个实例。
● 转换与继承
1.派生类到基类的转换
如果有一个派生类型的对象,则可以使用它的地址对基类类型的指针进行赋值或初始化。同样,可以使用派生类型的引用或对象初始化基类类型的引用。严
格说来,对对象没有类似转换。编译器不会自动将派生类型对象转换为基类类型对象。
但是,一般可以使用派生类型对象对基类对象进行赋值或初始化。对对象进行初始化和/或赋值以及可以自动转换引用或指针,这之间的区别是微妙的,必
须好好理解。
2.引用转换不同于转换对象
我们已经看到,可以将派生类型的对象传给希望接受基类引用的函数。也许会因此认为对象进行转换,但是,事实并非如此。将对象传给希望接受引用的函
数时,引用直接绑定到该对象,虽然看起来在传递对象,实际上实参是该对象的引用,对象本身未被复制,并且,转换不会在任何方面改变派生类型对象,该对象仍是派生类型对象。
将派生类对象传给希望接受基类类型对象(而不是引用)的函数时,情况完全不同。在这种情况下,形参的类型是固定的——在编译时和运行时形参都是基
类类型对象。如果用派生类型对象调用这样的函数,则该派生类对象的基类部分被复制到形参。
一个是派生类对象转换为基类类型引用,一个是用派生类对象对基类对象进行初始化或赋值,理解它们之间的区别很重要。
3.用派生类对象对基类对象进行初始化或赋值
对基类对象进行初始化或赋值,实际上是在调用函数:初始化时调用构造函数,赋值时调用赋值操作符。
第一种(虽然不太可能的)可能性是,基类可能显式定义了将派生类型对象复制或赋值给基类对象的含义,这可以通过定义适当的构造函数或赋值操作符实现。
class Derived;
class Base {
public:
Base(const Derived&); // create a new Base from a Derived
Base &operator=(const Derived&); // assign from a Derived
// …
};
基类一般(显式或隐式地)定义自己的复制构造函数和赋值操作符(第十三章),这些成员接受一个形参,该形参是基类类型的(const)引用。因为存在从派生类引用到基类引用的转换,这些复制控制成员可用于从派生类对象对基类对象进行初始化或赋值。
Item_base item; // object of base type
Bulk_item bulk; // object of derived type
// ok: uses Item_base::Item_base(const Item_base&) constructor
Item_base item(bulk); // bulk is “sliced down” to its Item_base portion
// ok: calls Item_base::operator=(const Item_base&)
item = bulk; // bulk is “sliced down” to its Item_base portion
4.从基类到派生类的自动转换是不存在的。
如果知道从基类到派生类的转换是安全的,就可以使用static_cast强制编译器进行转换。或者,可以用 dynamic_cast申请在运行时进行检查。
● 构造函数和复制控制
1.派生类构造函数
派生类的合成默认构造函数与非派生类的构造函数只有一点不同,除了初始化派生类的数据成员之外,它还初始化派生类对象的基类部分。基类部分由基类的默认构造函数初始化。
派生类的构造函数如果没有显式初始基类的构造函数,则会隐式调用Item_base的默认构造函数初始对象的基类部分。
也可以通过初始化列表来调用基类的构造函数并初始化基类中的成员。
一个类只能初始化自己的直接基类。
2.派生类复制构造函数
如果派生类显式定义自己的复制构造函数或复制操作符,则该定义将完全覆盖默认定义。被继承类的复制构造函数和赋值操作符负责对基类成分以及类自己的成员进行复制或赋值。
Derived(const Derived& d): Base(d) {}
初始化函数Base(d)将派生类对象d转换为它的基类部分的引用,并调用基类复制构造函数。
3.派生类复制操作符
复制操作符通常与复制构造函数类似:如果派生类定义了自己的赋值操作符,则该操作符必须对基类部分进行显式赋值。
Derived &Derived::operator=(const Derived &rhs)
{
if(this != &rhs){
Base::operator=(&rhs);
}
return *this;
}
4.派生类析构函数
派生类析构函数不负责撤销基类对象的成员。
对象的撤销顺序与构造顺序相反:首先运行派生类析构函数,然后按继承层次一次向上调用各基类析构函数。
5.虚析构函数
要保证运行适当的析构函数,基类中的析构函数必须为虚函数。
如果析构函数为虚函数,那么通过指针调用时,运行哪个析构函数将因指针所指对象类型的不同而不同。
如果基类为了将析构函数设为虚函数而具有空析构函数,那么类具有析构函数并不表示也需要赋值操作符或复制构造函数。
即使析构函数没有工作要做,继承层次的根类也应该定义一个虚析构函数。
● 名字查找与作用域
设计派生类时,只要可能,最好避免与基类成员的名字冲突。与基类成员同名的派生类成员将屏蔽对基类成员的直接访问。
在基类和派生类中使用同一名字的成员函数,其行为与数据成员一样:在派生类作用域中派生类成员将屏蔽基类成员。即使函数原型不同,基类成员也会被屏蔽。
局部作用域中声明的函数不会重载全局作用域中定义的函数,同样,派生类中定义的函数也不重载基类中定义的成员。通过派生类对象调用函数时,实参必须与派生类中定义的版本相匹配,只有在派生类根本没有定义该函数时,才考虑基类函数。
通过基类类型的引用或指针调用函数时,编译器将在基类中查找该函数而忽略派生类。
● 纯虚函数
将函数定义为纯虚函数能够说明,该函数为后代类型提供了可以覆盖的接口,但是这个类中的版本绝不会调用。含有(或继承)一个或多个纯虚函数的类是抽象基类。除了作为抽象类的派生类的对象的组成部分,不能创建抽象类型对象。
● 容器与继承
muliset<Item_base> basket;
Item_base base;
Bulk_item bulk;
basket.insert(base); //ok: add copy of base to basket
basket.insert(bulk); //ok: but bulk sliced down to its base part
加入派生类类型的对象时,只将对象的基类部分保存在容器中。记住,将派生类对象复制到基类对象时,派生类对象将被切掉。
● 句柄类与继承
对于容器,如果定义为基类类型,那么则不能通过容器访问派生类新增的成员;如果定义为派生类类型,一般不能用它承载基类的对象,即使利用类型转化强行承载,则基类对象可以访问没有意义的派生类成员,这样做是很危险的。对这个问题的解决办法,是使用容器保存基类的指针。
在C++中,这类问题有一种通用的解决办法,称为句柄类。它大体上完成两方面的工作:
1.管理指针。这与智能指针的功能类似
2.实现多态。利用动态绑定,是得指针既可以指向基类,也可以指向派生类。
句柄类的设计需要重点考虑两个因素:
1.如何管理指针。
2.是否屏蔽它所管理的基类和派生类的接口。这意味着,如果我们充分了解继承成层次的接口,那么就能直接使用它们;要么我们将这些接口封装起来,使用句柄类自身的接口。
下面引用网上的一个例子来说明这个问题:
这个例子的大体思路,是使用一个容器(multiset)来模拟一个购物车,里面装了许多书,有的书是原价销售的,有的书是打折销售的,并且打折销售也分为两种策略:买的多了才打折;买的少才打折,超出部分原价销售。最后能够方便的计算购买各种不同类型的书,在不同的打折条件下一共花了多少钱。
首先,是定义不同打折策略的书籍,它们时句柄类要管理的继承层次:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | //不使用折扣策略的基类 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); } }; |
1 2 3 4 5 6 7 8 | 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函数来返回一个自身的副本,在句柄类初始化时,会用得到它们。这里有一点需要注意:一般情况下,虚函数在继承体系中的声明应该是相同的,但是有一种例外情况:基类中的虚函数返回的是指向某一基类(并不一定是这个基类)的指针或者引用,那么派生类中的虚函数可以返回基类虚函数返回的那个基类的派生类(或者是它的指针或者引用)。
然后,我们定义一个句柄类里管理这个继承层次中的基类或者派生类对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | 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; } } }; |
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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函数来确定:函数返回的是什么类型,就是什么类型。
有了前面的铺垫,我们就可以编写真正的购物车类了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | //关联容器的对象必须定义<操作 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; }; |
1 2 3 4 5 6 7 8 9 10 11 | 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; } |
这个类在Sales_item对象的mutiset中保存顾客购买的商品,用multiset使顾客能够购买同一本书的多个副本。
对于关联容器,必须支持<操作,但是定义<操作并不好,因为我们的<是通过isbn序号判断的,而“==”,也改用isbn判断;可是按常理,只有isbn,价格,折扣生效数目,以及折扣率都相等时,才能算作相等,所以这样做很容易误导类的使用者。这里采取的办法是定义一个比较函数compare,把它定义成内联函数,因为每次向容器插入元素时,都要用到它。而将这个比较函数与容器关联起来的过程非常的“松散”,或者说,耦合度很低。
循环的遍历并不是使用iter++来完成的,而是使用iter = items.upper_bound(*iter)。对于multiset,upper_bound返回的是指向某一个键的最后一个元素的下一个位置,这样就可以一次处理同一本书。当然,这里的有一个前提,就是对于同一本书,它的折扣策略、折扣率以及达到折扣所满足的数量是一致的。