什么是虚函数?
简单地说,那些被virtual关键字修饰的成员函数,就是虚函数。
为什么要引入虚函数?
虚函数的作用是实现类的继承所体现的多态性,具体点是实现动态联编。
从程序的角度上来说,在定义了虚函数后,可以在派生类中对虚函数重新定义,以实现统一的接口,不同定义过程,在程序的运行阶段动态地选择合适的成员函数。
什么是多态性?
简单点说,多态性是将接口与实现进行分离;
C++实现运行时多态性的关键途径:在公有派生情况下,一个指向基类的指针可用来访问从基类继承的任何对象。
语法:普通函数的前面加上virtual
virtual 函数返回值类型 虚函数名(形参表)
{
//函数体
}
虚函数的调用方式:只能通过指向基类的指针或基类对象的引用来调用虚函数
调用语法:
指向基类的指针变量名->虚函数名(实参表)
基类对象的引用名. 虚函数名(实参表)
注意:正常情况下,如果不把函数声明为虚函数,指向基类的指针的访问情况如下:
1)基类指针指向基类对象:基类指针可以直接访问基类对象中的成员
2)基类指针指向派生类对象:基类指针只能访问派生类中的从基类中继承的成员,派生类有同名的函数或成员,也只能调用基类的成员。
如果定义成虚函数时:定义一个基类指针,把不同的派生类对象付给它,会调用对应派生类的函数,而非基类函数。
举例:
#include <iostream>
using namespace std;
class A
{
public:
virtual void show()
{
cout<<"A"<<endl;
}
};
class B:public A
{
public:
void show()
{
cout<<"B"<<endl;
}
};
class C:public A
{
public:
void show()
{
cout<<"C"<<endl;
}
};
void main()
{
A*a;
B b;
C c;
a=&b;
a->show();
a=&c;
a->show();
system("pause");
}
运行结果:B(换行)C(换行)--指向不同的派生类,调用不同的函数
如果不加基类A中的Virtual,则输出结果:A(换行)A(换行)--基类指针,调用派生类中继承的基类成分
定义虚函数,实现动态联编需要三个条件:
1)必须把动态联编的行为定义为类的虚函数---定义虚函数
2)类之间存在子类型关系,一般表现为一个类从另一个类公有派生而来---类之间是公有继承
3)基类指针指向派生类的对象,然后使用基类指针调用虚函数
注意:
1、使用时,虚函数可以在基类中声明,提供界面。可以在给派生类中定义具体的实现方法,而得到多种方法。
2、多态的实现必须是公有派生。原因:虚函数为了实现多态,虚函数建立在赋值兼容原则上,而赋值兼容原则成立的前提条件是派生类从基类那里公有派生。
3、派生类对基类中虚函数重新定义时,关键字可以写也可以不写,系统可以自动判断
4、虚函数必须是类的成员函数,不能是友元函数,也不能是静态成员函数。因为虚函数需要使用特定的对象激活函数。但是虚函数可以是另一个类的友元函数
5、内敛函数不能是虚函数,因为内敛函数不能在运行时动态确定位置。所以虚函数都是非内敛的
7、一个类的析构函数是虚函数,那么派生而来的所有派生类的析构函数也是虚函数,不管是否使用了关键字virtual
8、一旦一个函数被说明为虚函数,不管经历多少层派生,都将保持其虚特性
9、如果在派生类中没有对虚函数重新定义,则它继承其基类的虚函数。
10、虚函数在派生类中重新定义时,可以函数原型必须完全相同,如具有相同的形参个数和形参类型,返回值。
纯虚函数:如果基类的函数没有必要或者无法实现,完全要依赖子类去实现的话,可以使用纯虚函数
简单点说,基类提供接口,而不提供定义,派生类提供实现
语法:
virtual 函数返回值类型 虚函数名(形参表)=0;
注意:从基类继承来的纯虚函数,在派生类中仍是纯虚函数,除非派生类按原型一致地超载该纯虚函数,变成类的纯虚函数
抽象类:包含了纯虚函数的类
注意:抽象类只能用作其他类的基类,不能建立对象,不能作为参数类型、返回类型或显式转换类型,可声明指针和引用。可以定义返回引用。
说明:
1、为什么构造函数不能是虚函数?
因为:从使用上来说,虚函数是通过基类指针或引用来调用派生类的成员的,则在调用之前,对象必须存在,而构造函数是为了创建对象的。
因此,如果把构造函数设置为虚函数,就必须在调用构造函数之前定义有指向基类的指针,即必须调用构造函数前先有对象,但是这是不成立的。
2、为什么析构函数必须是虚函数?
一句话,是为了避免内存泄露
笼统说,析构函数设置为虚函数后,在使用指针引用时可以动态联编,实现运行时多态,来保证使用基类指针能够调用适当的析构函数针对不同的对象进行清理工作。
具体来说,不加virtual的析构函数只负责清除自己类的成员。如果有基类指针指向派生类的情况下,系统就只能调用基类析构函数,而不会调用派生类析构函数。
所以,在写一个类时,尽量将其析构函数设置为虚函数。
举例:
#include<iostream>
using namespace std;
class A
{
public:
A()
{
cout<<"调用基类的构造函数"<<endl;
}
virtual ~A()
{
cout<<"调用基类的析构函数"<<endl;
}
};
class B:public A
{
public:
B()
{
cout<<"调用派生类的构造函数"<<endl;
}
virtual ~B()
{
cout<<"调用派生类的析构函数"<<endl;
}
};
void main()
{
A *p = new B();
delete p;
system("pause");
}
运行结果:
析构函数设置为虚函数:运行结果正确
调用基类构造函数
调用派生类构造函数
调用派生类析构函数
调用基类析构函数
析构函数不设置虚函数:运行结果错误,没有调用派生类析构函数
调用基类构造函数
调用派生类构造函数
调用基类析构函数
分析原因:
前提:定义一个基类指针,使用new创建一个派生类对象
如果析构函数为虚函数,当delete释放基类指针指向的派生类对象时,先调用派生类的析构函数,再调用基类的析构函数。
如果析构函数不是虚函数,就认为pa为基类指针,释放的时候只会调用基类的构造函数,而不会调用派生类的构造函数,会造成内存泄漏。