《C++编程思想》
第十五章 多态性和虚函数
多态性改善了代码的组织性和可读性,同时也使创建的程序具有可扩展性。访问控制通过使细节数据设为private,将接口从具体实现中分离开来。虚函数则根据类型来解耦。
向上类型转换。取一个对象的地址,并将其作为基类的地址来处理,这称为向上类型转换。如下代码:
Class A{
Public:
Void play() const{。。。};
};
Class B:public A{
Public:
Void play() const{…};
};
Class C:public A{
Public:
Void play() const{…};
};
Void func(A & a)
{
a.play();
};
int main()
{
C c;
Func(c)
Func()接受一个A,但也不拒绝任何A派生的类,无需类型转换就能将对象传给func。
把函数体与函数调用相联系称为捆绑。当捆绑在程序运行之前完成时,这称为早捆绑。使用virtual可以实现晚捆绑。在派生类中virtual函数的重定义通常称为重写。注意,仅需要在基类中声明一个函数为virtual,调用所有匹配基类声明行为的派生类函数都将使用虚机制。
抽象基类和纯虚函数:只是想对基类进行向上类型转换,使用它的接口,不希望用户实际地创建一个基类的对象。要做到这一点,可以在基类中加入至少一个纯虚函数,使其基类称为抽象类。纯虚函数使用关键字virtual,并且在其后面加上=0。编译器会保证不能生成抽象类的对象。当继承一个抽象类时,必须实现所有的纯虚函数,否则继承出来的类也将是抽象。
纯虚函数禁止对抽象类的函数以传值方式调用。通过抽象类,保证在向上类型转换期间总是使用指针或引用。
编译器对新类创建一个新VTABLE表,并且插入新函数的地址,对于没有重新定义的虚函数使用基类函数的地址。
虚函数和构造函数:当创建一个包含有虚函数的对象时,必须初始化它的VPTR以指向相应的VTABLE。因为生成一个对象是构造函数的工作,所以设置VPTR也是构造函数的工作。编译器在构造函数的开头部分秘密地插入能初始化VPTR的代码。
构造函数有一个专门的工作:确保对象被正确地建立。在构造函数内,必须想办法保证所有成员都已经建立,保证它的唯一方法是让基类构造函数首先被调用。这样,当在派生类构造函数中,在基类中能访问的所有成员都已经被初始化。
对于在构造函数中调用一个虚函数的情况,被调用的只是这个函数的本地版本。虚机制在构造函数中不工作。
当这一系列构造函数调用正发生时,每个构造函数都已经设置VPTR指向它自己的VTABLE。如果函数调用使用虚机制,它将只产生通过他自己的VTABLE的调用,而不是最后派生的VTABLE(所有构造函数被调用后才会有最后派生的VTABLE)。
析构函数和虚拟析构函数:构造函数是不能为虚函数的,但析构函数能够且常常必须是虚的。析构函数的析构顺序是由最晚派生的类开始,并向上到基类。每个析构函数知道它所在类从哪一个派生而来,但不知道它派生出哪些类。
#include<iostream>
using namespacestd;
class Base1{
public:
~Base1(){cout <<"~Base1()\n";}
};
classDerived1:public Base1{
public:
~Derived1(){cout <<"~Derived1()\n";}
};
class Base2{
public:
virtual ~Base2(){cout <<"~Base2()\n";}
};
classDerived2:public Base2{
public:
~Derived2(){cout <<"~Derived2()\n";}
};
int main(intargc,char *argv[])
{
Base1* bp= new Derived1;
delete bp;
Base2* b2p= new Derived2;
delete b2p;
system("pause");
}
不把析构函数设置为虚函数是一个隐匿的错误,因为它常常不会对程序有直接的影响,但某些情况下会存在内存泄露。
作为一个准则,任何时候我们的类中都要有一个虚函数,我们应当立即增加一个虚析构函数,即使它什么也不做。
析构函数中的虚机制:在析构函数中,只有成员函数的“本地”版本被调用;虚机制被忽略。(就是说,在析构函数中,不会调用派生类里的虚函数。因为在析构到本类时,派生类的析构函数已经被调用,派生类的成员变量已被析构,不能再调用派生类中的函数)。