一个简单的继承例子
从一个类派生出另一个类时,原始类称为基类,继承类称为派生类。
先编写一个简单的基类:
#ifdef TABTENN0_H_
#define TABTENN0_H_
#include <string>
using std::string;
class TableTennisPlayer{
private:
string firstname;
string lastname;
bool hasTable;
public:
TableTennisPlayer(const string & fn="none",
const string & ln="none",bool ht=false);
void Name() const;
bool HasTable() const{ return hasTable;}
void ResetTable(bool v){ hasTable=v;}
};
#endif
#include "tabtenn0.h"
#include <iostream>
TableTennisPlayer::TableTennisPlayer(const string &fn,
const string &ln,bool ht):firstname(fn),
lastname(ln),hasTable(ht){} //使用成员初始化列表语法,只能用于构造函数
void TableTennisPlayer::Name() const{
std::cout<<lastname<<","<<firstname;
}
如果要派生一个类:
class RatedPlayer:public TableTennisPlayer{
...
};
这说明这是一个公有派生,基类的公有成员将成为派生类的公有成员,基类的私有部分将成为派生类的一部分,但是必须通过基类的公有和保护方法访问。
派生类需要添加自己的构造函数,可以根据需要添加额外的数据成员和成员函数。
class RatedPlayer:public TableTennisPlayer{
private:
int rating;
public:
RatedPlayer(int r=0,const string &fn="none",
const string &ln="none",bool ht=false);
RatedPlayer(int r,const TableTennisPlayer &tp);
int Rating() const{return rating;}
void ResetRating(int r){rating=r;}
}
这一段代码补在tablenn0.h中即可。
派生类构造函数
(1)必须使用基类构造函数。
(2)应通过成员初始化列表将基类信息传递给基类构造函数
(3)应初始化派生类新增的数据成员
创建派生类对象时,先调用基类构造函数,然后再调用派生类的构造函数。派生类对象过期时,先调用派生类析构函数,再调用基类析构函数。
RatedPlayer::RatedPlayer(int r, const string &fn,
const string &ln,bool ht):TableTennisPlayer(fn,ln,ht){
rating=r;
}
RatedPlayer::RatedPlayer(int r,const TableTennisPlayer &tp)
:TableTennisPlayer(tp),rating(r){
}
派生类对象可以使用基类的方法,条件是方法不是私有的。
基类指针可以在不进行显式类型转换的情况下指向派生类对象;基本引用可以在不进行显式类型转换的情况下引用派生类对象。
RatedPlayer rplayer(1140,"Mallory","Duck",true);
TableTennisPlayer &rt=rplayer;
TableTennisPlayer *pt=&rplayer;
rt.Name();
pt->Name();
rt.ResetRanking();//no!
不可以将基类对象的地址赋给派生类引用和指针:
TableTennisPlayer player("Besty","Bloop",true);
RatedPlayer &rr=player;//no
RatedPlayer *pr=&player;//no
可以将基类对象初始化为派生类对象,也可以将派生类对象赋给基类对象。这会触发复制构造函数和重载赋值运算符。
is-a关系
C++有三种继承方式:公有继承、保护继承和私有继承。
公有继承建立一种is-a关系,派生对象也是一个基类对象,可以对基类对象执行任何操作,也可以对派生对象执行。
多态公有继承
如果要实现多态公有继承:
(1)在派生类中重新定义基类的方法
(2)使用虚方法
#ifndef BRASS_H_
#define BRASS_H_
#include<string>
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.111);
BrassPlus(const Brass &ba,double ml=500,double r=0.111);
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;}
};
#endif
Brass类和BrassPlus类都声明了ViewAcct()和Withdraw()方法,但是方法的行为是不同的。程序将使用对象类型来确定使用哪个版本:
Brass dom("Dominic Banker",11224,4183.45);
BrassPlus dot("Dorothy Banker",12118,2592.00);
dom.ViewAcct();//use Brass::ViewAcct()
dot.ViewAcct();//use BrassPlus::ViewAcct()
被标上virtual关键字的函数被称为虚函数。Brass类还声明了一个虚析构函数。如果方法是通过引用或指针而不是对象调用的,它将确定使用哪一种方法。如果没有使用virtual,程序将根据引用类型或指针类型选择方法;如果使用了virtual,程序将根据引用或指针指向的对象的类型来选择方法。
如果ViewAcct()不是虚的:
Brass dom("Dominic Banker",11224,4183.45);
BrassPlus dot("Dorothy Banker",12118,2592.00);
Brass &b1_ref=dom;
Brass &b2_ref=dot;
b1_ref.ViewAcct();//use Brass::ViewAcct()
b2_ref.ViewAcct();//use Brass::ViewAcct()
如果ViewAcct是虚的:
Brass dom("Dominic Banker",11224,4183.45);
BrassPlus dot("Dorothy Banker",12118,2592.00);
Brass &b1_ref=dom;
Brass &b2_ref=dot;
b1_ref.ViewAcct();//use Brass::ViewAcct()
b2_ref.ViewAcct();//use BrassPlus::ViewAcct()
基类使用了虚的析构函数,这确保释放派生对象时,按正确的顺序调用析构函数。
如果要在派生类中重新定义基类的方法,通常应该将基类方法声明为虚的。
在函数声明中使用了virtual关键字,则函数定义中不必再写。
静态联编和动态联编
将源代码中的函数调用解释为执行特定的函数代码块被称为函数名联编。
函数重载可以在编译过程完成联编,这种联编为静态联编。
虚函数让编译器无法确定具体的函数,因此编译器必须生成能够在程序运行时选择正确虚方法的代码,这种联编为动态联编。
一般C++不允许将一种类型的地址赋给另一种类型的指针,也不允许一种类型的引用指向另一种类型。
double x=2.5;
int * pi= &x;//no
long & r1= x;//no
但是指向基类的引用或指针可以引用派生类对象,而不必进行显式类型转换,这被称为上强制转换。但是相反的过程(下强制转换)必须要使用显式强制转换。
对于使用基类引用或指针作为参数的函数调用,将进行向上转换。假设每个函数都调用虚方法ViewAcct():
void fr(Brass & rb);
void fp(Brass * pb);
void fv(Brass b);
int main(){
Brass b("Billy Bee",123432,10000.0);
BrassPlus bp("Betty Beep",232313,12345.0);
fr(b);//Brass
fr(bp);//BrassPlus
fp(b);//Brass
fp(bp);//BrassPlus
fv(b);//Brass
fv(bp);//Brass
}
构造函数不能是虚的,但是析构函数应该是虚的。友元不能是虚函数,因为友元不是类成员。
如果派生类没有重新定义函数,派生类将使用派生链中最新的虚函数版本,除非基类版本被隐藏。
重新定义将隐藏方法,如果重新定义派生类中的函数,无论参数列表是否相同,该操作将隐藏所有的同名基类方法。
因此,如果要重新定义继承的方法,应确保与原来的原型完全相同。但如果返回类型时基类引用或指针,则可以修改为指向派生类的引用或指针。
此外,如果基类声明被重载了,则应在派生类中重新定义所有的基类版本。如果只重新定义其中一个版本,则其他几个将被隐藏,派生类无法使用。
protected
protected和private相似,在类外只能用公有类成员来访问protected部分中的类成员。但是在派生类的成员可以直接访问基类的保护成员。
最好对类数据成员采用私有访问控制,不使用保护访问控制;同时通过基类方法使派生类能够访问基类数据。
抽象基类
C++通过使用纯虚函数提供未实现的函数。纯虚函数声明的结尾处为=0.
virtual double Area() const=0;
当类声明中包含纯虚函数时,不能创建该类的对象。要成为抽象基类,必须至少含有一个纯虚函数。
抽象基类描述的是至少使用一个纯虚函数的接口,派生类将根据具体特征,使用常规虚函数来实现这种接口。同时也可以通过虚函数来完成多态。
继承和动态内存分配
(1)派生类不使用new
如果基类使用动态内存分配,以及声明、定义了析构函数、复制构造函数、重载赋值运算符,则派生类若不使用动态内存分配,则不需要析构函数、复制构造函数、重载赋值运算符。
(2)派生类使用new
此时,必须为派生类定义显式析构函数、复制构造函数、重载赋值运算符。
派生类析构函数需要对派生类中动态内存分配空间进行释放,而不用关心基类动态内存分配空间,因为基类析构函数会在派生类西沟函数调用后调用。
派生类复制构造函数需要调用基类复制构造函数,并且需要为自身动态分配内存。
hasDMA::hasDMA(const hasDMA &hs):baseDMA(hs){
...
}
派生类重载赋值运算符,除了复制派生类成员变量,还要显式调用基类赋值运算符来完成。
hasDMA &hasDMA::operator=(const hasDMA &hs){
if (this==&hs) return *this;
baseDMA::operator=(hs);//*this=hs这种用法会导致递归
...
}
友元不是成员函数,不能使用作用域解析运算符来指出使用哪个函数。此时需要强制类型转换。
std::ostream &operator<<(std::ostream &os,const hasDMA &hs){
os<<(const baseDMA &)hs;//强制转换,调用基类的重载<<运算符
return os;
}
函数 | 能否继承 | 成员还是友元 | 默认能否生成 | 能否为虚函数 | 是否可以有返回类型 |
---|---|---|---|---|---|
构造函数 | 否 | 成员 | 能 | 否 | 否 |
析构函数 | 否 | 成员 | 能 | 能 | 否 |
= | 否 | 成员 | 能 | 能 | 能 |
转换函数 | 能 | 成员 | 否 | 能 | 否 |
友元 | 否 | 友元 | 否 | 否 | 能 |