先来讲讲赋值兼容规则。
前面说过,派生类如果是从基类公有继承的,则它会包含基类中除构造函数和析构函数外的所有成员,基类的公有成员也成为派生类的公有成员,又因为对象只能访问类的公有成员,所以基类对象具有的功能,派生类对象都有。这样就引出了赋值兼容规则。
赋值兼容规则就是指在基类对象可以使用的地方都可以用公有派生类对象来代替。注意必须是公有派生类。赋值兼容规则中的代替有三种方式,通过一个例子分别说明。
假设有基类Base,类Child是Base的公有派生类,base为Base类的对象,pBase为Base类指针,child为Child类的对象。代码如下:
class Base
{
...
};
class Child : public Base
{
...
};
Base base, *pBase;
Child child;
那么根据赋值兼容规则,可以使用类Base对象的地方都可以使用类Child的对象来代替。这里的代替有三种:
1.派生类的对象可以赋值给基类的对象。也就是将派生类对象从基类继承的成员的值分别赋值给基类对象相应的成员。例如:
base = child;
2.派生类对象的地址可以赋值给基类类型的指针。例如:
pBase = &child;
3.派生类对象可以用来初始化基类的引用。例如:
Base &b = child;
因为有了赋值兼容规则,有了上述三种赋值方式,所以函数的参数中有基类对象或者基类指针又或者基类引用时,我们可以直接传入派生类对象或者派生类对象的地址作为实参来执行相同的操作。这样的好处是什么呢?那就是我们想对基类及派生类的对象做相同的操作时,只要定义一个函数就行了,它的参数为基类对象或者基类指针也或者是基类引用。这样就大大提高了软件开发的效率。
公有派生类对象可以代替基类对象使用,但是我们只能使用它从基类继承的成员,而无法使用它的新添成员。
举个例子说明下赋值兼容规则:
类Base为基类,类Child0为Base的公有派生类,类Child1为类Child0的公有派生类。三个类中都定义了成员函数show()。
- #include <iostream>
- using namespace std;
- class Base // 基类Base的声明
- {
- public:
- void show() { cout << "Base::show()" << endl; } // 公有成员函数show
- };
- class Child0 : public Base // 类Base的公有派生类Child0的声明
- {
- public:
- void show() { cout << "Child0::show()" << endl; } // 公有成员函数show
- };
- class Child1 : public Child0 // 类Child0的公有派生类Child1的声明
- {
- public:
- void show() { cout << "Child1::show()" << endl; } // 公有成员函数show
- };
- void CallShow(Base *pBase) // 一般函数,参数为基类指针
- {
- pBase->show();
- }
- int main()
- {
- Base base; // 声明Base类的对象
- Base *pBase; // 声明Base类的指针
- Child0 ch0; // 声明Child0类的对象
- Child1 ch1; // 声明Child1类的对象
- pBase = &base; // 将Base类对象base的地址赋值给Base类指针pBase
- CallShow(pBase);
- pBase = &ch0; // 将Child0类对象ch0的地址赋值给Base类指针pBase
- CallShow(pBase);
- pBase = &ch1; // 将Child1类对象ch1的地址赋值给Base类指针pBase
- CallShow(pBase);
- return 0;
- }
程序运行结果为:
Base::show()
Base::show()
Base::show()
我们首先定义了一个函数CallShow,其参数pBase为基类Base类型的指针,根据赋值兼容规则,我们可以用公有派生类对象的地址为基类指针赋值,那么CallShow函数就可以处理这个类族的所有对象。在主函数中我们就分别把基类对象base的地址、派生类对象ch0的地址和派生类对象ch1的地址赋值给基类指针pBase,然后将pBase作为实参调用CallShow,在CallShow中调用了成员函数show。
但是,根据上面所讲,将派生类对象的地址赋值给pBase以后,通过pBase只能访问派生类从基类继承的成员。所以即使指针pBase指向的是派生类对象ch0或者ch1,在CallShow中通过pBase也只能调用从基类Base继承的成员函数show,而不会调用Child0类或者Child1类的成员函数show。因此主函数中三次调用CallShow函数,都是访问的基类Base的成员函数show,输出都是Base::show()。
这时我们深切的感受到,即使派生类对象代替了基类对象,它也只能产生基类的功能,自己的新功能无法体现。要想在代替以后同样能够实现自己的功能,就要用到面向对象设计的另一个特性--多态性。
一.虚函数的意义
在前面赋值兼容规则中给出了一个程序例子,其中包含类Base、Child0和Child1。在程序运行结果中我们看到,main函数中Base类型的指针pBase,分别指向Base、Child0和Child1类的对象时调用的show函数都是基类Base的show函数。因为基类类型的指针指向派生类对象时,通过此指针只能访问从基类继承来的同名成员。这些在前面C++赋值兼容规则中已经分析过了。
但是如果我们希望通过指向派生类对象的基类指针,访问派生类中的同名成员该怎么办呢?这就要用到虚函数了。我们在基类中将某个函数声明为虚函数,就可以通过指向派生类对象的基类指针访问派生类中的同名成员了。这样使用某基类指针指向不同派生类的不同对象时,就可以发生不同的行为,也就实现了运行时的多态(编译时并不知道调用的是哪个类的成员)。
虚函数是动态绑定的基础。记住,虚函数是非静态的成员函数,一定不能是静态(static)的成员函数。虚函数在以后我们进行软件架构设计时会起到很关键的作用。编程入门时可能不会有这方面的意识,等熟练到一定程度就会发现虚函数的强大。
二.一般虚函数成员的声明和使用
一般的虚函数声明形式为:
virtual 函数类型 函数名(形参表)
{
函数体
}
虚函数就是在类的声明中用关键字virtual限定的成员函数。以上声明形式是成员函数的实现也在类的声明中的情况。如果成员函数的实现在类的声明外给出时,则虚函数的声明只能出现在类的成员函数声明中,而不能在成员函数实现时出现,简而言之,只能在此成员函数的声明前加virtual修饰,而不能在它的实现前加。
总结下运行时多态的几个条件:1.类之间要满足赋值兼容规则;2.要声明虚函数;3.通过类的对象的指针、引用访问虚函数或者通过类的成员函数调用虚函数。下面举例说明下,大家通过这个例子来对照下这几个条件。
此例是由赋值兼容规则中的例子改进的。将基类中的函数show声明为虚函数,程序其他部分不做任何修改。
- #include <iostream>
- using namespace std;
- class Base // 基类Base的声明
- {
- public:
- virtual void show() { cout << "Base::show()" << endl; } // 虚成员函数show
- };
- class Child0 : public Base // 类Base的公有派生类Child0的声明
- {
- public:
- void show() { cout << "Child0::show()" << endl; } // 虚成员函数show
- };
- class Child1 : public Child0 // 类Child0的公有派生类Child1的声明
- {
- public:
- void show() { cout << "Child1::show()" << endl; } // 虚成员函数show
- };
- void CallShow(Base *pBase) // 一般函数,参数为基类指针
- {
- pBase->show();
- }
- int main()
- {
- Base base; // 声明Base类的对象
- Base *pBase; // 声明Base类的指针
- Child0 ch0; // 声明Child0类的对象
- Child1 ch1; // 声明Child1类的对象
- pBase = &base; // 将Base类对象base的地址赋值给Base类指针pBase
- CallShow(pBase);
- pBase = &ch0; // 将Child0类对象ch0的地址赋值给Base类指针pBase
- CallShow(pBase);
- pBase = &ch1; // 将Child1类对象ch1的地址赋值给Base类指针pBase
- CallShow(pBase);
- return 0;
- }
程序运行结果:
Base::show()
Child0::show()
Child1::show()
我们可以看出,仅仅是在Base类中的show函数前加了virtual的修饰,运行结果就差了很多,这正是虚函数的美丽所在。
例程中,类Base、Child0和Child1属于同一个类族,而且Child0是由Base公有派生的,Child1是从Child0公有派生的,所以满足赋值兼容规则,这就符合了运行时多态的第一个条件。基类Base的函数show声明为了虚函数,这是第二个条件。在CallShow函数中通过对象指针pBase来访问虚函数show,这又满足了第三个条件。这个动态绑定过程在运行时完成,实现了运行时的多态。这样通过基类指针就可以访问指向的不同派生类的对象的成员,这在软件开发中不仅使代码整齐简洁,而且也大大提高了开发效率。
基类的成员函数声明为虚函数以后,派生类中的同名函数可以加virtual修饰也可以不加。
三.虚析构函数
大家可能奇怪为什么不先讲虚构造函数,很简单,因为不能声明虚构造函数,而可以声明虚析构函数。
多态是指不同的对象接收了同样的消息而导致完全不同的行为,它是针对对象而言的,虚函数是运行时多态的基础,当然也是针对对象的,而构造函数是在对象生成之前调用的,即运行构造函数时还不存在对象,那么虚构造函数也就没有意义了。
析构函数用于在类的对象消亡时做一些清理工作,我们在基类中将析构函数声明为虚函数后,其所有派生类的析构函数也都是虚函数,使用指针引用时可以动态绑定,实现运行时多态,通过基类类型的指针就可以调用派生类的析构函数对派生类的对象做清理工作。
前面讲过,析构函数没有返回值类型,没有参数表,所以虚析构函数的声明也比较简单,形式如下:
virtual ~类名();