C++逆向分析----虚函数与多层继承

虚函数

C++通过关键字virtual来将函数声明为一个虚函数。当一个类包含虚函数后编译器就会给类的实例对象增加一个虚表指针,所谓的虚表指针就是指向虚表的指针。虚表就是一张地址表,它包含了这个类中所有虚函数的地址。对象所在内存的前四个字节就是虚表指针。

class Test
{
public:
	int iNum = 1;
	virtual void Proc1();
	virtual void Proc2();
	virtual void Proc3();
};

Test test();

例如以上示例中,如果实例化一个Test对象,在内存中查看对象内存地址。因为前四个字节存储的是虚表指针,我们查看对应的虚表地址发现对应的三个虚拟函数的地址。


对象在刚实例化时虚表指针中的值是无意义的,当对象调用构造函数时,构造函数会对虚表指针进行初始化为当前类的虚表地址。

所以当我们没有为类提供默认的构造函数时,为了能够初始化虚表指针编译器会为类提供一个默认的构造函数,从而在构造函数中对虚表指针进行初始化。

虚函数的直接调用

test.Proc1();

当我们没有使用对象指针或者对象引用类调用虚函数时,例如通过类对象调用虚函数时,编译器并不会去查虚表,而是直接调用对应类的虚函数。(因为查表是无意义的,此时其虚表中的函数地址一定是等于实际的虚函数地址的)

虚函数的间接调用

Test* test;
test = new Test;
test->Proc1();
test->Proc3();
test->Proc2();

当我们利用指针或者是引用来调用虚函数时,编译器就会通过虚表指针查询虚表来调用虚表中对应的虚函数的地址,继而形成多态。

多层继承中的虚函数

class Test
{
public:
	Test()
	{
		Proc1();
	}
	
	~Test()
	{
		Proc1();
	}
	virtual void Proc1();
	virtual void Proc2();
	virtual void Proc3();
};

class Test1:public Test
{
public:
	virtual void Proc1();
};

Test1 test1;

上例码从Test类派生出Test1类,并在Test1类中覆盖虚函数Proc1,Test类的构造函数和析构函数中调用虚函数Proc1。
实例化一个Test1类对象,Test1的构造函数会先调用基类的Test类的构造函数。注意其传递的this指针指向的是Test1类对象。

Test类的构造函数会利用传递过来的this指针来初始化虚表指针指向自己的虚表,但注意实际此虚表指针是Test1类对象的。这样我们在Test类中调用的虚函数Proc1()就失去了应该有的多态性,编译器直接将虚函数调用变为直接调用方式。

继续调用完Test类的构造函数后回到Test1类的构造函数中,其会将虚表指针指向自己的虚表。这样就可以让指针调用虚函数时能够通过查虚表实现多态。

接着当test1对象生命周期结束时,其会先调用Test1类的析构函数。然后在调用Test类的析构函数。

Test类的析构函数会现将虚表指针在次修改指向Test类的虚表,然后我们调用虚函数Proc1就失去了多态性,编译器直接将虚函数调用变为直接调用方式。

以上虚表指针变化过程如下

那么为什么在Test1的析构函数中为什么不更改虚表指针呢?个人觉得是因为其更改后虚拟指针不会变化所以编译器将其优化没了。

至此我们可以得知类的构造函数会对虚表指针初始化,而析构函数会将虚表指针还原。且构造函数和析构函数中调用虚函数会失去多态性(c++规则),因为此时虚表指针肯定是自己的(初始化),所以调用的虚函数肯定也是自己的,全部变为直接调用方式。

那么为什么在构造函数/析构函数中把所有的虚函数调用直接变为直接调用,还要多此一举将修改虚表指针呢(在调用父类Test的构造函数时将虚表指针指向Test类自己的虚表,在调用子类Test的析构函数时将虚表指针还原为指向Test类自己的虚表)。 原因是有可能在Test的 构造函数/析构函数 时会调用一个成员函数,而这个成员函数有可能会调用虚函数形成多态。如果不还原虚拟指针的话,因为此时如果虚表指针指向Test1的虚表,此成员函数调用的虚函数就有可能是Test1类的成员函数,而此时Test1类的构造函数还没调用会访问还没初始化的成员 / 析构函数已经调用过了会访问已经无意义(释放)的成员。

原文地址:https://www.cnblogs.com/revercc/p/14044734.html