1、虚函数的定义
- 虚函数就是在基类中被关键字 virtual 说明,并在派生类中重新定义的函数。
- 虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。
- 虚函数的定义是在基类中进行的,它是在基类中在那些需要定义为虚函数的成员函数的声明中冠以关键字 virtual 。定义虚函数的方法如下:
virtual 函数类型 函数名(形参表){
函数体;
}
在基类中的某个成员函数被声明为虚函数后,此虚函数就可以在一个或多个派生类中被重新定义。在派生类中重新定义时,其函数原型,包括函数类型、函数名、参数个数、参数类型的顺序,都必须与基类中的原型完全相同。
例 1:虚函数的使用
#include<iostream>
using namespace std;
class B0{
public:
virtual void print(char *p){ //定义虚函数 print
cout<<p<<"print()"<<endl;
}
};
class B1:public B0{
public:
virtual void print(char *p){ //重新定义虚函数 print
cout<<p<<"print()"<<endl;
}
};
class B2:public B1{
public:
virtual void print(char *p){ //重新定义虚函数 print
cout<<p<<"print()"<<endl;
}
};
int main(){
B0 ob0,*op; //定义基类对象 ob0 和对象指针 op
op=&ob0;
op->print("B0::"); //调用基类 B0 的 print
B1 ob1; //定义派生类 B1 的对象
op=&ob1;
op->print("B1::"); //调用派生类 B1 的 print
B2 ob2;
op=&ob2;
op->print("B2::");
return 0;
}
执行结果:
说明:
(1)若在基类中,只声明虚函数原型(需加上 virtual),而在类外定义虚函数时,则不必再加 virtual。例如:
class B0{
public:
virtual void print(char *p); //声明虚函数原型,需加上 virtual
};
在类外,定义虚函数时,不要加 virtual:
void B0::print(char *p){
cout<<p<<"print()"<<endl;
}
(2)在派生类中,虚函数被重新定义时,其函数的原型与基类中的函数原型(即包括函数类型、函数名、参数个数、参数类型的顺序)都必须完全相同。
(3)C++ 规定,当一个成员函数被定义为虚函数后,其派生类中符合重新定义虚函数要求的同名函数都自动称为虚函数。因此,在派生类中重新定义该虚函数时,关键字 virtual 可以不写。 但是,为了使程序更加清晰,最好在每一层派生类中定义该函数时都加上关键字 virtual。
(4)如果在派生类中没有对基类的虚函数重新定义,则公有派生类继承其直接基类的虚函数。一个虚函数无论被公有继承多少次,它仍然保持其虚函数的特性。 例如:
class B0{
···
public:
virtual void show(); //在基类定义 show 为虚函数
};
class B1:public B0{
···
};
若在公有派生类 B1 中没有重新定义虚函数 show ,则函数 show 在派生类中被继承,仍是虚函数。
(5)虚函数必须是其所在类的成员函数,而不能是友元函数,也不能是静态成员函数,因为虚函数调用要靠特定的对象来决定该激活哪个函数。
(6)使用对象名和点运算符的方式调用虚函数是在编译时进行的,是静态联编,没有利用虚函数的特性。只有通过基类指针访问虚函数时才能获得运行时的多态性。
例 2:使用对象名和点运算符的方式调用虚函数
#include<iostream>
using namespace std;
class B0{
public:
virtual void print(char *p){ //定义虚函数 print
cout<<p<<"print()"<<endl;
}
};
class B1:public B0{
public:
virtual void print(char *p){
cout<<p<<"print()"<<endl;
}
};
class B2:public B1{
public:
virtual void print(char *p){
cout<<p<<"print()"<<endl;
}
};
int main(){
B0 ob0;
ob0.print("B0::");
B1 ob1;
ob1.print("B1::");
B2 ob2;
ob2.print("B2::");
return 0;
}
2、虚析构函数
在 C++ 中,不能声明虚构造函数,但是可以声明虚析构函数。
https://blog.youkuaiyun.com/aaqian1/article/details/84915540 中介绍了先执行派生类的析构函数,再执行基类的析构函数。
例 3:虚析构函数的引例 1:
#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;
}
本程序运行结果符合预想,即先执行派生类的析构函数,再执行基类的析构函数。但是,如果在主函数中用 new 运算符建立一个派生类的无名对象和定义了一个基类的对象指针,并将无名对象的地址赋给这个对象指针。当用 delete 运算符撤销无名对象时,系统只执行基类的析构函数,而不执行派生类的析构函数。
例 4:虚析构函数的引例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;
}
执行结果:
当撤销指针 P 所指的派生类的无名对象,而调用析构函数时,采用了静态联编方式,只调用了基类 B 的析构函数。
如果希望程序执行动态联编方式,在用 delete 运算符撤销派生类的无名对象时,先调用派生类的析构函数,再调用基类的析构函数,可以将基类的析构函数声明为虚析构函数。
例 5:虚析构函数的使用
#include<iostream>
using namespace std;
class B{
public:
virtual ~B(){
cout<<"调用基类 B 的析构函数\n";
}
};
class D:public B{
public:
virtual ~D(){
cout<<"调用派生类 D 的析构函数\n";
}
};
int main(){
B *p; //定义指向基类 B 的指针变量 p
p=new D;
//用运算符 new 为派生类的无名对象动态地分配了一个存储空间,并将地址赋给对象指针 p
delete p;
//用 delete 撤销无名对象,释放动态存储空间
return 0;
}
由于使用了虚析构函数,程序执行了动态联编,实现了运行的动态性。虽然派生类的析构函数与基类的析构函数名字不相同,但是如果将基类的析构函数定义为虚函数,由该基类所派生的所有派生类的析构函数也都自动成为虚函数。
3、虚函数与重载函数的关系
在一个派生类中重新定义基类的虚函数是函数重载的另一种形式,但它不同于一般的函数重载。
当普通的函数重载时,其函数的 参数 或 参数类型 有所不同,函数的 返回类型 也可以不同。但是,当重载一个虚函数时,即在派生类中重新定义虚函数时,要求函数名、返回类型、参数个数、参数的类型和顺序与基类中的虚函数原型完全相同。①如果仅仅返回类型不同,其余均相同,系统会给出错误信息;②若仅仅函数名相同,而参数的个数,类型或顺序不同,系统将它作为普通的函数重载,这时虚函数的特性将丢失。
例 6:虚函数与重载函数的关系
#include<iostream>
using namespace std;
class Base{
public:
virtual void fun1();
virtual void fun2();
virtual void fun3();
void fun4();
};
class Derived:public Base{
public:
virtual void fun1(); //fun1 是虚函数,这里可不写 virtual
void fun2(int x); //与基类中的 fun2 作为普通函数重载,虚特性消失
// char fun3(); //错误,因为与基类只有返回类型不同,应删去
void fun4();
};
void Base::fun1(){
cout<<"---Base fun1---"<<endl;
}
void Base::fun2(){
cout<<"---Base fun2---"<<endl;
}
void Base::fun3(){
cout<<"---Base fun3---"<<endl;
}
void Base::fun4(){
cout<<"---Base fun4---"<<endl;
}
void Derived::fun1(){
cout<<"---Derived fun1---"<<endl;
}
void Derived::fun2(int x){
cout<<"---Derived fun2---"<<endl;
}
/*
void Derived::fun3(){
cout<<"---Derived fun3---"<<endl;
}*/
void Derived::fun4(){
cout<<"---Derived fun4---"<<endl;
}
int main(){
Base d1,*bp;
Derived d2;
bp=&d2;
bp->fun1();
bp->fun2();
bp->fun4();
return 0;
}
执行结果:
4、多重继承与虚函数
多重继承可以视为多个单继承的组合。因此,多重继承情况下的虚函数调用与单继承情况下的虚函数调用有相似之处。
例 7:多重继承与虚函数的例子
#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; //定义指向基类 Base1 的对象指针 ptr1
Base2 *ptr2; //定义指向基类 Base2 的对象指针 ptr2
Derived obj3; //定义派生类 Derived 的对象 obj3
ptr1=&obj3;
ptr1->fun();
//此处的 fun为虚函数,因此调用派生类 Derived 的虚函数 fun
ptr2=&obj3;
ptr2->fun();
//此处的 fun为非虚函数,而 ptr2 为类 Base2 的对象指针,因此调用基类 Base2 的函数 fun
return 0;
}
执行结果:
相对于 Base1 的派生路径,由于 Base1 中的 fun 是虚函数,当声明为指向 Base1 的指针指向派生类 Derived 的对象 obj3 时,函数 fun 呈现出虚特性。
相对于 Base2 的派生路径,由于 Base2 中的 fun 是
一般成员函数,所以此时它只能是一个普通的重载函数,当声明为指向 Base2 的指针指向 Derived 的对象 obj3 时,函数 fun 只呈现普通函数的重载特性。
5、虚函数举例
例 8:应用 C++ 的多态性,计算三角形、矩形和圆的面积。
#include<iostream>
using namespace std;
class Figure{ //定义一个公共基类
protected:
double x,y;
public:
Figure(double a,double b){
x=a;
y=b;
}
virtual void area(){ //定义一个虚函数,作为界面接口
cout<<"在基类中定义的虚函数,";
cout<<"为派生类提供一个公共接口,";
cout<<"以便派生类根据需要重新定义虚函数。";
}
};
class Triangle:public Figure{ //定义三角形派生类
public:
Triangle(double a,double b):Figure(a,b){ //构造函数
}
void area(){ //虚函数重新定义,用作求三角形的面积
cout<<"三角形的高是:"<<x<<",底是:"<<y;
cout<<",面积是:"<<0.5*x*y<<endl;
}
};
class Square:public Figure{
public:
Square(double a,double b):Figure(a,b){
}
void area(){ //虚函数重新定义,用作求矩形的面积
cout<<"矩形的长是:"<<x<<",宽是:"<<y<<",面积是:"<<x*y<<endl;
}
};
class Circle:public Figure{ //定义圆派生类
public:
Circle(double a):Figure(a,a){
}
void area(){
cout<<"圆的半径是:"<<x<<",面积是:"<<3.1416*x*x<<endl;
}
};
int main(){
Figure *p;
Triangle t(10.0,6.0);
Square s(10.0,6.0);
Circle c(10.0);
p=&t;
p->area();
p=&s;
p->area();
p=&c;
p->area();
return 0;
}
运行结果: