5.4.2 虚函数详解
1.虚函数的定义
虚函数就是在基类中被关键字virtual说明,并在派生类重新定义的函数。虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。
虚函数的定义是在基类中进行的,它是在基类中需要定义为虚函数的成员函数的声明中冠以关键字virtual。定义虚函数的格式如下:
virtual 函数类型 函数名(形参表)
{
函数体
}
在基类中的某个成员函数声明为虚函数后,此虚函数就可以在一个或多个派生类中被重新定义。在派生类中重新定义时,其函数类型、函数名、参数个数、参数类型的顺序,都必须与基类中的原型完全相同。
//例 5.21 虚函数的使用
#include<iostream> using namespace std; class B0{ public: virtual void print(char *p) //定义基类B0中的虚函数 { cout<<p<<"print()"<<endl; } }; class B1:public B0{ public: virtual void print(char *p) //定义基类B0的公有派生类B1中的虚函数 { cout<<p<<"print()"<<endl; } }; class B2:public B1{ public: virtual void print(char *p) //定义基类B1的公有派生类B2中的虚函数 { cout<<p<<"print()"<<endl; } }; int main() { B0 ob0,*op; //定义基类对象ob0和对象指针op op=&ob0;op->print("B0::"); //调用基类的B0的print() B1 ob1; //定义派生类B1的对象ob1 op=&ob1;op->print("B1::"); //调用派生类B1的print() B2 ob2; //定义派生类B2的对象ob2 op=&ob2;op->print("B2::"); //调用派生类B2的print() return 0; } /* 在程序中,语句op->print(); 出现了3次,由于op指向的对象不同,每次出现都执行了相应对象的虚函数print 程序运行结果: B0::print() B1::print() B2::print()
说明:
(1)若在基类中,只是声明虚函数原型(需要加上virtual),而在类外定义虚函数时,则不必再加上virtual。
(2)在派生类中重新定义时,其函数类型、函数名、参数个数、参数类型的顺序,都必须与基类中的原型完全相同。
(3)C++规定,当一个成员函数被定义为虚函数后,其派生类中符合重新定义虚函数要求的同名函数都自动称为虚函数。因此,在派生类中重新定义该虚函数时,关键字virtual可写可不写。但是为了程序更加清晰,最好在每一层派生类中定义函数时都加上关键字virtual。
(4)如果在派生类中没有对基类的虚函数重新定义,则公有派生类继承其直接基类的虚函数。
一个虚函数无论被公有继承多少次。它仍然保持其虚函数的特性。
例如:
class B0{
...
public:
virtual void show(); //在基类中定义show为虚函数
};
class B1:public B0{
...
};
若在公有派生类B1中没有重新定义虚函数show,则函数在派生类中被继承,仍然是虚函数。
(5)虚函数必须是其所在类的成员函数,而不能是友元函数,也不能是静态成员函数,因为虚函数的调用要靠特定的对象来决定该激活哪个函数。
(6)虽然使用对象名和点运算符的方式也可以调用虚函数,但是这种调用是在编译时进行的,是静态联编,它没有利用虚函数的特性。只有通过指针访问虚函数时才能获得运行时的多态性。
2. 虚析构函数
在C++中,不能声明虚构造函数,但是可以声明虚析构函数。
//例5.23 虚析构函数的引例
#include<iostream> using namespace std; class B{ public: ~B() { cout<<"调用基类B的析构函数\n"; } }; class D:public B{ public: ~D() { cout<<"调用派生类D的析构函数\n"; } }; int main() { D obj; return 0; } */ /* 运行结果是: 调用派生类D的析构函数 调用基类B的析构函数 显然本程序的运行结果是符和预想的。但是,如果在主函数中用new运算符建立一个无名对象 和定义了一个基类的对象指针,并将无名的对象的地址赋给这个对象指针。当用delete运算符 撤销无名对象时,系统只执行基类的析构函数,而不执行派生类的析构函数。 例如下面的例子: */
//例5.24 虚析构函数的引例2
#include<iostream> using namespace std; class B{ public: ~B() { cout<<"调用基类B的析构函数\n"; } }; class D:public B{ public: ~D() { cout<<"调用派生类D的析构函数\n"; } }; int main() { B *p; //定义指向基类B的指针变量p p = new D; //用new运算符为派生类的无名对象动态的分配了一个存储空间,并将地址赋给对象指针p delete p; //用delete撤销无名对象时,释放动态存储空间 return 0; } /* 程序运行结果: 调用基类B的析构函数 程序结果表示,本程序只执行了基类B的析构函数,而没有执行派生类D的析构函数。 原因是:当撤销指针p所指的派生类的无名对象,而调用析构函数时,采用了静态联编方式, 只调用了基类B的析构函数。 那么如何在撤销指针p所指的派生类的无名对象,既调用基类B的析构函数,也调用派生类D的 析构函数呢? 方法是:可以将基类的析构函数声明为虚析构函数,采用了多态性的动态联编方式。 虚析构函数没有类型,也没有参数,和普通虚函数相比,虚析构函数比较简单。 其声明格式: virtual ~类名() */
//例5.25 虚析构函数的使用
#include<iostream> using namespace std; class B{ public: virtual ~B() { cout<<"调用基类B的析构函数\n"; } }; class D:public B{ public: ~D() { cout<<"调用派生类D的析构函数\n"; } }; int main() { B *p; //定义指向基类B的指针变量p p = new D; //用new运算符为派生类的无名对象动态的分配了一个存储空间,并将地址赋给对象指针p delete p; //用delete撤销无名对象时,释放动态存储空间 return 0; } /* 程序运行结果是: 调用派生类D的析构函数 调用基类B的析构函数 说明:虽然派生类的析构函数与基类的析构函数名字不相同,但是如果将基类的析构函数 定义为虚函数,则由该基类所派生的所有派生类的析构函数也都自动成为虚函数。 */
3.虚函数与重载函数的关系
在一个派生类中重新定义基类的虚函数是函数重载的另一种形式,但它不同于一般函数重载。
当普通的函数重载时,其函数的参数或参数类型有所不同,函数的返回类型也可以不同。但是当重载一个虚函数时,也就是说在派生类中重新定义虚函数时,要求函数名、返回类型、参数个数、参数的类型和顺序与基类的虚函数原型完全相同。如果仅仅返回类型不同,其余均相同,系统会给出错误信息;若仅仅函数名相同,而参数的个数、类型或顺序不同,系统将它作为普通的函数重载,这时虚函数的特性将丢失。
//例5.26 虚函数与重载函数的关系
#include<iostream> using namespace std; class Base{ public: virtual void func1(); virtual void func2(); virtual void func3(); void func4(); }; class Derived:public Base{ public: virtual void func1(); //func1是虚函数,这里可以不写virtual void func2(int i); //与基类中的func2作为普通函数的重载,虚特性消失 //char func3(); //错误,因为与基类的func3返回类型不同,应删去 void func4(); //与基类中的func4是普通函数的重载,不是虚函数 }; void Base::func1() { cout<<"--Base func1--\n"; } void Base::func2() { cout<<"--Base func2--\n"; } void Base::func3() { cout<<"--Base func3--\n"; } void Base::func4() { cout<<"--Base func4--\n"; } void Derived::func1() { cout<<"--Derived func1--\n"; } void Derived::func2(int i) { cout<<"--Derived func2--\n"; } void Derived::func4() { cout<<"--Derived func4--\n"; } int main() { Base b,*pt; //定义基类的对象b和对象指针pt Derived d; //定义派生类的对象d pt = &d; //基类的指针pt象派生类的对象d pt->func1(); //调用的是派生类的fun1,结果是--Derived func1-- (虚函数的特性) pt->func2(); //调用的是基类的fun2,结果是--Base func2--(参数表中多了一个参数,变成普通重载函数,丢失虚函数的特性) pt->func4(); //调用的是基类的fun4,结果是--Base func4--(基类和派生类中均没有virtual关键字,普通成员函数的重载) return 0; } /* 程序运行结果是: --Derived func1-- --Base func2-- --Base func4-- */
4. 多重继承与虚函数
//例5.27 多重继承与虚函数的例子
#include<iostream> using namespace std; class Base1{ public: virtual void fun() //定义fun是虚函数 { cout<<"--Base1--\n"; } }; class Base2{ public: void fun() //定义fun是普通的成员函数 { cout<<"--Base2--\n"; } }; class Derived:public Base1,public Base2{ public: void fun() { cout<<"--Derived--\n"; } }; int main() { Base1 *ptr1; Base2 *ptr2; Derived d; ptr1 = &d; //基类的指针指向派生类的对象 ptr1->fun();/*(*ptr1).fun();*/ //调用派生类Derived的fun方法,因为它是由Base1派生来的,为虚函数,有虚特性 ptr2->fun();/*(*ptr2).fun();*/ //调用基类Base2的fun方法,派生类的fun方法是由Base2派生类来的,为普通成员重载函数 Derived obj;//基类的引用指向派生类的对象 Base1 &p1 = obj; Base2 &p2 = obj; p1.fun();//调用派生类Derived的fun方法,因为它是由Base1派生来的,为虚函数,有虚特性 p2.fun();//调用基类Base2的fun方法,派生类的fun方法是由Base2派生类来的,为普通成员重载函数,无虚特性 return 0; }
5.虚函数的综合应用
//例5.28 应用C++的多态性,计算三角形、矩形和圆的面积。
#include<iostream> #define PI 3.1416 using namespace std; class Shape{ //定义一个公共的基类 public: // Shape(){} Shape(double a=0.0,double b=0.0) //带默认的构造函数 { x = a; y = b; } virtual void area() { cout<<"在基类中定义的虚基类"; cout<<"为派生类提供一个公共的接口,"; cout<<"以便派生类根据需要重新定义虚函数"<<endl; } protected: double x; double y; }; class Triangle:public Shape{ //定义一个三角形的派生类 public: Triangle(double a,double b):Shape(a,b){} void area() { cout<<"三角形的高是:"<<x<<","<<"底是:"<<y<<endl; cout<<"三角形面积:"<<0.5*x*y<<endl; } }; class Square:public Shape{ //定义一个矩形的派生类 public: Square(double a,double b):Shape(a,b){} void area() { cout<<"矩形的长是:"<<x<<","<<"宽是:"<<y<<endl; cout<<"矩形面积:"<<x*y<<endl; } }; class Circle:public Shape{ //定义一个圆的派生类 public: Circle(double a):Shape(a,a){} void area() { cout<<"圆的半径是:"<<x/2<<endl; cout<<"圆面积:"<<PI*x*x<<endl; } }; int main() { Shape *p,obj; //定义基类的对象指针p和对象obj p=&obj; //基类的对象指针p指向基类的对象obj p->area(); //调用基类的area方法 Triangle t(10.0,6.0); //定义三角形的对象t Square s(10.0,6.0); //定义矩形的对象s Circle c(10.0); //定义圆形的对象t p=&t; p->area(); //计算三角形的面积 p=&s; p->area(); //计算矩形的面积 p=&c; p->area(); //计算圆形的面积 return 0; } /* 程序运行结果: 在基类中定义的虚基类为派生类提供一个公共的接口,以便派生类根据需要重新定义虚函数 三角形的高是:10,底是:6 三角形面积:30 矩形的长是:10,宽是:6 矩形面积:60 圆的半径是: 圆面积:314.16 */