一、概念
继承(is-a)是类之间关系的一种形式,其他形式还有组合(has-a)等;
基类:也称为父类,一个类继承父类的全部成员
派生类:也称为子类,他是由基类派生而来
语法:
class base{};
class derived : public base{};
二、继承的用途
- 可以在已有类的基础上添加功能
- 可以给类添加数据
- 可以修改类的行为
三、如果继承一个类,需要在派生类添加哪些特性
派生类需要自己的构造函数
派生类可以根据自己的需要添加额外的数据成员和成员函数
class RatedPlayer :
public TableTennisPlayer
{
private:
unsigned int rating;// add a data member
public:
RatedPlayer(unsigned int r, const TableTennisPlayer &tp);
RatedPlayer(unsigned int r = 0, const string &fn = "none",const string &ln = "none",bool ht = false);
~RatedPlayer();
unsigned int Rating()const; //add a method
void ResetRating(unsigned int r);//add a method
};
四、派生类构造函数
派生类不能直接访问基类的私有成员,但可以通过基类的public方法进行访问。
派生类创建对象过程:
- 首先调用基类的构造函数创建基类对象
- 调用派生类构造函数创建派生类对象
因此,引出成员初始化列表语法的概念
RatedPlayer::RatedPlayer(unsigned int r, const string & fn, const string & ln, bool ht)
:rating(r),TableTennisPlayer(fn,ln,ht)
{
}
由上段代码可知,派生类成员变量也可以使用初始化列表进行初始化。
总结:派生类构造函数的要点
- 首先创建基类对象; 派生类构造函数通过成
- 员初始化列表将基类信息传递给基类构造函数;
- 派生类构造函数应该初始化派生类新增的数据成员。
备注:初始化列表只能用于构造函数,如果没有成员初始化列表,程序将使用默认的基类构造函数。
五、 派生类和基类之间的特殊关系
派生类对象可以使用基类的公有方法
基类指针可以不进行显示类型转化的情况下指向派生类对象
基类引用可以不进行显示类型转化的情况下引用派生类对象
基类指针或者引用只能调用基类的方法,不能调用派生类的方法。
TableTennisPlayer player1("Tara", "Boomdea", false);
RatedPlayer rplayer1(1140, "Mallory", "Duck", true);
//基类引用/指针可以不进行显示类型转化的情况下引用派生类对象/指向派生类对象
TableTennisPlayer & rt = rplayer1;
TableTennisPlayer * pt = &rplayer1;
rt.Name();
printf("\n");
pt->Name();
不能将基类对象赋给派生类指针或者引用的原因
派生类继承了基类全部成员变量和方法,派生类是一个特殊的基类,派生类可以用自己特有的成员变量和成员函数,因此当派生类指针和引用访问了基类中不存在的成员变量将出错。
多态公有继承
实现多态公有继承的机制:
- 在派生类中重新定义基类的方法;
- 使用虚方法;
基类:
class Brass
{
private:
string fullName;//客户姓名
long acctNum;//账号
double balance;//余额
public:
Brass(const string &s = "Nullbody",long an = -1,double bal = 0.0);
virtual ~Brass();
void Deposit(double amt);//存款
virtual void Withdraw(double amt);//取款
double Balance()const;//当前余额
virtual void ViewAcct()const;//显示账户信息
};
派生类:
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);
~BrassPlus();
virtual void ViewAcct()const;//显示账户信息
virtual void Withdraw(double amt);//取款
void ResetMax(double m);
void ResetRate(double r);
void ResetOwes();
};
实现多态:
Brass Piggy("Porcelot Pigg", 381299, 4000.00);
BrassPlus Hoggy("Horatio Hogg", 382288, 3000.00);
Piggy.ViewAcct();//调用基类方法
cout << endl;
Hoggy.ViewAcct();//调用派生类方法
cout << endl;
虚方法语法:
virtual void ViewAcct()const;//显示账户信息
备注:
1、可以使用类型限定符指出具体使用哪个类的方法
2、如果使用了虚方法,程序将根据引用或者指针的对象的类型来选择方法
3、如果没有使用虚方法,程序将根据引用类型或者指针类型选择方法
虚析构函数
虚析构函数的作用:让析构函数呈现多态,能够释放子类的对象
如果析构函数不是虚的,则将只调用对于指针类型的析构函数;
如果析构函数是虚的,则将调用相应对象类型的析构函数;
使用析构函数可以保证正确的析构函数序列被调用。
静态联编和动态联编
静态联编:编译器在编译过程完成的联编
动态联编:编译器在程序运行时生成的正确的代码
指针和引用兼容性原则原理就是动态联编。
为什么有两种联编以及默认时静态联编的原因
不摒弃静态联编的原因:
效率:如果派生类不重新定义基类的任何方法,不需要使用动态联编;如果类不用做基类,也不需要动态联编。在这些情况下,使用静态联编更合理。效率更高。
概念模型:在设计类时,可能包含一些不在派生类重新定义的成员函数。不定义为虚函数,其效率会很高。因此,如果在派生类中重新定义基类的方法,则将其设置为虚方法;否则,设置为非虚方法。
虚函数原理
编译器处理虚函数的方法:给每个对象添加一个隐形成员。
隐形成员保存了一个指向函数地址数组的指针,这个数组称为虚函数表。
虚函数表里面存放了类对象声明的虚函数的地址。
例如:
基类对象包含一个指针,指向基类的虚函数表;
派生类对象包含一个指针,指向派生类的虚函数表;
如果派生类提供了虚函数的新定义,则将对应的新地址加入虚函数表;如果没有重新定义虚函数,则拷贝基类中虚函数表中对应的函数地址。
虚函数成本分析
每个对象都将增大,增大量为存储地址的空间;
每个类,都要创建虚函数表;
每个函数调用,需要到虚函数表中查找地址
哪些函数可以设置为虚函数,哪些不可以
构造函数不能是虚函数:派生类创建对象的机制决定
虚函数可以为虚函数;
友元不能是虚函数:友元不是类的成员(友元函数调用虚函数)
抽象基类
基本思想:以Ellipse和Circle类为例,从Ellipse和Circle类中抽象出他们的共性,将这些共性放到一个抽象基类里面。然后从该抽象基类派生出Ellipse和Circle类。这样就可以使用基类指针数组管理Ellipse和Circle类对象,即可以使用多态方法。
定义
含有纯虚函数的类,称为抽象类。抽象类不可以创建该类对象。通常被用来设计接口。