面向对象的主要目的之一是提供可重用的代码。面向对象主要包括封装、继承、多态等特性,在做项目的时候碰到的最多的应该是继承了,但是总是对继承的感觉是模模糊糊的,在上一篇中记录了三种继承,本篇记录一下在继承过程中虚函数的作用以及使用。
虚函数的主要作用是在继承中实现函数的动态绑定,进而形成多态。它可以根据对象的实际类型实现相应的操作。下面根据例子来说明。
基类代码如下:
BaseClass.h
class BaseClass
{
public:
BaseClass(void);
virtual ~BaseClass(void);
public:
virtual void v_func1();
virtual void v_func2();
void ord_func1();
void ord_func2()
{
cout<<"ord_fun2 execute!"<<"\n";
}
private:
virtual void v_func3();
private:
int m_nvalue;
int m_ncount;
};
BaseClass.cpp
#include "BaseClass.h"
BaseClass::BaseClass(void)
{
m_nvalue =10;
m_ncount =10;
}
BaseClass::~BaseClass(void)
{
}
void BaseClass::v_func1()
{
cout<<"v_func1 execute!"<<"\n";
return ;
}
void BaseClass::v_func2()
{
cout<<"v_func2 execute!"<<"\n";
return ;
}
void BaseClass::v_func3()
{
cout<<"v_func3 execute!"<<"\n";
return ;
}
void BaseClass::ord_func1()
{
cout<<"ord_func1 execute!"<<"\n";
this->v_func3();
}
如上所示BaseClass中声明三个虚函数以及一个虚析构函数(基类析构函数一般声明为虚函数:v_func1();v_func2();v_func3();
派生类代码如下:
DeriveClass.h
#include "baseclass.h"
class DeriveClass :
public BaseClass
{
public:
DeriveClass(void);
~DeriveClass(void);
public:
virtual void v_func1();
virtual void v_func3();
virtual void v_func4();
void ord_func1(int invalue);
};
DeriveClass.cpp
#include "DeriveClass.h"
DeriveClass::DeriveClass(void)
{
}
DeriveClass::~DeriveClass(void)
{
}
void DeriveClass::v_func1()
{
cout<<"DeriveClass v_func1 execute"<<"\n";
}
void DeriveClass::v_func4()
{
cout<<"DeriveClass v_func4 execute"<<"\n";
}
void DeriveClass::v_func3()
{
cout<<"DeriveClass v_func3 execute"<<"\n";
}
void DeriveClass::ord_func1(int invalue)
{
cout<<"DeriveClass ord_func1 execute"<<"\n";
}
派生类DeriveClass中继承了基类BaseClass中公有虚函数v_func1(),重写了虚函数v_func2(),声明了新的虚函数v_func4()。
下面是主程序:
#include "DeriveClass.h"
typedef void (*Func)(void);
int main()
{
cout<<"****************************************************"<<"\n";
BaseClass* temp_ptr = new DeriveClass();
temp_ptr->ord_func1();
temp_ptr->v_func1();
delete temp_ptr;
temp_ptr = NULL;
cout<<"****************************************************"<<"\n";
system("pause");
return 0;
}
主程序中分配了一个派生对象,声明了一个基类指针指向该派生对象。通过指针调用了虚函数v_func1()。执行结果如下:
可以看到实际执行的是派生类的v_fun1()。那么虚函数是如何完成这种操作的呢?实际上c++对虚函数有一个专门存储的地方:虚函数表(vtbll),虚表地址即为存储在每个类的首地址,虚表里面按照声明顺序存储多个虚函数的函数地址。下面就是使用命令看看BaseClass的内存布局。将命令/d1 reportSingleClassLayoutBaseClass 放在 VS->properties->c/c++->Command line->addtional options中,重新编译工程即可看到BaseClass的布局:
如上所示可以看到:虚函数表地址就存储在类的首地址处,这个虚函数表默认是4个字节(32位),所以只要有虚函数声明的对象至少是4个字节大小。虚函数表指向的区域:包含一个析构函数和三个自定义虚函数的函数地址。现在知道了虚函数的函数地址,我们完全可以不管访问权限,直接通过地址访问对象的虚函数。如下一段程序:
cout<<"****************************************************"<<"\n";
BaseClass temp_class;
cout<<"通过对象调用访问虚函数:"<<"\n";
temp_class.v_func1();
temp_class.v_func2();
cout<<"通过地址直接访问虚函数:"<<"\n";
int* pBase = (int*)&temp_class; //对象所在地址
int* vtbl_addr = (int*)(*pBase);//虚函数表的地址
Func pFunc = (Func)(vtbl_addr[1]);//第一个虚函数
pFunc();
pFunc = (Func)(vtbl_addr[2]);//第二个虚函数
pFunc();
pFunc = (Func)(vtbl_addr[3]);//第三个虚函数
pFunc();
cout<<"****************************************************"<<"\n";
程序中根据上述BaseClass的内存布局,使用找到函数地址,直接访问了三个自定义的虚函数。第三个虚函数在声明的时候访问权限是私有的。
那么执行主程序运行结果是:
可以看见通过函数地址访问和通过对象访问虚函数结果一样,不同之处是可以通过函数地址访问私有的虚函数v_func3()。
上述分析了基类中虚函数的位置,那么在派生类中的虚函数存储是什么呢?仍然使用上述方式获取派生类的内存布局:
从内存布局中可以看到:如果继承类重新声明了基类的虚函数,那么在派生类的虚函数表中就会把基类中的同名的虚函数的地址覆盖;派生类虚函数表中派生类新声明的虚函数地址在基类中已经声明的虚函数地址之后。
所以主程序调用的时候按照对象的实际类型查找函数地址就会自然而然找到派生类的函数地址,从而实际执行的就是派生类的虚函数。
在此延伸一下,在23中设计模式中的模板方法模式,实际上基于虚函数的多态实现的实,在在上面的程序中就存在,就是主程序中这句程序:
temp_ptr->ord_func1();
在执行完这句代码之后,执行结果如下:
根据上面分析:在基类的ord_func1()中只是调用了基类自己的函数v_func3(),但是执行结果则是ord_func1()确实执行的是基类的实现,但是v_fun3()执行的是派生类的实现。
这就是模板方法的原理。基类提供一个模板方法,方法中某些具体实现则在每个派生类中。
如果在声明虚函数的时候在函数最后添加=0;则表示虚函数是纯虚函数,拥有纯虚函数的类称之为抽象类,不可以实例化,纯虚函数必须在派生类中实现,派生类才可以被实例化。
如果派生类是多重继承,比如有两个基类,派生类的内存布局是什么?实际上与用法是一致的。留在下一节吧。