第十三章 类继承
Is_a关系的继承
如何以公有方式从一个类派生出另一个类
保护访问
构造函数成员初始化列表
向上和向下强制转换
虚成员函数
静态联编和动态联编
抽象基类
纯虚函数
何时及如何使用公有继承
1.成员初始化列表语法
TableTennisPlayer::TableTennisPlayer(const string&fn,const string&ln,bool ht)
{
firstname=fn;
lastname =ln;
hasTable=ht;
}
TableTennisPlayer::TableTennisPlayer(const string&fn,const string&ln,bool ht):
Firstname(fn),lastname(ln),hasTable(ht){}
2.派生类需要在继承特性中添加:
1.需要自己的构造函数
2.可以根据需要添加额外的数据成员和成员函数
3.派生类不能直接访问基类的私有成员,而必须通过基类方法访问。具体的说,派生类构造函数必须使用基类构造函数。创建派生类对象时,程序首先创建基类对象。这意味着基类对象应当在程序进入派生类构造函数之前被创建,C++使用成员初始化列表语法来完成这种工作。
RatedPlayer::RatedPlayer(unisigned int r,const string&fn,const string&ln,bool ht):TableTennisPlayer(fn,ln,ht)
{
rating=r;
}
如果不调用基类构造函数,程序将使用默认的基类构造函数
RatedPlayer::RatedPlayer(unsigned int r,const TableTennisPlayer&tp):TableTennisPlayer(tp)
{
rating=r;
}
这将调用基类的复制构造函数,如果需要但是没有,编译器将自动生成一个。
4.派生类和基类之间的特殊关系
1.派生类对象可以使用基类的方法,条件是方法不是私有的。
2.基类的指针可以在不进行显式类型转换的情况下指向派生类对象。
3.基类的引用可以在不进行显式类型转换的情况下引用派生类对象。
4.基类指针或引用只能用于调用基类方法。
5.可以将派生对象赋给基类对象
5.is-a关系
公有继承是最常用的方式,它建立一种is-a关系,即派生类对象也是一个基类对象,可以对基类对象执行的任何操作也可以对派生类对象执行。(is-a-kind-of)
在C++中 ,完全可以使用公有继承来建立has-a,is-implemented-as-a或uses-a关系,但是这样做通常会导致编程方面的问题。
6.多态公有继承
希望一个方法在派生类和基类中的行为是不同的
1.在派生类中重新定义基类的方法
2.使用虚方法
使用虚方法时,就算用基类指针指向派生类,调用函数时调用的版本仍然是对应的。
如果没有使用关键词virtual,程序将根据引用类型或者指针类型选择方法。
如果使用了virtual程序将根据引用或指针指向的对象的类型来选择方法。
如果ViewAcct()是虚的
Brass dom("Dominic Banker",12118,4183.45);
BrassPlus dot("Dorothy Banker",12118,25.92.00);
Brass &b1_ref=dom;
Brass &b2_ref=dot;
b1_ref.ViewAcct(); //use Brass::ViewAcct()
b2_ref.ViewAcct(); //use BrassPlus::ViewAcct()
7.基类声明虚析构函数,是为了确保释放派成对象时,按正确的顺序调用析构函数。
8.关键字virtual只用于类声明的方法原型中,不会位于类实现的程序中
9.非构造函数不能使用成员初始化列表语法,但派生类方法可以调用公有的基类方法。(要有作用域解析运算符)
10.可以使用基类指针数组来指向基类和继承类,这就是多态性。
11.因此,如果指针指向的是BrassPlus对象,将调用BrassPlus的析构函数,然后自动调用基类的析构函数。使用虚析构函数可以确保正确的析构函数被调用。
12.静态联编和动态联编:
执行代码块由编译器负责,将源代码中的函数调用解释为执行特定的函数代码块被称为函数名联编,在C++中,由于函数重载,编译器必须查看函数参数以及函数名才能确定使用哪个函数。在编译过程中进行联编被称为静态联编(早期联编)。C++中的虚函数使这项功能变得更困难,使用函数是不能在编译时决定的,因为编译器不知道用户的输入,所以,编译器必须生成能够在程序运行时选择正确的虚方法的代码,这被称为动态联编(晚期联编)
13.将派生类引用或指针转换为基类引用和指针被称为向上强制转换,这使公有继承不需要进行显式类型转换。改规则是is-a关系的一部分。
14.相反的过程,将基类指针或引用转换为派生类指针或引用,称为向下强制转换,如果不使用显式类型转换,则向下强制转换是不允许的。原因是is-a关系通常是不可逆的。
15.对于使用基类引用或者指针作为参数的函数调用,将进行向上转换。
void fr(Brass &rb);
void fp(Brass *pb);
void fv(Brass b);
BrassPlus bp("Betty Beep",232313,12345.0);
fr(bp);//use BrassPlus::ViewAcct();
fp(bp);//use BrassPlus::ViewAcct();
fv(bp);//use Brass::ViewAcct();
16.隐式向上转换使基类指针或引用可以指向基类对象或派生类对象,因此需要动态联编。C++使用虚成员函数来满足这种需求。
17.动态联编让您能够重新定义类方法,而静态联编在这方面很差,原因有两个——效率和概念模型。
18.虚函数的工作原理(vtbl:virtual function table):
通常,编译器处理虚函数的方法是:给每个对象添加一个隐藏成员。隐藏成员中保存了一个指向函数地址数组的指针。这种数组称为虚函数表。虚函数表中存储了为类对象进行声明的虚函数的地址。基类对象包含一个指针,指向基类所有虚函数的地址表。派生类将包含一个指向独立地址表的指针。如果派生类提供了虚函数的新定义,该虚函数表将保存新函数的地址;若无,将保存函数原始版本的地址。如果添加了新的虚函数,则该函数的地址也将被添加到vbtl中。
调用虚函数时,程序将查看存储在对象中的vtbl地址,然后转向响应的函数地址表。
19.构造函数:
构造函数不能是虚函数。创建派生类对象时,将调用派生类的构造函数,而不是基类的构造函数,然后派生类的构造函数将使用基类的一个构造函数,这种顺序不是继承机制。
20.析构函数:
析构函数应当是虚函数,除非类不用做基类。
Employee*pe = new Singer;
delete pe;
如果使用默认的静态联编,delete语句将调用~Employee()析构函数。这将释放由Singer对象中的Employee部分指向的内存,但不会释放新的成员指向的内存。
21.友元:
友元不能是虚函数,因为友元不是类成员。
22.重新定义将隐藏方法:
1.如果重新定义继承的方法,应确保与原来的原型完全相同,但如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针。(返回类型协变)
2.如果基类声明被重载了,则应该在派生类中重新定义所有的基类版本。
class Dwelling
{
public:
virtual void showperks(int a)const;
virtual void showperks(double x)const;
virtual void showperks()const;
}
class Hovel:public Dwelling
{
public:
virtual void showperks(int a)const;
virtual void showperks(double x)const;
virtual void showperks()const;
}
23.访问控制:protected
派生类的成员可以直接访问基类的保护成员,但不能访问基类的私有成员。
对于外部世界,保护成员的行为和私有成员类似,对于派生类,和公有成员相似。
24.抽象基类(ABC abstract base class):
从Ellipse和Circle类中抽象出它们的共性,将这些特性放到一个ABC中。然后从该ABC派生出Circle和Ellipse类。可以使用基类指针数组同时管理Circle和Ellipse对象。
C++通过使用纯虚函数提供未实现的函数。纯虚函数声明的结尾处为=0。
要成为真正的ABC,必须至少包含一个纯虚函数。
ABC描述的是至少使用一个纯虚函数的接口,从ABC派生出的类将根据派生类的具体特征,使用常规虚函数来实现这种接口。
25.继承和动态内存分配:
1.派生类不使用new
不需要为派生类定义显式析构函数、复制构造函数和赋值运算符。
2.派生类使用new
必须为派生类定义显式析构函数、复制构造函数和赋值运算符。
26.std::ostream & operator<<(std::ostream&os,const hasDMA & rs);
{
os<<(const baseDMA &)hs;
os<<"Style:"<<hs.style<<std::endl;
return os;
}
派生类在使用基类的友元时,要进行强制类型转换,以便匹配原型时能够选择正确的函数。
27.派生类不能从基类继承构造函数、析构函数、赋值运算符和友元。
28.假设baseDMA::operator=()的函数返回类型为void,而不是baseDMA&,仍可以使用单个赋值,但不能使用连续赋值:
baseDMA magazine("Pandering to Glitz",1);
baseDMA gift1,gift2,gift3;
gift1 = magazine; //ok
gift2 = gift3=gift1; //no longer valid
29.如果派生类构造函数使用new或new[]运算符来初始化类的指针成员,则应定义一个赋值运算符。