c++primer plus第十三章-虚函数virtual
1)同一个方法在派生类和基类中的行为是不同的。也就是说,方法的行为应取决于调用该方法的对象。这种较复杂的行为称为多态--具有多种形态,即同一个方法的行为随上下文而异。有两种重要的机制用于实现多态公有继承:
第一:在派生类中重新定义基类的方法;
第二:使用虚方法
2)如果方法是通过引用或指针而不是对象调用的,它将确定使用哪一种方法。如果没有使用关键字virtual,程序将根据引用的类型或指针类型选择方法;如果使用了virtual,程序将根据引用或指针指向的对象的类型来选择方法。
class Brass//基类
{
private:...
public:...
virtual void Withdraw(double amt);
virtual void ViewAcct() const;
};
class BrassPlus : public Brass//派生类
{
private:...
public:...
virtual void Withdraw(double amt);
virtual void ViewAcct() const;
};
3)虚函数:经常在基类中将派生类会重新定义的方法声明为虚方法。方法在基类中被声明后,它在派生类中将自动成为虚方法。然而,在派生类声明中使用关键字virtual来指出哪些函数是虚函数也不失为一个好办法。
Brass dom(...);
BrassPlus dot(..);
Brass &b1_ref = dom;
BrassPlus &b2_ref = dot;
b1_ref.ViewAcct();//调用Brass::ViewAcct();
b2_ref.ViewAcct();//调用BrassPlus::ViewAcct();
4)如果要在派生类中重新定义基类的方法,通常应该将基类方法声明为虚的。这样,程序将根据对象类型而不是引用或指针的类型来选择方法版本。为基类声明一个 虚析构函数也是一种惯例。这样做可以确保释放派生类对象时,按正确的顺序调用析构函数。
5)派生类并不能直接访问基类的私有数据,而必须使用基类的公有方法才能访问这些数据。
派生类构造函数在初始化基类私有数据时,采用的是成员初始化列表语法。非构造函数不能使用成员初始化列表语法。
派生类方法可以调用公有的基类方法。标准做法是使用作用域解析运算符。
BrassPlus:: BrassPlus(const string & s, long an,
double bal, double ml ,double r) : Brass(s, an, bal)
{ ... ...}
6)静态联编和动态联编:将源代码中的函数调用解释为执行特定的函数代码块被称为函数名联编。在编译过程中进行联编称为静态联编,又称为早期联编。然而,虚函数使这项工作变得更加困难。使用哪一个函数是不能在编译时确定的,因为编译器不知道用户将选择哪种类型的对象。所以,编译器必须生成能够在程序运行时选择正确的虚方法的代码,这被称为动态联编,又称为晚期联编。
7)向上强制转换和向下强制转换:将派生类引用或指针转换为基类引用或指针被称为向上强制转换,这使公有继承不需要进行显式类型转换。向上强制转换是可以传递的,也就是说,如果从BrassPlus派生出BrassPlusPlus类,则Brass指针或引用可以引用Brass对象、BrassPlus对象或BrassPlusPlus对象。相反过程,将基类指针或引用转换为派生类指针或引用--称为向下强制转换。如果不使用显式类型转换,则向下强制类型转换是不允许的。
8)为什么默认为静态联编?
为使程序能够在运行阶段进行决策,必须采取一些方法来跟踪基类指针或引用指向的对象类型,这增加了额外的处理开销。如果派生类不重新定义基类的任何方法,也不需要使用动态联编,在这些情况下,使用静态联编更合理。效率也更高,由于静态联编的效率更高,因此被设置为默认的选择。
概念模型,不将一个函数设置为虚函数,有两方面的好处。首先提高了效率,其次,指出不要重定义该函数,这表明,仅仅将那些预期被重新定义方法声明为虚的。
提示:如果要在派生类中重新定义基类的方法,则将它设置为虚方法;否则,设置为非虚方法。
9)虚函数工作原理:编译器处理虚函数的方法是给每个对象添加一个隐藏成员。隐藏成员中保存了一个指向函数地址数组的指针。这种数组称为虚函数表(vtbl)。虚函数表中存储了为类对象进行声明的虚函数的地址。调用虚函数时,程序将查看存储在对象中的vtbl地址,然后转向相应的函数地址表。
总之,使用虚函数时,在内存和速度方面有一定的成本,包括:
第一:每个对象都将增大,增大量为存储地址的空间。
第二:对于每个类,编译器都创建一个虚函数地址表(数组);
第三:对于每个函数调用,都需要执行一项额外的操作,即到表中查找地址。
10)虚函数的有关注意事项:
第一:在基类方法的声明中使用关键字virtual可使该方法在基类以及所有的派生类(包括从派生类派生出来的类)中都是虚的。
第二:如果使用指向对象的引用或指针来调用虚方法,程序将使用为对象类型定义的方法,而不使用为引用或指针类型定义的方法。这称为动态联编或晚期联编。这种行为很重要,这样基类指针或引用可以指向派生类对象。
第三:如果定义的类将被用作基类,则应将那些要在派生类中重新定义的类方法声明为虚的。
11)构造函数不能为虚函数。创建派生类对象时,将调用派生类的构造函数,而不是基类的构造函数,然后,派生类的构造函数将使用基类的一个构造函数,这中顺序不同于继承机制。因此,派生类不继承基类的构造函数,所以讲类构造函数声明为虚的没有什么意义。
12)析构函数应当是虚函数,除非类不用做基类。这意味着,即使基类不需要显式析构函数提供服务,也不应依赖于默认构造函数,而应提供虚析构函数,即使它不执行任何操作。
顺便说一句。给类定义一个虚析构函数并非错误,即使这个类不用做基类;这只是一个效率方面的问题。
提示:通常应给基类提供一个虚析构函数,即使它并不需要析构函数。
13)友元不能是虚函数,因为友元不是类成员,而只有成员才能使虚函数。
14)如果派生类没有重新定义函数,将使用该函数的基类版本。如果派生类位于派生链中,则将使用最新的虚函数版本。
15)重新定义将隐藏方法
重新定义不会生成函数的两个重载版本,而是隐藏了一个基类版本。总之,重新定义继承的方法并不是重载。如果重新定义派生类中的函数,将不只是使用相同的函数参数列表覆盖基类声明,无论参数列表是否相同,该操作将隐藏所有的同名基类方法。
16)两条经验规则:
第一:如果重新定义继承的方法,应确保与原来的原型完全相同,但是如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针。这种特性被称为返回类型协变,因为允许返回类型随类型的变化而变化。
第二:如果基类声明被重载了,则应在派生类中重新定义所有的基类版本。