c++的virtual function实现原理和systemVerilog的virtual function/task实现很相似
1. virtual funtion(task)的使用场景
在父类中定义virtual funtion(task),就可以在子类中重写这个function或task,最终执行的会是子类的同名function或task。
举个systemVerilog的例子,假设有一个base_case, base_case中有main_phase task。
然后需要补充100个case_1~case_100.
那么可以如下这么用
class case_1 extends base_case;
`uvm_compoent_utils(`case_1)
extern virtual task main_phase(uvm_phase phase)
endclass
task case_1::main_phase(uvm_phase phase);
super.main_phase(phase); //这里可以调用父类的main_phase
phase.raise_objection(this);
#5;
`uvm_info(get_full_name,"xxxxxx");
@(posedge clk);
phase.drop_objection(this);
endtask
如果100个case中的main_phase都有相同的操作(即共性的操作)比如上面代码中`uvm_info的打印,此时就可以将该条语句放到父类的main_phase中。
再使用super.main_phase调用父类的main_phase即可。子类自己的main_phase可以实现一些不同功能。
C++中也是同样的概念,父类的virtual function定义一些通用特性或者为空,子类可以添加自己的特性或者直接覆盖。
2.virtual function(task)的实现原理
除了下面的一篇文章还可以参考
【C++拾遗】 从内存布局看C++虚继承的实现原理https://blog.youkuaiyun.com/xiejingfa/article/details/48028491
咱们知道,与C语言相比,C++在布局和存取时间上的额外开销主要是由虚函数(virtual function)机制和虚继承(virtual base class)机制引发的。在前面一篇文章中,咱们从内存布局的角度入手,分析了虚继承的实现原理,传送门:从内存布局看C++虚继承的实现原理。svg
今天,咱们来分析C++的虚函数机制。函数
初探virtual function
在C++中,存在着静态联编和动态联编的区别。简而言之,“静态联编”是指编译器在编译过程当中就完成了联编或绑定(binding),好比函数重载,C++编译器根据传递给函数的参数和函数名称就能够判断具体要使用哪个函数,这种在编译过程当中进行的绑定就称做静态联编(static binding)。而“动态联编”是指在在程序运行时完成的联编。工具
C++经过虚函数(virtual function)机制来支持动态联编(dynamic binding),并实现了多态机制。多态是面向对象程序设计语言的基本特征之一。在C++中,多态就是利用基类指针指向子类实例,而后经过基类指针调用子类(虚)函数从而实现“一个接口,多种形态”的效果。布局
C++利用基类指针和虚函数来实现动态多态,虚函数的定义很简单,只要在成员函数原型前加上关键字virtual便可,而且,virtual关键字只要声明一次,其派生类中的相应函数仍为虚函数,能够省略virtual关键字。
下面,咱们就经过一个简单的例子来展现一下虚函数如何实现多态:spa
代码1:.net
class Base1
{
public:
virtual void func1() { cout << "Base1::func1()" << endl; }
void func2() { cout << "Base1::func2()" << endl; }
};
class Base2 : public Base1
{
void func1() { cout << "Base2::func1()" << endl; }
};
int main()
{
cout << "virtual function testing:" << endl;
Base2 b;
Base1 *ptr = &b;
ptr->func1();
ptr->func2();
}
输出以下:命令行
咱们看到,一样是基类指针指向子类对象,对于func1,调用了基类的实现版本,对于func2,却调用了子类的实现版本。因而可知设计
- 对于virtual函数,具体调用哪一个版本的函数取决于指针所指向对象类型。
- 对于非virtual函数,具体调用哪一个版本的函数取决于指针自己的类型,而和指针所指对象类型无关。
那么,这一功能是如何实现的呢?下面咱们就来探讨一下虚函数的实现原理。
virtual函数的实现原理
C++中虚函数是如何实现的呢?很多资料中都提到过,C++经过虚函数表和虚函数表指针来实现virtual function机制,具体而言:
- 对于一个class,产生一堆指向virtual functions的指针,这些指针被统一放在一个表格中。这个表格被称为虚函数表,英文又称作virtual table(vtbl)。
- 每个对象中都添加一个指针,指向相关的virtual table。一般这个指针被称做虚函数表指针(vptr)。出于效率的考虑,该指针一般放在对象实例最前面的位置(第一个slot处)。每个class所关联的type_info信息也由virtual table指出(一般放在表格的最前面)。
为了更加直观地了解上面描述的实现机制,咱们经过查看带有virtual function的类的内存布局来证明一下。关于如何查看C++类的内存布局,能够参考我以前写的一篇文章传送门。
咱们先定义一个类Base:
代码2:
class Base
{
public:
// 虚函数func1
virtual void func1() { cout << "Base::func1()" << endl; }
// 虚函数func2
virtual void func2() { cout << "Base::func2()" << endl; }
// 虚函数func3
virtual void func3() { cout << "Base::func3()" << endl; }
int a;
};
使用Visual Studio的命令行选项,咱们能够看到类Base的内存布局以下所示:
咱们能够看到在Base类的内存布局上,第一个位置上存放虚函数表指针,接下来才是Base的成员变量。另外,存在着虚函数表,该表里存放着Base类的全部virtual函数。
咱们用一幅图来展现一下Base类的内存布局:
在上图中,虚函数表中存在在一个“结束结点”,用以标识虚函数表的结束(具体实现与编译器有关)。
既然虚函数表指针一般放在对象实例的最前面的位置,那么咱们应该能够经过代码来访问虚函数表:
代码3:
int main()
{
typedef void(*pFunc)(void);
cout << "virtual function testing:" << endl;
Base b;
cout << "虚函数表地址:" << (int *)(&b) << endl;
pFunc pfunc;
pfunc = (pFunc)*((int *)(*((int *)(&b))));
pfunc();
pfunc = (pFunc)*((int *)(*((int *)(&b))) + 1);
pfunc();
pfunc = (pFunc)*((int *)(*((int *)(&b))) + 2);
pfunc();
}
在Visual Studio2013 + Window10中运行结果以下:
在代码3中,咱们把&b强转为int *,这样就取到了虚函数表的地址,而后,再次取址,获得了第一个虚函数地址。一样,咱们经过对函数指针进行下标操做就能够进一步获得第二和第三个虚函数地址。
经过上面实例演示,咱们了解了虚函数具体的实现机制。那C++又是如何利用基类指针和虚函数来实现多态的呢?这是,咱们就须要探讨一下在继承环境下虚函数表示如何工做的。
单继承环境下的虚函数
咱们先来看看简单的单继承状况。假设存在下面的两个类Base和A,A类继承自Base类:
代码4:
class Base
{
public:
// 虚函数func1
virtual void func1() { cout << "Base::func1()" << endl; }
// 虚函数func2
virtual void func2() { cout << "Base::func2()" << endl; }
// 虚函数func3
virtual void func3() { cout << "Base::func3()" << endl; }
int a;
};
class A : public Base
{
public:
// 重写父类虚函数func1
void func1() { cout << "A::func1()" << endl; }
void func2() { cout << "A::func2()" << endl; }
// 新增虚函数func4
virtual void func4() { cout << "A::func3()" << endl; }
};
咱们继续利用Visual Studio提供的命令行工具查看一下这两个类的内存布局。Base类已经在上面展现过,为了便于比较,这里再贴一次图片:
-
类Base的内存布局图:
-
类A的内存布局图:
经过两幅图片的对比,咱们能够看到:
- 在单继承中,A类覆盖了Base类中的同名虚函数,在虚函数表中体现为对应位置被A类中的新函数替换,而没有被覆盖的函数则没有发生变化。
- 对于子类本身的虚函数,直接添加到虚函数表后面。
另外,咱们注意到,类A和类Base中都只有一个vfptr指针,前面咱们说过,该指针指向虚函数表,咱们分别输出类A和类Base的vfptr:
代码5:
int main()
{
typedef void(*pFunc)(void);
cout << "virtual function testing:" << endl;
Base b;
cout << "Base虚函数表地址:" << (int *)(&b) << endl;
A a;
cout << "A类虚函数表地址:" << (int *)(&a) << endl;
}
输出信息以下:
咱们能够看到,类A和类B分别拥有本身的虚函数表指针vptr和虚函数表vtbl。到这里,你是否已经明白为何指向子类实例的基类指针能够调用子类(虚)函数?每个实例对象中都存在一个vptr指针,编译器会先取出vptr的值,这个值就是虚函数表vtbl的地址,再根据这个值来到vtbl中调用目标函数。因此,只要vptr不一样,指向的虚函数表vtbl就不一样,而不一样的虚函数表中存放着对应类的虚函数地址,这样就实现了多态的”效果“。
最后,咱们用一幅图来表示单继承下的虚函数实现:
多继承环境下的虚函数
假设存在下面这样的四个类:
代码6:
class Base
{
public:
// 虚函数func1
virtual void func1() { cout << "Base::func1()" << endl; }
// 虚函数func2
virtual void func2() { cout << "Base::func2()" << endl; }
// 虚函数func3
virtual void func3() { cout << "Base::func3()" << endl; }
};
class A : public Base
{
public:
// 重写父类虚函数func1
void func1() { cout << "A::func1()" << endl; }
void func2() { cout << "A::func2()" << endl; }
};
class B : public Base
{
public:
void func1() { cout << "B::func1()" << endl; }
void func2() { cout << "B::func2()" << endl; }
};
class C : public A, public B
{
public:
void func1() { cout << "D::func1()" << endl; }
void func2() { cout << "D::func2()" << endl; }
};
在代码6中,类A和类B分别继承自类Base,类C继承了类B和类A,咱们查看一下类C的内存布局:
咱们能够看到,类C中拥有两个虚函数表指针vptr。类C中覆盖了类A的两个同名函数,在虚函数表中体现为对应位置替换为C中新函数;类C中覆盖了类B中的两个同名函数,在虚函数表中体现为对应位置替换为C中新函数(注意,这里使用跳转语句,而不是重复定义)。
类C的内存布局能够概括为下图: