C++区别的C的一大特性就是C++是面向对象的,面向对象有着三大特性:继承性,封装性和多态性。其中C++的动态多态性是通过虚函数来实现的。简单的说,通过virtual函数,指向子类的基类指针可以调用子类的函数。 下面先来看一段代码:#include <iostream>
using namespace std ;
class BaseClass
{
public :
BaseClass() = default ;
virtual void fun ()
{
cout << "call fun in BaseClass" << endl ;
}
virtual void fun2 ()
{
cout << "call fun2 in BaseClass" << endl ;
}
virtual ~BaseClass() = default ;
};
class DerivedClass: public BaseClass
{
public :
DerivedClass() = default ;
virtual void fun ()
{
cout << "call fun in DerivedClass" << endl ;
}
virtual void fun2 ()
{
cout << "call fun2 in DerivedClass" << endl ;
}
virtual ~DerivedClass() = default ;
};
void *getp (void *p)
{
return (void *)*(unsigned long *)p;
}
typedef void (*fun) () ;
fun getfun (void * obj, unsigned int off)
{
void *vptr = getp(obj);
unsigned char *p = (unsigned char *)vptr;
p += sizeof (void *) * off;
return (fun)getp(p);
}
int main (void )
{
BaseClass *b = new DerivedClass();
b->fun();
delete b;
return 0 ;
}
运行结果: BaseClass::fun() DerivedClass::fun()
程序仅通过基类的指针,却调用了子类的函数。其实编译器在编译的时候使用了延迟绑定技术(late binding)。 通过virtual关键字创建虚函数能引发晚捆绑,编译器在幕后完成了实现晚捆绑的必要机制。它对每个包含虚函数的类创建一个表(称为VTABLE),用于放置虚函数的地址。在每个包含虚函数的类中,编译器秘密地放置了一个称之为vpointer(缩写为VPTR)的指针,指向这个对象的VTABLE。所以无论这个对象包含一个或是多少虚函数,编译器都只放置一个VPTR即可。VPTR由编译器在构造函数中秘密地插入的代码来完成初始化,指向相应的VTABLE,这样对象就“知道”自己是什么类型了。 VPTR都在对象的相同位置,常常是对象的开头。这样,编译器可以容易地找到对象的VTABLE并获取函数体的地址。如下图所示: 我们可以做一个实验,如果定义一个只有一个虚函数的空类,那么sizeof这个类的大小应该是一个VPTR的大小,即4(32位系统)或8(64位系统),而如果定义一个个类,这个类里面有2个虚函数,那么sizeof这个类的大小是多少呢?也是4或8,因为它只保存了一个VPTR,这个VPTR指向的VTABLE才保存了真正虚函数地址。代码如下:#include <iostream>
using namespace std ;
class A
{
virtual void fun ()
{
cout << "A::fun()" << endl ;
}
};
class B
{
virtual void fun ()
{
cout << "B::fun()" << endl ;
}
virtual void fun1 ()
{
cout << "B::fun1()" << endl ;
}
};
int main (void )
{
cout << "sizeof A: " << sizeof (A) << endl ;
cout << "sizeof B: " << sizeof (B) << endl ;
return 0 ;
}
运行结果: sizeof A: 8 sizeof B: 8
既然知道了以上原理,其实我们可以自己编写代码来通过查找vtable获取虚函数的实际地址。首先我们需要获取vptr这个指针,因为vptr通常都在对象的开头,假设现在我们有一个BaseClass的对象指针p,那么在64位系统上我们通过(void*)*(unsigned long *)p
即可获得vptr指向的vtable的首地址。而vtable其实就是一个指针数组,按照虚函数在类中被声明的顺序排列。因此,如果我们需要获取虚函数表的第2个函数,只要把指针后移1个指针的长度就可以了。至于返回的指针,可以存放在typedef void (*fun)();
中,代码如下:#include <iostream>
using namespace std ;
class BaseClass
{
public :
BaseClass() = default ;
virtual void fun ()
{
cout << "BaseClass::fun()" << endl ;
}
virtual void fun2 ()
{
cout << "BaseClass::fun2()" << endl ;
}
virtual ~BaseClass() = default ;
};
class DerivedClass: public BaseClass
{
public :
DerivedClass() = default ;
virtual void fun ()
{
cout << "DerivedClass::fun()" << endl ;
}
virtual void fun2 ()
{
cout << "DerivedClass::fun2()" << endl ;
}
virtual ~DerivedClass() = default ;
};
void *getp (void *p)
{
return (void *)*(unsigned long *)p;
}
typedef void (*fun) () ;
fun getfun (void * obj, unsigned int off)
{
void *vptr = getp(obj);
unsigned char *p = (unsigned char *)vptr;
p += sizeof (void *) * off;
return (fun)getp(p);
}
int main (void )
{
BaseClass a;
BaseClass *b = new DerivedClass();
BaseClass &c = a;
a.fun();
b->fun();
c.fun();
fun f1 = getfun(b, 0 );
fun f2 = getfun(b, 1 );
(*f1)();
(*f2)();
delete b;
return 0 ;
}
输出如下: BaseClass::fun() DerivedClass::fun() DerivedClass::fun() DerivedClass::fun() DerivedClass::fun2() 可见,我们成功通过虚函数表的方式,执行了虚函数,也知道了C++中的动态性原来是这样实现的。