1.虚函数就是在基类中被关键字 virtual 说明,并在派生类中重新定义的函数。
2.虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。
3.虚函数的定义是在基类中进行的,它是在基类中在那些需要定义为虚函数的成员函数的声明中冠以关键字 virtual 。
定义虚函数的方法如下:
virtual 函数类型 函数名(形参表){
函数体;
}
总而言之,虚函数是在编译时,并不能确定的类函数,而是在运行时确定的。
核心点:通过基类对象访问派生类实现的函数。
2.虚函数的例子虚函数的例子,通常有三步。
第一步,定义基类,声明基类函数为
virtual
的。第二步,定义派生类(继承基类),派生类实现了定义在基类的
virtual
函数。第三步,声明基类指针,并指向派生类,调用
virtual
函数,此时虽然是基类指针,但调用的是派生类实现的基类virtual
函数。
// 例子来源于: 菜鸟教程
class A
{
public:
virtual void foo()
{
cout<<"A::foo() is called"<foo(); // 在这里,a虽然是指向A的指针,但是被调用的函数(foo)却是B的!
return 0;
}
3.纯虚函数
纯虚函数与虚函数的区别在于,纯虚函数的基类中的virtual
函数,只定义了,但不实现。实现交给派生类来做。
PS:带纯虚函数的类也叫抽象类,因为这种基类不能直接生成对象。
优点:
防止派生类忘记实现虚函数,纯虚函数使得派生类必须实现基类的虚函数。
在某些场景下,创建基类对象是不合理的,含有纯虚拟函数的类称为抽象类,它不能生成对象。
声明方法
在基类中纯虚函数的方法的后面加 =0:
// 例子来源于: 菜鸟教程
virtual void funtion()=0
4.虚函数的使用
例1:
#include
using namespace std;
class B0{
public:
virtual void print(char *p){ //定义虚函数 print
cout<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<
(2)在派生类中,虚函数被重新定义时,其函数的原型与基类中的函数原型(即包括函数类型、函数名、参数个数、参数类型的顺序)都必须完全相同。
(3)C++ 规定,当一个成员函数被定义为虚函数后,其派生类中符合重新定义虚函数要求的同名函数都自动称为虚函数。因此,在派生类中重新定义该虚函数时,关键字 virtual 可以不写。 但是,为了使程序更加清晰,最好在每一层派生类中定义该函数时都加上关键字 virtual。
(4)如果在派生类中没有对基类的虚函数重新定义,则公有派生类继承其直接基类的虚函数。一个虚函数无论被公有继承多少次,它仍然保持其虚函数的特性。 例如:
class B0{
···
public:
virtual void show(); //在基类定义 show 为虚函数
};
class B1:public B0{
···
};
若在公有派生类 B1 中没有重新定义虚函数 show ,则函数 show 在派生类中被继承,仍是虚函数。
(5)虚函数必须是其所在类的成员函数,而不能是友元函数,也不能是静态成员函数,因为虚函数调用要靠特定的对象来决定该激活哪个函数。
(6)使用对象名和点运算符的方式调用虚函数是在编译时进行的,是静态联编,没有利用虚函数的特性。只有通过基类指针访问虚函数时才能获得运行时的多态性。
例 2:使用对象名和点运算符的方式调用虚函数
#include
using namespace std;
class B0{
public:
virtual void print(char *p){ //定义虚函数 print
cout<
5.虚析构函数
我们在动态分配堆上内存的时候,析构函数必须是虚函数
原因:动态分配堆上内存,无法自动回收。若基类指针指向派生类,然后基类指针调用delete
方法,只能释放基类的内存,无法释放派生类特有的部分内存,进而导致内存泄露。
析构函数定义成虚函数,基类指针调用delete
方法,会先调用派生类的析构函数,然后自动调用基类的析构函数。
所以,将可能被继承的基类的构造函数设置为虚函数,可以防止用基类指针指向子类是,释放基类指针是可以释放掉子类独有的空间,进而防止内存泄漏。
例1
#include
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 运算符撤销无名对象时,系统只执行基类的析构函数,而不执行派生类的析构函数。
例2:虚析构函数的使用
#include
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;
}
测试结果:
由于使用了虚析构函数,程序执行了动态联编,实现了运行的动态性。虽然派生类的析构函数与基类的析构函数名字不相同,但是如果将基类的析构函数定义为虚函数,由该基类所派生的所有派生类的析构函数也都自动成为虚函数。
二、虚函数表虚函数表是由有虚函数的类生成的,简称为 V-Table
。
虚函数表由编译器生成,如果一个类有虚函数,那么该类就会生成一个4个字节的虚函数表指针,指向虚函数表。指针存储在对象实例的最前面位置。
虚函数表就可以理解为一个数组,每个单元用来存放虚函数的地址(这个概念与我们前面学习的deque的中控器有些相似,可以分段管理duque中使用的分段连续线性空间)
1.单继承下的虚函数表派生类直接继承基类虚函数
下图展示了一个派生类继承基类虚函数,且没有重写基类的虚函数,对应的虚函数指针、虚函数表、和虚函数表对应指针调用的方法。可以看出:
虚函数表中的指针顺序,按照虚函数声明的顺序。
基类的虚函数指针在派生类的前面。
派生类重写基类虚函数
下图展示了一个派生类重写基类虚函数,且重写基类的部分虚函数,对应的虚函数指针、虚函数表、和虚函数表对应指针调用的方法。可以看出:
2.多继承下的虚函数表虚函数表中,派生类重写的虚函数替换了基类虚函数指针,并指向了派生类的函数实现。
多继承下的虚函数表,还是只有一个虚函数表。
多个基类之间的虚函数,按照继承的顺序,存放虚函数指针。
基类内部的虚函数,按照虚函数内部声明的顺序存放。
派生类直接继承基类虚函数
下图展示了多继承下的虚函数表,派生类直接继承基类虚函数,类似与单继承下的虚函数表的情况。
区别在于:
多个基类之间的虚函数,按照继承的顺序,存放虚函数指针。
基类内部的虚函数,按照虚函数内部声明的顺序存放。
派生类重写基类虚函数
下图展示了多继承下的虚函数表,派生类重写部分基类虚函数,类似与单继承下的虚函数表的情况。
区别在于:
多个基类之间的虚函数,按照继承的顺序,存放虚函数指针。
基类内部的虚函数,按照虚函数内部声明的顺序存放。
虚函数表中,派生类重写的虚函数替换了基类虚函数指针,并指向了派生类的函数实现。
这就是C++虚函数和虚函数表的部分内容了,相信通过今天的学习,我们一定能对虚函数和虚函数表有着更加深刻的理解。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)