1. 类和继承
面向对象编程的主要目的之一就是提供可以重复使用的代码,减少开发周期,提高开发效率。
继承可以完成的一些工作:
- 在已有类的基础上添加功能。
- 给类添加新的数据成员。
- 可以修改类方法的行为。
派生类与基类之间的关系:
- 派生类对象可以使用基类(公有的)方法。
- 基类指针可以在不进行显示类型转换的情况下指向派生类对象,但只能调用基类方法。
- 基类引用可以在不进行显示类型转换的情况先引用派生类对象,但只能调用基类方法。
2. 多态公有继承
在实际开发过程中,我们有时候希望同一个方法在派生类和基类中的行为是不同的,也就是说,方法的行为取决于调用该方法的对象,即同一个方法的行为随上下文而异,我们将这种复杂的行为称为多态(具有多种形态)。
实现多态公有继承的方法:
- 在派生类中重新定义基类的方法。
- 使用虚方法(虚函数)。
3. 静态联编和动态联编
函数名联编:编译器将源代码中的函数调用解释为执行特定的函数代码称为函数名联编。
我们将在编译过程中进行联编称为静态联编(早期联编)。但对于虚函数这种情况,编译器在编译的时候并不一定知道用户将会选择哪种类型的对象,因此,编译器必须生成能够在程序运行时根据用户的选择正确的虚函数的代码。我们将这种在程序运行过程中选择对应的方法(函数)的联编称为动态联编(晚期联编)。
编译器对非虚方法使用静态联编,对虚方法使用动态联编。
因此,动态联编是在虚函数的支持下实现的。
动态联编主要包含一下方面:
- 成员函数必须声明为虚函数,即前面加virtual。
- 如果基类中对某个成员函数声明了虚函数,则其派生类中的该成员函数不需要再声明。
从代码维护的层面考虑,随着类的层级的扩展,动态联编提高了代码的灵活性和问题的抽象性,使得程序的维护成本大大降低。
将派生类引用或指针转换为基类引用或指针称为向上强制转换,该转换使得公有继承不需要进行显示类型转换。且该转换是可以传递的,例如基类A,其派生类AP,AP的派生类APP,则A指针或引用可以指向或引用AP类对象和APP类对象。
相反的,我们将基类指针或引用转换为派生类指针或引用称为向下强制转换。但该种转换只能使用显示类型转换,防止无意间指向派生类独有的方法或成员造成的异常情况的发生。
虚函数的工作原理:
通常情况下,编译器在处理虚函数的时候,会给每个对象添加一个隐藏的成员,该成员中保存了一个指向函数地址的数组的指针,该数组我们称为虚函数表。虚函数表存储了为类对象进行声明的虚函数的地址。通常情况下,基类对象包含一个指向该类中虚函数表的指针。在派生类对象中将包含一个自己的虚函数表,如果派生类没有重新定义虚函数,则派生类的虚函数列表保函数的原始版本地址;如果派生类提供了虚函数的新定义,则该虚函数列表对应的位置将保存新函数的地址;如果派生类新增加虚函数,则该函数的地址也会添加到虚函数表中。
从虚函数的工作过程中可以看出,使用动态联编能够让程序能选择特定的类型的设计方法,提高了代码的灵活性和抽象性。但同时在内存和执行速度方面增加了一定的成本。因此在实际开发过程中选择合适的联编功能。
虚函数重新定义的注意事项:
假设创建如下代码:
代码语言:javascript复制class Dwelling
{
public:
virtual void showperks(int a) const;
...
}
class Hovel : public Dwelling
{
public:
virtual void showperks() const;
...
}
那么在使用下列代码的时候会出现错误:
代码语言:javascript复制Hovel trump;
trump.showperks(); //正常
trump.showperks(5); //错误
Hovel
中重新定义showperks()
一个不接受任何参数的函数,此时的重新定义不会生成两个函数重载版本,而是将基类的showperks(int a)
隐藏了。总之,重新定义继承的方法不是重载,而是隐藏同名基类的方法。
因此,如果要重新定义继承的方法,则应确保与原来的原型完全相同,但是如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针,即允许返回类型随类类型的变化而变化,这种特性被称为返回类型协变。
如果基类中重载函数被重新定义了,则应在派生类中重新定义所有的基类版本。如果只重新定义一个版本,则另外几个重载版本将被隐藏,派生类无法使用他们。
4. 抽象基类
虚函数声明的结尾处为=0,该虚函数称为纯虚函数。当类声明中包含纯虚函数时,则不能创建该类的对象。
在虚函数原型中使用=0指出类是一个抽象基类,在类中可以不定义该函数。