189 8069 5689

探索c++对象模型(一)

    C++中虚函数的诞生,就是为了多态的实现。当子类对父类的虚函数进行了重写,在父类指针调用重写的虚函数时,如果父类指针(或引用)指向了父类的对象,则调用父类的虚函数,如果父类指针(或引用)指向了子类对象,则调用子类的虚函数。

成都创新互联服务项目包括正阳网站建设、正阳网站制作、正阳网页制作以及正阳网络营销策划等。多年来,我们专注于互联网行业,利用自身积累的技术优势、行业经验、深度合作伙伴关系等,向广大中小型企业、政府机构等提供互联网行业的解决方案,正阳网站推广取得了明显的社会效益与经济效益。目前,我们服务的客户以成都为中心已经辐射到正阳省份的部分城市,未来相信会继续扩大服务区域并继续获得客户的支持与信任!

    要想了解多态的实现,就必须要知道虚函数表的构成。

【注:文章中代码测试环境为Win7 64位 VS2013】

首先,我们讨论单个含有虚函数的类,即不存在继承关系。

    当我们的类中含有虚函数时,类实例化出来的对象,他的成员除了自己的成员变量外,还会多出一个指针。这个指针我们称为虚表指针,他所指向的是我们类对象所维护的虚表。虚表中保存的是类中所有虚函数的地址。代码如下:

class B
{
public:
	B()
		:_val(1){}
	virtual void fun1()
	{
		cout << "void B::fun1()" << endl;
	}
	virtual void fun2()
	{
		cout << "void B::fun2()" << endl;
	}
	void fun3()
	{
		cout << "void B::fun3()" << endl;
	}
private:
	int _val;
};

int main()
{
	B b;
	cout << sizeof(b) << endl;//输出结果为8
	system("pause");
	return 0;
}

    代码说明:构造函数给类成员变量赋值为1,方便内存查看,成员函数有两个虚函数,一个非虚函数,断点打在return 0;处。

    然后我们切换到监视窗口,<如下>可以发现,对象b实际上维护了两个成员,"__vfptr"和"_val",在内存窗口中"&b",得到的是 0012cc74 ,即 __vfptr,下一个是 00000001,即我们的成员 _val。这也解释了为什么 sizeof(b) 的输出结果是8。不管有多少虚函数,类中只保留了一个虚表指针,添加再多的虚函数,也不会改变sizeof(b)的值

    另外,可以看到的是虚表只保存虚函数地址,非虚函数,依旧是属于整个类而非对象。

    

探索c++对象模型(一)

我们再次查看 __vfptr 指向的空间

探索c++对象模型(一)

前两个是我虚函数的地址,也就是说,他们之间存在关系如下

探索c++对象模型(一)

    为了验证,这里我重新封装一个函数指针,通过偏移,看能不能输出cout里面的内容。代码如下:

typedef void(*p_fun)();

void Print_fun(p_fun* ppfun)
{
	for (int i = 0;/* ppfun[i] != NULL*/i<2; i++)
	{
		ppfun[i]();
	}
}
void test()
{
	B b;
	Print_fun((p_fun*)(*(int*)&b));
}

测试是可以输出我们函数内容的。多说一句,虚表中前面保存的都是虚函数的地址,最后结束项在不同编译器下是不一样的,在VS2013环境下,最后一项保存的地址是不可访问的,VS2008环境下,最后是以0x00000000结尾,即是NULL。所以打印函数Print_fun()中for循环条件我做了修改。当然可以更加直接的这样调用函数。

//	((p_fun*)(*(int*)&b))[0]();
//	((p_fun*)(*(int*)&b))[1]();

接下来,我们看看包含虚函数重写的单继承中的虚表

    这里给出单继承的测试代码

class A
{
public:
	A()
		:_a_val(1){}
	 virtual void fun1()
	{
		cout << "void A::fun1()" << endl;
	}
	virtual void fun2()
	{
		cout << "void A::fun2()" << endl;
	}
protected:
	int _a_val;
};

class B:public A
{
public:
	B()
		:_b_val(2){}
	virtual void fun1()
	{
		cout << "virtual B::fun1()" << endl;
	}
	virtual void fun3()
	{
		cout << "virtual B::fun3()" << endl;
	}
	virtual void fun4()
	{
		cout << "virtual B::fun4()" << endl;
	}
protected:
	int _b_val;
};

void test()
{
	B b;
}

    代码说明:父类 A 包含两个虚函数 fun1() 、fun2(),一个成员变量 _a_val ,构造成员变量为 1 ;子类 B 共有继承了 A ,重写函数 fun1() ,同时添加两个自己的虚函数 fun3()、fun4(),成员变量 _b_val ,构造为 2 。

    接下来切换到调试窗口<如下图>,可以看到类B实例化对象 b 实际上维护了三个成员,"__vfptr"、"_a_val"、"_b_val",在内存窗口中“&b”,得到的是0x009bcd48,对应到监视窗口,即 __vfptr ,接下来是0x00000001,0x00000002,即成员变量_a_val,_b_val。如果在这里去sizeof(b),得到的结果应该是12。

探索c++对象模型(一)

接下来查看 __vfptr 指向的空间

探索c++对象模型(一)

    监视中看到的是只有两个函数地址,但内存窗口中可以看到,前四个在内存中是在一起的,或者说很近。为了确认,使用刚刚的打印函数,不过需要改变一下循环次数,受编译器的限制,这里只能手动修改循环次数,看最多打印多少次是正常结束,而非程序崩溃。

