C++虚函数表的理解、通过虚函数表访问非public成员函数

时间:2021-01-15 01:44:14

阅读了陈皓老师的虚函数表解析:  点击打开链接    ,以及虚函数表存放位置一文: 点击打开链接


对C++如何实现多态应该有了个粗浅认识.

1)虚函数表是一个数组,每个元素存储virtual函数的指针
2)如果一个类存在虚函数,编译器一般会将该类实例的前4/8个字节(取决于系统位数以及编译器)初始化为虚函数表的地址.
3)虚函数表是针对类而言的,类似于static成员变量,存放在全局数据区.
(c/c++所用内存分为五种:栈区,堆区,程序代码区,全局数据区/静态区,文字常量区.)

4)类型为基类的指针(引用)能够访问函数名称是由基类决定的,因为指针类型是编译期决定的,属于静态绑定。

5)类型为基类的指针(引用)是指向基类/派生类函数是由在分配在stack或heap的实际对象决定的,因为在stack/heap构造对象都是运行期行为,属于动态绑定。


有个Base类以及Derived类,故意将Base的func都写成private:

class Base{
private:
virtual void func1(){
cout<<"Base::func1()"<<endl;
}
virtual void func2(){
cout<<"Base::func2()"<<endl;
}
virtual void func3(){
cout<<"Base::func3()"<<endl;
}

};

class Derived:public Base
{
public:
void func2(){
cout<<"Derived::func2()"<<endl;
}
virtual void func4(){
cout<<"Derived::func4()"<<endl;
}
void func5(){
cout<<"Derived::func5()"<<endl;
}
};

根据上述的3),Base类和Derived类都有一份类独有的虚函数表vtbl,每个实例都有一个指针指向它们。

根据4),通过Base类的指针只能调用func1、func2、func3,尽管实际指向类型有可能是Derived类。程序运行时,若Base* ptr绑定的是Derived类对象;

根据5),则ptr通过Derived对象内的vtbl指针实现多态。

根据名称遮掩规则,通过ptr调用func1、func2、func3,结果应该分别是Base::func1、Derived::func2、Base::func3。

C++虚函数表的理解、通过虚函数表访问非public成员函数

调用:

	Derived obj;
Base* ptrb=&obj;
//ptrb->func2(); //err
//ptrb->func4(); //err
而在这个例子中,调用func2和func4出现了编译错误。原因其实上面已经分析过:

Base类里有func2,但是访问权限是private,访问权限在编译期就已经决定了,Derived重新声明为public不起作用;Base类里没有func4,同样在编译期就已经决定了。


了解了虚函数表,并根据1)、2),可通过一些手段绕开类的访问权限,调用非public成员函数(这也是C++不足之处):

	typedef void (*func)(void);
Derived obj;

cout<<"obj_addr "<<(&obj)<<endl;
cout<<"vtbl_addr_addr "<<(long*)(&obj)<<endl;
cout<<"vtbl_addr "<<"0x"<<hex<<*(long*)(&obj)<<endl;
cout<<"fun1_addr_addr "<<(long *)(*(long*)(&obj))<<endl; //output hexadecimal: cout << hex << num
//cout<<"func1_addr "<<"0x"<<hex<<*(long *)(*(long*)(&obj))<<endl;
func ptr1=(func)(*(long *)(*(long *)(&obj)));
ptr1();
func ptr2=(func)(*((long *)(*(long *)(&obj))+1));
ptr2();
func ptr3=(func)(*((long *)(*(long *)(&obj))+2));
ptr3();
func ptr4=(func)(*((long *)(*(long *)(&obj))+3));
ptr4();
//func ptr5=(func)(*((long *)(*(long *)(&obj))+4)); //seg fault, non-virutal isnt included in vtbl
//ptr5();

C++虚函数表的理解、通过虚函数表访问非public成员函数

这里&obj是对象的地址。(long*)(&obj)将对象的地址转为vtbl指针的地址。

vtbl指针的地址解引用后是vtbl指针。(long *)(*(long*)(&obj))再次将vtbl的指针转为vtbl内指向第一个元素的指针。

最后将指向第一个元素的指针转为函数指针,即可通过此函数指针调用。

结果:

C++虚函数表的理解、通过虚函数表访问非public成员函数

这里已经访问到了Base的private func1、func3。此外注意,func5是个non-virtual,不在虚函数表内,如果调用程序可能奔溃。

那为什么Derived里重写func2的时候没有加virtual,也在虚函数表中呢?

c++规定当一个成员函数被声明为虚函数后,其派生类中的同名函数都自动成为虚函数。因此在子类重新声明该虚函数时,可加可不加,但习惯上每一层声明函数时都加virtual,使程序更加清晰。