第13章 类继承
1.一个简单的基类
class RatedPlayer:public TableTennisPlayer
{
...
};
(1)上述特殊的声明头表明TableTennisPlayer是一个公有基类,这被称为公有派生。派生类对象包含基类对象。使用公有派生,基类的公有成员将成为派生类的公有成员;基类的私有部分也将成为派生类的一部分,但只能通过基类的公有和保护方法访问。
(2)Ratedplayer对象将具有以下特征:
1)派生类对象存储了基类的数据成员(派生类继承了基类的实现);
2)派生类对象可以使用基类的方法(派生类继承了基类的接口)
(3)继承特性
1)派生类需要自己的构造函数;
2)派生类可以根据需要添加额外的数据成员和成员函数
RatedPlayer::RatedPlayer(unsigned int r, const string &fn, const string &ln,bool ht)
{
rating=r; //:TableTennisPlayer()使用默认的基类构造函数
}
RatedPlayer::RatedPlayer(unsigned int r, const TableTennisPlayer & tp):TableTennisPlayer(tp)
{
rating=r; //显式调用正确的基类构造函数
}
RatedPlayer::RatedPlayer(unsigned int r, const TableTennisPlayer &tp):TableTennisPlayer(tp),rating(r)
{
} //显式调用基类构造函数,对派生类成员使用成员初始化列表
(4)有关派生类构造函数的要点:
1)首先创建基类对象;
2)派生类构造函数应通过成员初始化列表将基类信息传递给基类构造函数;
3)派生类构造函数应初始化派生类新增的数据成员
(5)派生类和基类之间的特殊关系
1)派生类对象可以使用基类的方法,条件是方法不是私有的
2)基类指针可以在不进行显式类型转换的情况下指向派生类对象;
3)基类引用可以在不进行显式类型转换的情况下引用派生类对象
RatedPlayer rplayer1(1140,"Mallory","Duck",true);
TableTennisPlayer & rt=rplayer;
TableTennisPlayer * pt=&rplayer;
rt.Name();
pt->Name();
2.继承:is-a关系
C++有3种继承方式:公有继承、私有继承和保护继承;
公有继承是最常用的方式,它建立一种is-a关系,即派生类对象也是一个基类对象,可以对基对象执行的任何操作,也可以对派生类对象执行
3.多态公有继承
(1)多态:方法的行为应取决于调用该方法的对象,这种较复杂的行为称为多态——具有多种形态,即同一个方法的行为随上下文而异
(2)虚方法:经常在基类中将派生类会重新定义的方法声明为虚方法,方法在基类中被声明为虚的后,它在派生类中将自动成为虚方法
class Brass
{
private:
std::string fullName;
long acctNum;
double balance;
public:
Brass(const std::string & s = "Nullbody", long an = -1, double bal = 0.0);
void Deposit(double amt);
virtual void Withdraw(double amt);
double Balance() const;
virtual void ViewAcct() const;
virtual ~Brass() {}
};
class BrassPlus :public Brass
{
private:
double maxLoan;
double rate;
double owesBank;
public:
BrassPlus(const std::string & s = "Nullbody", long an = -1, double bal = 0.0, double ml = 500, double r = 0.11125);
BrassPlus(const Brass &ba, double ml = 500, double r = 0.11125);
virtual void ViewAcct() const;
virtual void Withdraw(double amt);
void ResetMax(double m) { maxLoan = m; }
void ResetRate(double r) { rate = r; }
void ResetOwes() { owesBank = 0; }
};
注意:如果要在派生类中重新定义基类的方法,通常应将基类方法声明为虚的,这样,程序将根据对象类型而不是引用或指针的类型来选择方法版本,为基类声明一个虚析构函数也是一种惯例
(3)基类声明了一个虚析构函数,这样做的目的是为了确保释放派生对象时,按正确的顺序调用析构函数
(4)设置格式的代码:
//formatting stuff
typedef std::ios_base::fmtflags format;
typedef std::streamsize precis;
format setFormat();
void restore(format f, precis p);
函数setFormat()设置定点表示法并返回以前的标记设置:
format setFormat()
{
//set up ###.## format
return cout.setf(std::ios_base::fixed,std::ios_base::floatfield);
}
函数restore()重置格式和精度:
void restore(format f, precis p)
{
cout.setf(f, std::ios_base::floatfield);
cout.precision(p);
}
4.静态联编和动态联编
(1)定义
函数名联编(binding):将源代码中的函数调用解释为执行特定的函数代码块称为函数名联编
静态联编(static binding):在编译过程中进行联编被称为静态联编,又称为早期联编(early binding)
动态联编(dynamic binding):编译器必须生成能够在程序运行时选择正确的虚方法的代码,这被称为动态联编(dynamic binding),又称为晚期联编(late binding)
(2)向上强制转换和向下强制转换
class Employee
{
private:
char name[40];
...
public:
void show_name();
...
};
class Singer: public Employee
{
...
public:
void range();
...
};
Employee veep;
Singer trala;
...
Employee *pe=&trala; //允许向上隐式类型转换
Singer *ps=(Singer *)&veep; //必须向下显示类型转换
...
pe->show_name(); //向上转换带来安全操作,因为Singer是Employee(每个singer都继承姓名)
ps->range(); //向下转换可能带来不安全的操作,因为Employee并不是Singer(Employee有range()方法)
向上强制转换(upcasting):将派生类引用或指针转换为基类引用或指针(指向基类的引用或指针可以引用派生类对象),这使公有继承不需要进行显式类型转换(可传递)
BrassPlus dilly("Annie Dill",493222,2000);
Brass *pb=&dilly; //ok
Brass &rb=dilly; //ok
向下强制转换(downcasting):将基类指针或引用转换为派生类指针或引用,显式类型转换
对于使用基类引用或指针作为参数的函数调用,将进行向上转换,这里假定每个函数都调用虚方法ViewAcct():
void fr(Brass &rb); //uses rb.ViewAcct()
void fp(Brass *pb); //uses pb->ViewAcct()
void fv(Brass b); //uses b.ViewAcct()
int main()
{
Brass b("Billy Bee",123432,10000.0);
BrassPlus bp("Betty Beep",232313,12345.0);
fr(b); //uses Brass::ViewAcct()
fr(bp); //uses BrassPlus::ViewAcct()
fp(b); //uses Brass::ViewAcct()
fp(bp); //uses BrassPlus::ViewAcct()
fv(b); //uses Brass::ViewAcct()
fv(bp); //uses Brass::ViewAcct()
...
}
其中,按值传递导致只将BrassPlus对象的Brass部分传递给函数fv()
(3)虚成员函数
隐式向上强制转换使基类指针或引用可以指向基类对象或派生类对象,因此需要动态联编,C++使用虚成员函数来满足这种需求
注意:如果要在派生类中重新定义基类的方法,则将它设置为虚方法;否则,设置为非虚方法
(4)虚函数的工作原理
编译器处理虚函数的方法是:给每个对象添加一个隐藏成员,隐藏成员中保存了一个指向函数地址数组的指针,这种数组称为虚函数表(virtual function table, vtbl),虚函数表中存储了为类对象进行声明的虚函数的地址
注意:无论类中包含的虚函数是1个还是10个,都只需要在对象中添加一个地址成员,只是表的大小不同而已
(5)成本(内存、执行速度)
a.每个对象都将增大,增大量为存储地址的空间;
b.对于每个类,编译器都创建一个虚函数地址表(数组);
c.对于每个函数调用,都需要执行一项额外的操作,即到表中查找地址
(6)有关虚函数注意事项
a.构造函数不能是虚函数
b.析构函数应该是虚函数
注意:通常应该給基类提供一个虚析构函数,即使它并不需要析构函数
virtual ~BaseClass() { }
c.友元:友元不是虚函数,因为友元不是类成员,而只有成员才能是虚函数
(7)重新定义将隐藏方法
1)如果重新定义继承的方法,应确保与原来的原型完全相同,但如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针(这种例外是新发现的),这种特性被称为返回类型协变(convariance of return type),因为允许返回类型随类类型的变化而变化:
class Dwelling
{
public:
// a base method
virtual Dwelling & build(int n);
...
};
class Hovel:public Dwelling
{
public:
//a derived method with a covariant return type
virtual Hovel & build(int n); //same function signature
...
};
注意:这种例外只适用于返回值,而不适用于参数
2)如果基类声明被重载了,则应在派生类中重新定义所有的基类版本
class Dwelling
{
public:
//three overloaded showperks()
virtual void showperks(int a)const;
virtual void showperks(double x)const;
virtual void showperks() const;
...
};
class Hovel:public Dwelling
{
public:
//three redefined showperks()
virtual void showperks(int a)const;
virtual void showperks(double x)const;
virtual void showperks() const;
...
};
如果只重新定义一个版本,则另外两个版本将被隐藏,派生类对象将无法使用它们。注意,如果不需要修改,则新定义可只调用基类版本:
void Hovel::showperks() const{ Dwelling::showperks();}
5.访问控制:protected
private和protected相似点:在类外只能用公有类成员来访问protected部分中的类成员
区别点:只有在基类派生出的类中才能表现出来,派生类的成员可以直接访问基类的保护成员,但不能直接访问基类的私有成员
对于外部世界来说,保护成员的行为与私有成员相似;但对于派生类来说,保护成员的行为与公有成员类似
注意:最好对类数据成员采用私有访问控制,不要使用保护访问控制;同时通过基类方法使派生类能够访问基类数据,然而对于成员函数来说,保护访问控制很有用,它让派生类能够访问公众不能使用的内部函数
6.抽象基类(abstract base class,ABC)
class BaseEllipse
{
private:
double x;
double y;
...
public:
BaseEllipse(double x0=0,double y0=0):x(x0),y(y0) {}
virtual ~BaseEllipse() {}
void Move(int nx, ny) {x=nx; y=ny;}
virtual double Area() const=0; //纯虚函数
当类声明中包含纯虚函数时,则不能创建该类的对象。这里的理念是,包含纯虚函数的类只用作基类。要成为真正的ABC,必须至少包含一个纯虚函数。总之,BaseEllipse不可以创建对象,Circle和Ellipse被称为具体类(concrete),可以创建这种类型的对象
7.继承和动态内存分配
(1)派生类不使用new
//Base Class Using DMA
class baseDMA
{
private:
char *label;
int rating;
public:
baseDMA(const char *l="null", int r=0);
baseDMA(const baseDMA & rs);
virtual ~baseDMA();
baseDMA & operator=(const baseDMA &rs);
...
};
//derived class without DMA
class lacksDMA:public baseDMA
{
private:
char color[40];
public:
...
};
注意:不需要为lackDMA类定义显式析构函数、复制构造函数和赋值运算符
(2)派生类使用new
//derived class with DMA
class hasDMA:public baseDMA
{
private:
char * style; //use new in constructors
public:
...
};
注意:必须为派生类定义显式析构函数、复制构造函数和赋值运算符
a.析构函数:派生类析构函数自动调用基类的析构函数,故其自身的职责是对派生类构造函数执行工作进行清理,故hasDMA析构函数必须释放指针style管理的内存,并依赖于baseDMA的析构函数来释放指针label管理的内存
baseDMA:~baseDMA()
{
delete[] label;
}
hasDMA:~hasDMA()
{
delete[] style;
}
b.复制构造函数
baseDMA::baseDMA(const baseDMA & rs)
{
label=new char[std::strlen(rs.label)+1];
std::strcpy(label,rs.label);
rating=rs.rating;
}
hasDMA::hasDMA(const hasDMA &hs): baseDMA(hs)
{
style=new char[std::strlen(hs.style)+1];
std::strcpy(style,hs.style);
}
注意:成员初始化列表将一个hasDMA引用传递给baseDMA构造函数,hasDMA复制构造函数只能访问hasDMA的数据,因此它必须调用baseDMA复制构造函数来处理共享的baseDMA数据
c.赋值运算符
hasDMA & hasDMA::operator=(const hasDMA & hs)
{
if(this==&hs)
return *this;
baseDMA::operator=(hs); //copy base portion
delete[] style;
style=new char[std::strlen(hs.style)+1];
std::strcpy(style,hs.style);
return *this;
}
编译器将使用hasDMA::operator=(),从而形成递归调用,使用函数表示法使得赋值运算符被正确调用
总结:当基类和派生类都采用动态内存分配时,对于析构函数,这是自动完成的;对于构造函数,这是通过在初始化成员列表中调用基类的复制构造函数来完成的;如果不这么做,将自动调用基类的默认构造函数。对于赋值运算符,通过作用域解析运算符显式地调用基类的赋值运算符来完成的