定义一个函数为虚函数,不代表函数为不被实现的函数。定义他为虚函数是为了允许用基类的指针来调用子类的这个函数(记住这句,后面要用)。
在下面的程序中Student是基类,Graduate是派生类,它们都有display这个同名的函数。
ex:
//Student类声明
class Student
{
public:
Student(int, string);
void display( );
protected ://受保护成员,派生类可以访问
int num;
string name;
};
//Student类定义
Student::Student(int n, string num){
num=n;name=nam;score=s;
}
void Student::display( ){
cout<<″num:″<<num<<″\\nname:″<<name″;
}
//Graduate类声明
class Graduate:public Student
{
public:
Graduate(int, string, float);
void display( );
private:
float pay;
};
// Graduate定义
void Graduate::display( ){
cout<<″num:″<<num<<″\\nname:″<<name<<\\npay=″<<pay<<endl;
}
Graduate::Graduate(int n, string nam,float s,float p):Student(n,nam,s),pay(p){}
//主函数
int main()
{
Student stud1(1001,″Li″);//定义Student类对象stud1
Graduate grad1(2001,″Wang″,563.5);//定义Graduate类对象grad1
Student *pt=&stud1;//定义指向基类对象的指针变量pt
pt->display( );
pt=&grad1;
pt->display( );
return 0;
}
运行结果如下。
num:1001(stud1的数据)
name:Li
num:2001 (grad1中基类部分的数据)
name:wang
下面对程序作一点修改,在Student类中声明display函数时,在最左面加一个关键字virtual,即
virtual void display( );
这样就把Student类的display函数声明为虚函数。程序其他部分都不改动。再编译和运行程序,请注意分析运行结果:
num:1001(stud1的数据)
name:Li
num:2001 (grad1中基类部分的数据)
name:wang
pay=563.5(这一项以前是没有的)
虚函数的使用方法是:
- 在基类用virtual声明成员函数为虚函数。
这样就可以在派生类中重新定义此函数,为它赋予新的功能,并能方便地被调用。在类外定义虚函数时,不必再加virtual。 - 在派生类中重新定义此函数,要求函数名、函数类型、函数参数个数和类型全部与基类的虚函数相同,并根据派生类的需要重新定义函数体。
C++规定,当一个成员函数被声明为虚函数后,其派生类中的同名函数都自动成为虚函数。因此在派生类重新声明该虚函数时,可以加virtual,也可以不加,但习惯上一般在每一层声明该函数时都加virtual,使程序更加清晰。如果在派生类中没有对基类的虚函数重新定义,则派生类简单地继承其直接基类的虚函数。 - 定义一个指向基类对象的指针变量,并使它指向同一类族中需要调用该函数的对象。
- 通过该指针变量调用此虚函数,此时调用的就是指针变量指向的对象的同名函数。
通过虚函数与指向基类对象的指针变量的配合使用,就能方便地调用同一类族中不同类的同名函数,只要先用基类指针指向即可。如果指针不断地指向同一类族中不同类的对象,就能不断地调用这些对象中的同名函数。这就如同前面说的,不断地告诉出租车司机要去的目的地,然后司机把你送到你要去的地方。
如果修改main函数
int main() { Student stud(1001,″Li″);//定义Student类对象stud Graduate grad(2001,″Wang″,563.5);//定义Graduate类对象grad Student *pt1=&stud;//定义指向基类对象的指针 Graduate *pt2=&grad;//定义指向派生类的指针 pt1->display(); pt2->display(); return 0; }
运行结果如下。
num:1001(stud1的数据)
name:Li
num:2001 (grad1中基类部分的数据)
name:wang
pay=563.5
需要说明;有时在基类中定义的非虚函数会在派生类中被重新定义,如果用基类指针调用该成员函数,则系统会调用对象中基类部分的成员函数;如果用派生类指针调用该成员函数,则系统会调用派生类对象中的成员函数,这并不是多态性行为(使用的是不同类型的指针),没有用到虚函数的功能。
2.定义一个函数为纯虚函数,才代表函数没有被实现。定义他是为了实现一个接口,起到一个规范的作用,规范继承这个。类的程序员必须实现这个函数。 有纯虚函数的类是不可能生成类对象的,如果没有纯虚函数则可以。
class CA
{
public:
virtual void fun() = 0; // 说明fun函数为纯虚函数
virtual void fun1();
};
class CB
{
public:
virtual void fun();
virtual void fun1();
};
// CA,CB类的实现
...
void main()
{
CA a; // 不允许,因为类CA中有纯虚函数
CB b; // 可以,因为类CB中没有纯虚函数
}
需要说明的是:使用虚函数,系统要有一定的空间开销。当一个类带有虚函数时,编译系统会为该类构造一个虚函数表(virtual function table,简称vtable),它是一个指针数组,存放每个虚函数的入口地址。系统在进行动态关联时的时间开销是很少的,因此,多态性是高效的。
有时在基类中将某一成员函数定为虚函数,并不是基类本身的要求,而是考虑到派生类的需要,在基类中预留了一个函数名,具体功能留给派生类根据需要去定义。
例如在前边的例12.1程序中,基类Point中没有求面积的area函数,因为“点”是没有面积的,也就是说,基类本身不需要这个函数,所以在例12.1程序中的Point类中没有定义area函数。
但是,在其直接派生类Circle和间接派生类Cylinder中都需要有area函数,而且这两个area函数的功能不同,一个是求圆面积,一个是求圆柱体表面积。
有的读者自然会想到,在这种情况下应当将area声明为虚函数。可以在基类Point中加一个area函数,并声明为虚函数:
virtual float area( )const {return 0;}
其返回值为0,表示“点”是没有面积的。
其实,在基类中并不使用这个函数,其返回值也是没有意义的。为简化,可以不写出这种无意义的函数体,只给出函数的原型,并在后面加上“=0”,如
virtual float area( )const =0;//纯虚函数
这就将area声明为一个纯虚函数(pure virtual function)。
纯虚函数是在声明虚函数时被“初始化”为0的函数。
声明纯虚函数的一般形式是
virtual 函数类型 函数名 (参数表列)=0;
关于纯虚函数需要注意的几点:
- 纯虚函数没有函数体;
- 最后面的“=0”并不表示函数返回值为0,它只起形式上的作用,告诉编译系统“这是纯虚函数”;
- 这是一个声明语句,最后应有分号。
纯虚函数只有函数的名字而不具备函数的功能,不能被调用。它只是通知编译系统:“在这里声明一个虚函数,留待派生类中定义”。在派生类中对此函数提供定义后,它才能具备函数的功能,可被调用。
纯虚函数的作用是在基类中为其派生类保留一个函数的名字,以便派生类根据需要对它进行定义。
如果在基类中没有保留函数名字,则无法实现多态性。如果在一个类中声明了纯虚函数,而在其派生类中没有对该函数定义,则该虚函数在派生类中仍然为纯虚函数。
抽象类:
这种不用来定义对象而只作为一种基本类型用作继承的类,称为抽象类(abstract class ),由于它常用作基类,通常称为抽象基类(abstract base class )。
凡是包含纯虚函数的类都是抽象类。因为纯虚函数是不能被调用的,包含纯虚函数的类是无法建立对象的。
抽象类的作用是作为一个类族的共同基类,或者说,为一个类族提供一个公共接口。一个类层次结构中当然也可不包含任何抽象类,每一层次的类都是实际可用的,可以用来建立对象的。
但是,许多好的面向对象的系统,其层次结构的顶部是一个抽象类,甚至顶部有好几层都是抽象类。
如果在抽象类所派生出的新类中对基类的所有纯虚函数进行了定义,那么这些函数就被赋予了功能,可以被调用。这个派生类就不是抽象类,而是可以用来定义对象的具体类(concrete class )。
如果在派生类中没有对所有纯虚函数进行定义,则此派生类仍然是抽象类,不能用来定义对象。虽然抽象类不能定义对象(或者说抽象类不能实例化),但是可以定义指向抽象类数据的指针变量。当派生类成为具体类之后,就可以用这种指针指向派生类对象,然后通过该指针调用虚函数,实现多态性的操作
虚函数和抽象基类的应用:
类的层次结构的顶层是抽象基类Shape(形状)。Point(点), Circle(圆), Cylinder(圆柱体)都是Shape类的直接派生类和间接派生类。下面是一个完整的程序,为了便于阅读,分段插入了一些文字说明。程序如下:
第(1)部分
#include <iostream>
using namespace std;
//声明抽象基类Shape
class Shape
{
public:
virtual float area( )const {return 0.0;}//虚函数
virtual float volume()const {return 0.0;}//虚函数
virtual void shapeName()const =0;//纯虚函数
};
第(2)部分
//声明Point类
class Point:public Shape//Point是Shape的公用派生类
{
public:
Point(float=0,float=0);
void setPoint(float ,float ); float getX( )const {return x;}float getY( )const {return y;}
virtual void shapeName( )const {cout<<″Point:″;}//对虚函数进行再定义
friend ostream & operator <<(ostream &,const Point &);
protected:
float x,y;
};
//定义Point类成员函数
Point::Point(float a,float b)
{x=a;y=b;}
void Point::setPoint(float a,float b)
{x=a;y=b;}
ostream & operator <<(ostream &output,const Point &p)
{
output<<″[″<<p.x<<″,″<<p.y<<″]″;
return output;
}
第(3)部分
//声明Circle类
class Circle:public Point
{
public:
Circle(float x=0,float y=0,float r=0);
void setRadius(float );
float getRadius( )const;
virtual float area( )const;
virtual void shapeName( )const {cout<<″Circle:″;}//对虚函数进行再定义
friend ostream &operator <<(ostream &,const Circle &);
protected :
float radius;
};
//声明Circle类成员函数
Circle::Circle(float a,float b,float r):Point(a,b),radius(r){}
void Circle::setRadius(float r):radius(r){}
float Circle::getRadius( )const {return radius;}
float Circle::area( )const {return 3.14159*radius*radius;}
ostream &operator <<(ostream &output,const Circle &c)
{
output<<″[″<<c.x<<″,″<<c.y<<″], r=″<<c.radius;
return output;
}
第(4)部分
//声明Cylinder类
class Cylinder:public Circle
{
public:
Cylinder (float x=0,float y=0,float r=0,float h=0);
void setHeight(float );
virtual float area( )const;
virtual float volume( )const;
virtual void shapeName( )const {cout<<″Cylinder:″;}//对虚函数进行再定义
friend ostream& operator <<(ostream&,const Cylinder&);
protected:
float height;
};
//定义Cylinder类成员函数
Cylinder::Cylinder(float a,float b,float r,float h):Circle(a,b,r),height(h){}
void Cylinder::setHeight(float h){height=h;}
float Cylinder::area( )const{return 2*Circle::area( )+2*3.14159*radius*height;}
float Cylinder::volume( )const{return Circle::area( )*height;}
ostream &operator <<(ostream &output,const Cylinder& cy){output<<″[″<<cy.x<<″,″<<cy.y<<″], r=″<<cy.radius<<″, h=″<<cy.height; return output;}
第(5)部分
//main函数
int main( )
{
Point point(3.2,4.5);//建立Point类对象point
Circle circle(2.4,1.2,5.6);
//建立Circle类对象circle
Cylinder cylinder(3.5,6.4,5.2,10.5);
//建立Cylinder类对象cylinder
point.shapeName();
//静态关联
cout<<point<<endl;
circle.shapeName();//静态关联
cout<<circle<<endl;
cylinder.shapeName();//静态关联
cout<<cylinder<<endl<<endl;
Shape *pt;//定义基类指针
pt=&point;//指针指向Point类对象
pt->shapeName( );//动态关联
cout<<″x=″<<point.getX( )<<″,y=″<<point.getY( )<<″\\narea=″<<pt->area( )
<<″\\nvolume=″<<pt->volume()<<″\\n\\n″;
pt=&circle;//指针指向Circle类对象
pt->shapeName( );//动态关联
cout<<″x=″<<circle.getX( )<<″,y=″<<circle.getY( )<<″\\narea=″<<pt->area( )
<<″\\nvolume=″<<pt->volume( )<<″\\n\\n″;
pt=&cylinder;//指针指向Cylinder类对象
pt->shapeName( );//动态关联
cout<<″x=″<<cylinder.getX( )<<″,y=″<<cylinder.getY( )<<″\\narea=″<<pt->area( )
<<″\\nvolume=″<<pt->volume( )<<″\\n\\n″;
return 0;
}
程序运行结果如下。
请读者对照程序分析。
Point:[3.2,4.5](Point类对象point的数据:点的坐标)
Circle:[2.4,1.2], r=5.6 (Circle类对象circle的数据:圆心和半径)
Cylinder:[3.5,6.4], r=5.5, h=10.5 (Cylinder类对象cylinder的数据: 圆心、半径和高)
Point:x=3.2,y=4.5 (输出Point类对象point的数据:点的坐标)
area=0 (点的面积)
volume=0 (点的体积)
Circle:x=2.4,y=1.2 (输出Circle类对象circle的数据:圆心坐标)
area=98.5203 (圆的面积)
volume=0 (圆的体积)
Cylinder:x=3.5,y=6.4 (输出Cylinder类对象cylinder的数据:圆心坐标)
area=512.595 (圆的面积)
volume=891.96 (圆柱的体积)
从本例可以进一步明确以下结论:
- 一个基类如果包含一个或一个以上纯虚函数,就是抽象基类。抽象基类不能也不必要定义对象。
- 抽象基类与普通基类不同,它一般并不是现实存在的对象的抽象(例如圆形(Circle)就是千千万万个实际的圆的抽象),它可以没有任何物理上的或其他实际意义方面的含义。
- 在类的层次结构中,顶层或最上面的几层可以是抽象基类。抽象基类体现了本类族中各类的共性,把各类中共有的成员函数集中在抽象基类中声明。
- 抽象基类是本类族的公共接口。或者说,从同一基类派生出的多个类有同一接口。
- 区别静态关联和动态关联。
- 如果在基类声明了虚函数,则在派生类中凡是与该函数有相同的函数名、函数类型、参数个数和类型的函数,均为虚函数(不论在派生类中是否用virtual声明)。
- 使用虚函数提高了程序的可扩充性。把类的声明与类的使用分离。这对于设计类库的软件开发商来说尤为重要。
开发商设计了各种各样的类,但不向用户提供源代码,用户可以不知道类是怎样声明的,但是可以使用这些类来派生出自己的类。利用虚函数和多态性,程序员的注意力集中在处理普遍性,而让执行环境处理特殊性。
3.使用虚函数时,有两点要注意:
- 只能用virtual声明类的成员函数,使它成为虚函数,而不能将类外的普通函数声明为虚函数。
因为虚函数的作用是允许在派生类中对基类的虚函数重新定义。显然,它只能用于类的继承层次结构中。 - 一个成员函数被声明为虚函数后,在同一类族中的类就不能再定义一个非virtual的但与该虚函数具有相同的参数(包括个数和类型)和函数返回值类型的同名函数。
根据什么考虑是否把一个成员函数声明为虚函数呢?主要考虑以下几点:
- 首先看成员函数所在的类是否会作为基类。
然后看成员函数在类的继承后有无可能被更改功能,如果希望更改其功能的,一般应该将它声明为虚函数。 - 如果成员函数在类被继承后功能不需修改,或派生类用不到该函数,则不要把它声明为虚函数。
不要仅仅考虑到要作为基类而把类中的所有成员函数都声明为虚函数。 - 应考虑对成员函数的调用是通过对象名还是通过基类指针或引用去访问,如果是通过基类指针或引用去访问的,则应当声明为虚函数。
- 有时,在定义虚函数时,并不定义其函数体,即函数体是空的。它的作用只是定义了一个虚函数名,具体功能留给派生类去添加。
3.虚席构函数:
当派生类的对象从内存中撤销时一般先调用派生类的析构函数,然后再调用基类的析构函数。但是,如果用new运算符建立了临时对象,若基类中有析构函数,并且定义了一个指向该基类的指针变量。在程序用带指针参数的delete运算符撤销对象时,会发生一个情况:系统会只执行基类的析构函数,而不执行派生类的析构函数。
例12.3 基类中有非虚析构函数时的执行情况。
为简化程序,只列出最必要的部分。
#include <iostream>
using namespace std;
class Point//定义基类Point类
{
public:
Point( ){}//Point类构造函数
~Point(){cout<<″executing Point destructor″<<endl;}//Point类析构函数
};
class Circle:public Point//定义派生类Circle类
{
public:
Circle( ){}//Circle类构造函数
~Circle( ){cout<<″executing Circle destructor″<<endl;}//Circle类析构函数
private:
int radius;
};
int main( )
{
Point *p=new Circle;//用new开辟动态存储空间
delete p;//用delete释放动态存储空间
return 0;
}
这只是一个示意的程序。p是指向基类的指针变量,指向new开辟的动态存储空间,希望用detele释放p所指向的空间。但运行结果为:
executing Point destructor
表示只执行了基类Point的析构函数,而没有执行派生类Circle的析构函数。原因是以前介绍过的。
如果希望能执行派生类Circle的析构函数,可以将基类的析构函数声明为虚析构函数,如
virtual ~Point(){cout<<″executing Point destructor″<<endl;}
程序其他部分不改动,再运行程序,结果为
executing Circle destructor
executing Point destructor
先调用了派生类的析构函数,再调用了基类的析构函数,符合人们的愿望。
当基类的析构函数为虚函数时,无论指针指的是同一类族中的哪一个类对象,系统会采用动态关联,调用相应的析构函数,对该对象进行清理工作。
如果将基类的析构函数声明为虚函数时,由该基类所派生的所有派生类的析构函数也都自动成为虚函数,即使派生类的析构函数与基类的析构函数名字不相同。
最好把基类的析构函数声明为虚函数。这将使所有派生类的析构函数自动成为虚函数。 这样,如果程序中显式地用了delete运算符准备删除一个对象,而delete运算符的操作对象用了指向派生类对象的基类指针,则系统会调用相应类的析构函数。
虚析构函数的概念和用法很简单,但它在面向对象程序设计中却是很重要的技巧。
专业人员一般都习惯声明虚析构函数,即使基类并不需要析构函数,也显式地定义一个函数体为空的虚析构函数,以保证在撤销动态分配空间时能得到正确的处理。
构造函数不能声明为虚函数。这是因为在执行构造函数时类对象还未完成建立过程,当然谈不上函数与类对象的绑定。