继承完成的工作
1 可以在已有的类的基础上添加功能。
2 可以给类添加数据成员。
3 可以修改类的方法。
#ifndef __TABRENN_H__
#define __TABRENN_H__
#include <string>
using std::string;
void start_tabtenn_eng();
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;};
~TableTennisPlayer();
};
#endif //__TABRENN_H__
#include <stdio.h>
#include <stdlib.h>
#include <iostream>
#include "tabtenn.h"
using namespace std;
void start_tabtenn_eng() {
cout << "start_tabtenn_eng." << endl;
TableTennisPlayer player1("Chuck", "Blizzard", true);
TableTennisPlayer player2("Tara", "Boomdea", false);
player1.Name();
}
TableTennisPlayer::TableTennisPlayer(const string & fn, const string & ln, bool ht) :
firstname_(fn), lastname_(ln), hasTable_(ht){}
void TableTennisPlayer::Name() const {
std::cout<< lastname_ << ", " << firstname_;
}
TableTennisPlayer::~TableTennisPlayer() {
}
RatedPlayer 是TableTennisPlayer 的派生类,public表明 TableTennisPlayer是一个公有基类,又称公有派生。
class RatedPlayer : public TableTennisPlayer { //公有派生
//TableTennisPlayer 是公有基类
}
• 派生类对象存储了基类的数据成员(派生类继承了基类的实现)
• 派生类对象可以使用基类的方法(派生类继承了基类的接口)
继承类中需要添加的内容:
• 派生类需要有自己的构造函数
• 派生类可以根据需要添加额外的数据成员和成员函数。
class RatedPlayer : public TableTennisPlayer { //共有派生 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; };
}
构造函数必须给新成员和继承的成员提供数据
构造函数:访问权限的考虑
派生类不能直接访问基类的私有成员,必须通过基类的方法进行访问,派生类就像类外部一样,不能直接访问。所以构造函数不能直接设置基类的成员,必须通过基类公有方法访问,所以派生类的构造函数必须使用基类的构造函数。
RatedPlayer::RatedPlayer(int r, const string & fn, const string & ln, bool ht) : TableTennisPlayer(fn, ln, ht) {
rating_ = r;
}
其中,: TableTennisPlayer(fn, ln, ht) 是成员初始化列表,是可执行的代码,表示调用TableTennisPlayer的构造函数,将创建一个嵌套的TableTennisPlayer类对象,然后程序进入RatedPlayer构造函数,完成RatedPlayer对象的创建。
如果省略初始化列表,程序将调用默认的基类构造函数,所以正常情况下应使用。
RatedPlayer::RatedPlayer(int r, const string & fn, const string & ln, bool ht) {
rating_ = r;
} // 调用默认的 RatedPlayer 构造函数
派生类成员也可以使用成员初始化列表
RatedPlayer::RatedPlayer(int r, const string & fn, const string & ln, bool ht) : TableTennisPlayer(fn, ln, ht),rating(r) {
}
派生类构造函数应注意:
• 首先创建基类对象。
• 派生类构造函数应通过成员初始化列表将基类信息传递类基类构造函数。
• 派生类构造函数应初始化派生类新增的数据成员。
释放对象的顺序与创建对象的顺序相反,即首先执行派生类的析构函数,然后自动调用基类的析构函数。
note:创建派生类对象时,程序首先调用基类构造函数,然后再调用派生类构造函数。基类构造函数负责初始化继承的数据成员,派生类构造函数主要用于初始化新增的数据成员。派生类的构造函数总是调用一个基类构造函数,可以使用初始化列表语法指明调用哪个基类构造函数,否则使用默认的基类构造函数。
派生类和基类的关系:
1 派生类对象可以使用基类的公有方法
2 基类指针可以在不显式转换的情况下指向派生类
3 基类引用可以在不显式转换的情况下指向派生类
RatedPlayer player("Betsy", "Bloop", true);
TableTennisPlayer & rr = player;
TableTennisPlayer *pr = &player;
注意:基类指针或者引用只能调用基类的方法,不能调用派生类方法。
应用场景,函数的参数时基类的引用或者指针,可以用基类对象或者派生类对象当实参
void show(const TableTennisPlayer & rt) {
cout << "Name:"
rt.Name();
}
RatedPlayer player("Betsy", "Bloop", true);
TableTennisPlayer player1("Tara", "Bloop", true);
show(player);
show(player1);
可以将派生类对象赋值给基类,将调用隐式重载赋值运算符。
TableTennisPlayer &operator(const TableTennisPlayer &) const;
RatedPlayer olaf1("Betsy", "Bloop", true);
TableTennisPlayer winner("Tara", "Bloop", true);
winner = olaf1;
可以使用基类对象初始化为派生类对象,此种情况将自动调用隐式复制构造函数
TableTennisPlayer(const RatedPlayer &);
RatedPlayer olaf1("Betsy", "Bloop", true);
TableTennisPlayer winner(olaf1);
继承 is-a关系
继承的方式分为:公有继承、保护继承、私有继承
公有继承建立一种is-a关系(is a kind of),例如水果派生出香蕉,香蕉is a kind of水果,香蕉继承了水果的所有特性。新类将继承基类的所有特性。
公有继承不建立has-a的关系,例如午餐可以包括水果,但不是水果,将水果加入午餐类的数据成员是可以的。
公有继承不建立is-like-a的关系,例如胖子像猪,但是胖子并不是猪,猪有四条腿,律师不能继承,继承可以在基类的基础上添加属性,但不能删除属性。
公有继承不建立is-implemented-as-a的关系,例如用数组实现栈
公有继承不建立use-a的关系,
多态公有继承
多态是方法的行为取决于调用该方法的对象,即一种方法在派生类和基类中的行为是不同的,
多态,具有多种形态,即同一个方法的行为随着上下文而异。
两种实现多态公有继承:
• 在派生类中重新定义基类的方法。
• 使用虚方法。
class Brass
{
private:
/* data */
string fullName;
long acctNum;
public:
Brass(const string & s = "Null", long an = -1, doublr bal = 0.0);
void Deposit(double amt);
vitrual void Withdraw(double amt);
double Balance() count;
vitrual void ViewAcct() const;
vitrual ~Brass(); //虚析构函数
};
class BrassPluse : Brass {
private:
/* data */
double macLoan;
double rate;
double owesBank;
public:
BrassPluse(const string & s = "Null", long an = -1, doublr bal = 0.0,
double bal = 0.0, double ml = 500, double r = 0.1125);
void Deposit(double amt);
vitrual void Withdraw(double amt);
vitrual void ViewAcct() const;
void ResetMax(double m) {maxLoan = m;}
}
函数 Withdraw()和ViewAcc()被重写,那么程序是如何决定调用哪个方法呢
1 使用对象类型来决定
Brass dom();
BrassPluss dot();
dom.ViewAcc(); //use Brass::iewAcc()
dot.ViewAcc(); //use BrassPluss::iewAcc()
2 使用引用或者指针调用函数
如果 ViewAcct() 函数没有被vitrual修饰,即不是虚函数,程序将根据引用或者指针类型选择方法。
Brass dom();
BrassPluss dot();
Brass & b1_ref = dom;
Brass b2_ref = dot;
b1_ref.ViewAcct(); //use brass::ViewAcct()
b2_ref.ViewAcct(); //use brass::ViewAcct()
如果使用了vitrual, 程序将根据指针或者引用指向的对象的类型决定调用哪个方法
Brass dom();
BrassPluss dot();
Brass & b1_ref = dom;
Brass b2_ref = dot;
b1_ref.ViewAcct(); //use brass::ViewAcct()
b2_ref.ViewAcct(); //use BrassPluss::ViewAcct()
注意:如果要在派生类中重新定义基类的方法,通常应将基类的方法声明为虚函数,这样,程序将根据对象的类型而不是引用或指针的类型来选择方法的版本。为基类声明一个虚析构函数也是惯例。
为什么需要虚析构函数
如果析构函数不是虚的,则将只调用对应于指针类型的析构函数,
如果析构函数是虚函数,则程序将调用指针指向的对象的析构函数,即如果指针指向的是子类对象,将先调用子类的析构函数,然后程序会自动调用父类的析构函数。
Brass dom = new Brass();
BrassPluss dot = new BrassPluss();
Brass * b1_ref = dom;
Brass * b2_ref = dot;
delete b1_ref; // 程序调用Brass::~Brass();
delete b2_ref; // 因为b2_ref指向BrassPluss类对象,程序先调用BrassPluss ::~Brass();,然后自动调用Brass::~Brass();
所以如果子类的析构函数执行一些操作,则必须包含虚析构函数,才能正确调用到子类的析构函数
静态联编和动态联编
将源代码中的调用函数解释为执行特定的的函数代码块被称为函数名联编。C++中由于函数重载,编译器必须查看函数参数以及函数名才能确认使用哪个函数。
这种在编译过程中进行联编称为静态联编。
由于虚函数的存在,编译器必须在程序运行时选择正确的虚方法的代码,被称为动态联编
指针和引用类型的兼容性
动态联编与通过指针和引用调用方法相关;c++不允许将一种类型的地址赋给另一种类型的指针或引用,但是基类的指针或者引用可以引用派生类对象,而不必进行显式的转换。
虚函数的使用可以让基类指针或引用也可以调用派生类的方法(虚函数),因此需要动态联编。
虚成员函数和动态联编
注意,通常的做法是,如果要在派生类中重新定义基类的方法,则将它设置为虚方法,否则为非虚方法。
虚方法的工作原理
编译器会给每个对象添加一个隐藏成员,隐藏成员中保存一个指向函数地址数组的指针。这个数组称为虚函数表,虚函数表存储了为类对象进行声明的虚函数的地址。
基类对象包含一个指针,指向基类中所有虚函数的地址表,派生类对象将包含一个指向独立地址表的指针。
如果派生类重写了虚函数的定义,该虚函数表将保存新函数的地址;
如果派生类没有重新定义虚函数,则表中将保存函数的原始版本的地址
如果派生类添加了新的虚函数,则该函数的地址也将添加到表中
所以虚函数需要下边的开销:
1 每个对象都将增大,增大两为存储地址的空间
2 对于每个类,编译器都创建一个虚函数地址表。
3 对于每个虚函数的调用,都需要执行一项额外的操作,即到表中查找地址。
虚函数注意事项:
虚函数的要点:
• 在基类的方法的声明中使用关键字virtual可以是该方法在基类,以及派生类(包括多重派生)中是虚的。
• 如果使用指向对象的引用或者指针来调用虚方法。程序将使用为对象类型定义的方法,而不视使用为引用或指针类型定义的方法,称为动态联编
• 如果定义的类将被用作基类,则应将那些要在派生类中重新定义的类方法声明为虚的。
1 构造函数:构造函数不能是虚函数,
2 析构函数:析构函数应该是虚函数,除非类不做基类,因为delete是,虚析构函数将先调用子类的析构函数,然后自动调用基类的。所以,通常应给基类提供一个虚析构函数
3 友元,
友元不能是虚函数,因为友元不是类成员,只有类成员才能是虚函数。
4 重新定义将隐藏方法
在派生类中重新定义方法,将不是生成两个重载版本,而是隐藏了基类的函数。
如果基类声明的函数被重载,则派生类中应该重新定义的所有基类函数;否则如果只重新定义一个另外两个将被隐藏。
访问控制:protect
关键字protect和private相似。在类外只能用公有类成员来访问protected部分的类成员,protect和private区别只有在基类派生的类中才能表现出来。
派生类的成员可以直接访问基类的保护成员,但不能直接访问基类的私有成员。对于类外部来说,保护成员的行为与私有成员相同,对于派生类来说,保护成员的行为与公有成员类似。
注意: 对于类数据成员应采用私有访问控制,不要使用保护访问控制,提供通过基类的方法使派生类能够访问基类的数据。对于成员函数来说,保护访问控制比较实用,可让派生类访问外部不能使用的内部函数。
抽象基类
当类声明中包含纯虚函数时,此类为抽象基类,不能创建该类的对象。包含纯虚函数的类只作为基类。纯虚函数写法,函数原型=0;使虚函数称为纯虚函数。
纯虚函数可以在基类中定义,也可以不进行定义,只做声明。
只要有一个函数时纯虚函数,此类就是抽象基类,不能被实例化。
可以用抽象基类指针,指向不同的派生类,派生类根据需要实现虚函数。
ABC描述的是至少使用一个纯虚
函数的接口,从ABC派生出的类将根据派生类的具体特征,使用纯虚函数来实现这种接口。
设计类时必须确定是否将类方法声明为虚的。如果希望派生类能够重新定义方法,则应在基类中将方法定义为虚的,这样可以启用动态联编。如果不希望方法重新定义,则不必将其声明为虚的,虽然无法禁止被重新定义,但表达了不希望被重新定义。