目录
1、is-a关系的继承
从一个类派生出另一个类时,原始类称为基类,继承类称为派生类。可以称派生类is-a基类。
语法:class derived bass:public base{};
: 冒号指出派生类的基类是base,上述方式为public继承。称为公有派生。使用公有派生,基类的公有成员将成为派生类的公有成员;基类的私有成员将成为派生类的一部分,但不能直接访问,只能通过基类的公有和保护方法访问。
如图所示:
派生类:
- 存储了基类的数据成员(继承实现)
- 可以使用基类的方法(继承了接口)
- 需要添加自己的构造函数
- 需要添加额外的数据成员和成员函数。
- 要使用派生类,程序必须能够访问基类声明,因此通常将基类和派生类的声明放在同一个头文件中。
派生类访问基类的私有数据
- 派生类不能直接访问基类的私有成员,必须通过基类的方法进行访问。访问方式取决于方法,一种是构造函数,一种是其他成员函数。
构造函数
派生类构造函数必须使用基类构造函数。通过成员初始化列表语法来完成。关于基类的信息会自动传递给基类构造函数。
son::son(int r,const string &f,const string &l,bool ht):
parent(f,l,ht)
{
rating =r;
}
//如果不调用parent(f,l,ht)父类的构造函数,程序将使用默认的父类构造函数。
//除非要使用默认构造函数,否则应该显示调用正确的父类构造函数。
关于派生类的构造函数几点说明
- 创建派生类时,先调用基类的构造函数,再调用派生类的构造函数。
- 基类的构造函数负责初始化从基类继承的数据成员,派生类的构造函数初始化新增的数据成员
- 派生类总会调用一个基类的构造函数。可以通过成员初始化列表指明要使用的基类构造函数,否则会使用默认的构造函数。
非构造函数
非构造函数不能使用成员初始化列表语法,但派生类可以调用公有的基类方法。如果派生类重新定义了基类的方法,则调用时应该使用作用域解析运算符::来调用基类方法。对于派生类没有重新定义基类的方法,则不需要实验作用解析运算符号。
派生类与基类的关系
- 基类指针或引用 可以在不进行显示类型的转换情况下指向派生类对象,但是该指针或引用只能调用基类方法。
- 而派生类指针或引用不能指向基类对象。
2、多态公有继承(虚函数机制)
同一个方法在派生类和基类的行为不同,这种行为称为多态——具有多种形态。实现方法:
- 在派生类重新定义基类的方法。
- 使用虚方法。
多态公有继承的实例
- 在基类Brass和派生类BrassPlus中都定义了ViewAcct()和Withdraw()方法,但两者的行为不同。
- 在基类中ViewAcct()和Withdraw()方法前加上关键字virtual。则这些方法被称为虚方法。
class Brass
{
private:
std::string fullName;
long acctNum;
double balance;
public:
···
virtual void Withdraw(double amt);
virtual void ViewAcct() const;
virtual ~Brass() {}
};
//Brass Plus Account Class
class BrassPlus : public Brass
{
private:
double maxLoan;
double rate;
double owesBank;
public:
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; }
};
//1、方法通过对象类型调用
Brass dom;
BrassPlus dot;
dom.ViewAcct(); //则调用基类的ViewAcct()方法
dot.ViewAcct(); //则调用派生类的ViewAcct()方法
//2、方法是通过引用或指针调用
//2(1) 没有virtual关键字,则会根据引用或指针的类型来选择方法
Brass dom;
BrassPlus dot;
Brass &b1_ref =dom;
Brass &b2_ref = dot;
b1_ref.ViewAcct(); //调用基类的ViewAcct()方法,原因是引用是基类类型
b2_ref.ViewAcct(); //调用基类的ViewAcct()方法,原因是引用是基类类型
//2(2) 使用了virtual关键字,则会根据引用或指针 指向的 类型来选择方法
Brass dom;
BrassPlus dot;
Brass &b1_ref =dom;
Brass &b2_ref = dot;
b1_ref.ViewAcct(); //调用基类的ViewAcct()方法,原因是引用指向基类
b2_ref.ViewAcct(); 调用派生类的ViewAcct()方法,原因是引用指向派生类
- 基类中的虚方法,在派生类中自动称为虚方法。virtual关键字可省略。一般会为基类声明一个虚析构函数,为了在释放子对象时,有正确的析构顺序。
3 虚函数的工作原理
函数联编:将源代码的函数调用解释为执行到特定的函数代码块称为函数名联编。
- 静态联编:在编译过程中进行联编被称为静态联编或早期联编。
- 动态联编:在程序运行时才能选择正确的虚方法的代码,称为动态联编或晚期联编。
- 总结:编译器对非虚方法使用静态联编。对虚方法使用动态联编。
向上强制转换:即将派生类引用或指针转为基类引用或指针称为向上强制转换。
向下强制转换:即将基类引用或指针转为派生类引用或指针称为向下强制转换。
如果不适用显示类型转换,则向下类型转换是不允许的。
虚函数
通常,处理虚函数时,给每个对象添加了一个隐藏成员,该隐藏成员其实是一个虚函数指针(virtual function pointer)Vptr,指向了一个虚函数表(Virtual function table,vtbl)。 虚函数表实质上是一个指针数组,里面保存了为类对象进行声明的虚函数地址的数组。
例如:基类对象包含了一个指针,指向基类所以虚函数的地址表。派生类对象也包含一个指向独立地址表的指针,如果派生类提供了虚函数的新定义,该虚函数表则会保存新函数的地址。如果派生类没有重新定义虚函数,则虚函数表保存函数原始版本的地址。如果派生类定义了新的虚函数,则该函数的地址会被添加到该虚函数表中。
具体解释:如图
关于虚函数的注意事项
- 构造函数不能是虚函数
- 析构函数应当是虚函数
- 友元函数不能是虚函数,因为友元不是类成员。
- 如果派生类没有重新定义虚函数,则使用该函数的基类版本。
抽象基类
包含纯虚函数的类称为抽象基类。抽象基类不能创建该类的对象。
纯虚函数:virtual function()=0;可以为纯虚函数定义。
4、继承和动态内存分配
派生类不使用new
结论:不需要为派生类定义显示析构函数,拷贝构造函数和赋值构造函数。
派生类使用new
结论:在这种情况下,必须为派生类定义显示析构函数、拷贝构造函数和赋值构造函数。
- 派生类的析构函数:自动调用基类的构造函数,故其自身的职责是对派生类的构造函数进行清理。
- 派生类拷贝构造函数:由于派生类不能访问到基类的数据,因此必须调用基类的拷贝构造函数(通过初始化成员列表调用基类的拷贝构造函数,如果不这样做,将会调用基类的默认构造函数)来处理共享的基类数据。
- 派生类的赋值构造函数:同样,派生类不能访问到基类的数据,需要通过作用域运算符显示调用基类的赋值运算符。