typedef void(*p_fun)();
void Print_fun(p_fun* ppfun)
{
	for (int i = 0;/* ppfun[i] != NULL*/i<4; i++)
	{
		ppfun[i]();
	}
}
void test()
{
	B b;
//	((p_fun*)(*(int*)&b))[0]();
//	((p_fun*)(*(int*)&b))[1]();
//	((p_fun*)(*(int*)&b))[2]();
//	((p_fun*)(*(int*)&b))[3]();
	Print_fun((p_fun*)(*(int*)&b));
}

     测试得到,最多可以打印四次,打印结果如下:

探索c++对象模型(一)

    *可以看到fun1()函数被子类 B 重写,fun2()函数继承自父类。得到结果监视窗口未显示虚函数fun3()和fun4()地址,但实际上子类新创建的虚函数地址也会保存到虚表当中,而且在单继承过程中,子类的虚函数和父类的虚函数是保存在同一虚表当中,并未对子类的虚函数创建独立的虚表。

    即有下图关系:

探索c++对象模型(一)

接下来的多继承中的对象模型

    首先给出测试代码,如下

class A
{
public:
	A()
		:_a_val(1){}
	virtual void test1()
	{
		cout << "A::test1()" << endl;
	}
	virtual void test2()
	{
		cout << "A::test2()" << endl;
	}
protected:
	int _a_val;
};

class B
{
public:
	B()
		:_b_val(2){}
	virtual void test1()
	{
		cout << "B::test1()" << endl;
	}
	virtual void test3()
	{
		cout << "B::test3()" << endl;
	}
protected:
	int _b_val;
};

class C
{
public:
	C()
		:_c_val(3){}
	virtual void test1()
	{
		cout << "C::test1()" << endl;
	}
	virtual void test4()
	{
		cout << "C::test4()" << endl;
	}
protected:
	int _c_val;
};

class D:public A,public B,public C
{
public:
	D()
		:_d_val(4){}
	virtual void test1()
	{
		cout << "D::test1()" << endl;
	}
	virtual void test5()
	{
		cout << "D::test5()" << endl;
	}
protected:
	int _d_val;
};

void test()
{
	D d;
}

    代码说明:首先创造三个基类,类 A 包含两个虚函数fun1()、fun2(),类包含成员变量 _a_val ,构造为1;类 B 包含两个虚函数 fun1()、fun3(),类包含成员变量 _b_val,构造为2;类 C 包含两个虚函数 fun1()、fun4(),类包含成员变量 _c_val,构造为3;创建第四个类,作为派生类 D ,同时共有继承类A、类B、类C,包含虚函数fun1(),fun2(),fun1()函数对子类中的fun1()进行重写,同时包含成员变量_d_val。

        接下来切换到调试窗口<如下图>,可以看到类 D 实例化对象 d 这里维护了七个成员,由于继承了三个类,因此这里有三个虚表指针"__vfptr"、同时包含继承自三个类的成员变量"_a_val"、"_b_val"、"_c_val"和自己本身的成员变量"_d_val"。在内存窗口中“&d”,得到的是0x013bdd04,对应到监视窗口,即继承的第一个类的虚表指针 __vfptr ,接下来是0x00000001,即成员变量_a_val,接下来依次类推,得到第二个类的虚表指针,和继承自第二个类的成员变量,第三个类的虚表指针,和继承自第三个类的成员变量,最后一项是子类 D 的成员变量。如果在这里去 sizeof(b),得到的结果应该是28。

探索c++对象模型(一)

    不过这里有个问题,是子类 D 的虚函数地址在哪里。。这里我们打开多个内存窗口,同时把各个虚表指针指向的内容列出来。<如图>

    探索c++对象模型(一)    可以看到,尽管监视窗口中,A的虚表指针下只有两项,但对应到内存中却有三项,可以推测,子类单独的虚函数地址是保存在了第一继承子类的虚函数表中,未覆盖的虚函数不会单独创建一块虚函数表。

    除此之外,还应该可以看到,子类每继承一个含有虚函数的父类,就会多一个虚表指针,可能会同时维护多个虚表。

    换句话说,存在如下图对应关系。

探索c++对象模型(一)

    多提一点,子类继承了多个父类,父类虚表的地址不一定是连续的

    这里依旧使用函数指针的方式去调用我的成员函数来加以验证。代码如下

typedef void(*p_fun)();
void Print_fun(p_fun* ppfun)
{
	for (int i = 0; ppfun[i] != NULL; i++)
	{
		ppfun[i]();
	}
}

void test()
{
	D d;
	Print_fun((p_fun*)(*(int*)&d));
	cout << "-----------------------------------------" <

打印结果如下:

探索c++对象模型(一)

    由打印结果可见,子类专有的虚函数test5()的函数地址放在了第一个继承的虚表中,test1()函数均被子类 D 重写。







    我们通过虚函数表理解C++ 中的对象模型,了解多态实际上是用虚函数实现覆盖,但通过上面的测试,可以发现,实现多态的同时,无疑会带来效率的下降(通过两次指针解引用才可以访问)。

    除此之外应该看到的一点是,多态实现的过程是不安全的,尽管虚函数表的内容我们不能够随意修改,但永远可以被直接访问,这是不安全的一种直接表现。

    关于菱形继承的对象模型和菱形虚拟继承的对象模型,会在下一篇中提到。

------------------------------------------muhuizz------------------------------------------

                                                                http://11331490.blog.51cto.com 


文章标题:探索c++对象模型(一)
URL地址:http://cdxtjz.cn/article/ppijds.html

其他资讯