当基类的指针指向子类的对象,释放对象的内存时,如果基类的析构函数没有定义为虚函数,那么只会调用基类的析构函数,不会调用子类的析构函数,容易导致内存泄漏等问题。
对于这种情况,可以将基类的析构函数设置为虚函数,即在基类的析构函数声明时加上virtual关键字,实现动态析构,当基类(该类为基类的情况下)的指针指向的是基类对象时,只调用基类的析构函数,当基类的指针指向的是子类的对象,那么先调用子类的析构函数,再调用基类的析构函数。
虚函数就是实现多态的手段,虚函数只需要在声明时加上virtual关键字,在函数实现的时候不需要加virtual。
如果某个成员函数被声明为虚函数,那么它的子类所继承的这个成员函数,也自动是虚函数。
按照子类型的概念,用子类对象代替父类,调用方法时,调用的是父类中的方法,但是,当父类的成员函数声明为virtual(虚函数)时,子类及子类的子类所继承的该成员函数也默认是虚函数。
虚函数意味着,即使用父类的指针指向一个子类对象,调用的方法也是调用子类中的同名方法,而不是父类的同名方法。
虚函数是通过虚函数表来实现的,虚函数表并不在对象的内存中,对象的内存中有虚函数表指针。
用代码来验证:
Father.cpp
#pragma once
class Father
{
public:
Father(void);
~Father(void);
virtual void fun1();
virtual void fun2();
virtual void fun3();
void fun4();
public: //使用public是为了便于测试
int x;
int y;
static int z;
};
Son.h
#pragma once
#include"Father.h"
class Son:public Father
{
public:
Son(void);
~Son(void);
//子类重写虚函数
virtual void fun1(); //这里就算不加virtual,编译器也会自动加上
//子类定义一个新的虚函数
virtual void fun5();
};
Father.cpp
#include<iostream>
#include "Father.h"
using namespace std;
int Father::z=0;
Father::Father(void)
{
x=200;
y=300;
}
Father::~Father(void)
{
}
void Father::fun1(){
cout<<"Father::fun1()"<<endl;
}
void Father::fun2(){
cout<<"Father::fun2()"<<endl;
}
void Father::fun3(){
cout<<"Father::fun3()"<<endl;
}
void Father::fun4(){
cout<<"非虚函数: Father::fun4()"<<endl;
}
Son.cpp
#include<iostream>
#include "Son.h"
using namespace std;
Son::Son(void)
{
}
Son::~Son(void)
{
}
void Son::fun1(){
cout<<"Son::fun1()"<<endl;
}
void Son::fun5(){
cout<<"Son::fun5()"<<endl;
}
main.cpp
#include<iostream>
#include<Windows.h>
#include"Father.h"
#include"Son.h"
using namespace std;
typedef void(*func_t)(void); //定义一个函数指针,指向返回类型void,参数也是void的函数
int main(){
//虚函数的原理是通过虚函数表来实现的
Father father;
//注意现在是win32环境下,指针长度4字节
//结果发现对象的内存只有12个字节(内容是一个虚函数表指针(4字节),两个int普通数据成员(8字节)),
//没有静态数据成员,也没有成员函数(虚函数表也不在对象的内存中)
cout<<"sizeof(father) :"<<sizeof(father)<<endl;
/*
下边是为了验证对象的内存中第一个是虚函数表指针
以及虚函数表在内存中的位置及结构,尝试使用地址来调用虚函数
*/
cout<<"父类对象地址:"<<(int*)&father<<endl; //强制转成指针会按照16进制打印
//&father取出对象father的地址,(int*)是将对象father的地址转换成一个4字节的整数(指针)(对象的第一个内存存的是虚函数表指针)
//然后*解引就是虚函数表指针内存的值,即指向的虚函数地址,再(int*)类型强转赋值给vfptr
int* vfptr=(int*)*(int*)(&father); //现在vfptr就相当于虚函数表指针
cout<<"调用第一个虚函数:"<<endl;
((func_t)*vfptr)(); //*vfptr解引虚函数表指针,强制转换成函数指针类型,然后调用
cout<<"调用第二个虚函数:"<<endl;
((func_t)*(vfptr+1))();
cout<<"调用第三个虚函数:"<<endl;
((func_t)*(vfptr+2))();
//下边验证对象的内存中第二存储的是普通数据成员
cout<<"第一个数据成员的地址:"<<&father.x<<"----"<<hex<<(int*)&father+1<<endl; //这里将地址转为int*类型的指针,加1就相当于加了4字节
cout<<hex<<(int)&father+4<<endl; //hex是十六进制的意思 因为这里将地址转换为int类型,所以加的是4字节
cout<<"第一个数据成员的值是:"<<dec<<father.x<<endl; //dec是十进制打印
cout<<*((int*)((int)&father+4))<<endl;
/************使用继承的虚函数表************/
cout<<"-------------------------------------"<<endl;
Son son;
cout<<sizeof(son)<<endl;
cout<<"子类对象son地址:"<<(int*)&son<<endl; //转成int*会按照十六进制打印
int *vptr=(int*)(*(int*)&son);
cout<<"子类虚函数表指针的值:"<<vptr<<endl;
//调用子类虚函数表的虚函数 (现在子类有四个虚函数)
for(int i=0;i<4;i++){
cout<<"调用第"<<i+1<<"个虚函数"<<endl;
//((func_t)((*(int*)&son)+i))(); //这样写报错的原因是需要先把虚函数表的内容转成(int*),再解引,才能转函数指针
((func_t)*((int*)(*(int*)&son)+i))();
((func_t)*(vptr+i))();
}
//调用子类对象内存的两个数据成员x,y
for(int i=1;i<3;i++){
cout<<"子类对象的第"<<i<<"个数据成员的值:"<<*((int*)&son+i)<<endl;
}
system("pause");
return 0;
}
纯虚函数
如果某个类不需要创建实例对象,只是作为基类放一些共性的成员且这些成员在本类中不需要具体实现,就可以把这些成员定义为纯虚函数(纯虚函数不需要实现)
这个类也就叫抽象类
纯虚函数即只提供形式上的接口,让子类来做具体的实现
某个类中只要包含了纯虚函数,那么这个类就叫抽象类(抽象类不能创建对象)
虚函数的修饰
final(final是C++11的新特性,vs2010不支持):final修饰类的时候(在类名后加final),该类无法被继承(即该类不能做基类,不会有子类)
final用来修饰类的虚函数的时候,该虚函数在子类中就不能被重写,但是子类还可以调用父类的这个final虚函数。
override只能修饰虚函数 override修饰的虚函数表示该虚函数是重写父类的方法,在程序员阅读代码的时候便于理解,也可以防止程序员写错函数名(即,如果在子类重写的时候,override修饰的虚函数,没有在继承自父类的虚函数表中找到该虚函数,那么就说明虚函数名写错了,程序报错),override没有功能上的改变。