探索C++对象模型(二)

上一篇讨论过了关于虚表的一些基础知识,除了介绍了虚函数在内存中的存储结构,还剖析了单继承与多继承的底层实现方式。

首先重提多态,多态是指C++的多种状态。

分为静态多态和动态多态。静态多态,简言之就是重载,是在编译期决议确定函数地址。动态多态主要指的是通过继承和重写基类的虚函数实现的多态,运行时决议确定,在虚表中确定调用函数的地址。C++ 实现多态,其实是通过虚表来实现。这里就菱形继承做出内存剖析。

菱形继承:

菱形继承,大致关系如下

    了解过C++的人都知道,菱形继承必然会带来数据冗余和二义性问题,通过虚继承可以来解决这个问题。首先给出一组测试代码,进入内存底部,去查看没有虚拟继承下的菱形继承,会带来什么样的问题。代码如下:

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

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

class B2 :public B
{
public:
	B2()
		:_b2_val(3){}
	virtual void test1()
	{
		cout << "B2::test1()" << endl;
	}
	virtual void test4()
	{
		cout << "B2::test4()" << endl;
	}
	virtual void test5()
	{
		cout << "B2::test5()" << endl;
	}
protected:
	int _b2_val;
};

class D:public B1,public B2
{
public:
	D()
		:_d_val(4){}
	virtual void tets1()
	{
		cout << "D::test1()" << endl;
	}
	virtual void test2()
	{
		cout << "D::test2()" << endl;
	}
	virtual void tets4()
	{
		cout << "D::test4()" << endl;
	}
	virtual void test6()
	{
		cout << "D::test6()" << endl;
	}
protected:
	int _d_val;
};

void test()
{
	D d;
}

代码说明:定义父类B,包含一个虚函数test1(),同时包含一个成员变量,构造为1;定义类B1,共有继承类 B ,包含三个虚函数,test1()重写 B 的虚函数,test2()和test3()为自己的虚函数,同时包含成员变量_b1_val,构造为2;定义类B2,共有继承类 B ,包含三个虚函数,test1()重写 B 的虚函数,test4()和test5()为自己的虚函数,同时包含成员变量_b2_val,构造为3;定义类 D,共有继承类 B1和B2,包含四个虚成员函数,test1()、test2()、test4()为重写继承父类的虚函数,test6()为类 D 自己的虚函数,同时包含成员变量_d_val,构造为4。

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

在这里,依旧没有能直接找到子类 D 自己的虚成员函数地址。接下来,分别打开两个虚表指针所指向的内存空间。

多说一点,这里两个虚表指针指向的内容是没有相同的函数指针的。

这里可以注意到,第一个继承的类的虚表中有五个地址,第二个继承的类中国有三个地址,但我们在定义类 B1 和 B2 时,是完全相同的,也都是直接共有继承,所以推断,我们的子类 D 中自己的虚函数指针是保存在了第一个继承的类的虚表中。还是沿用上一篇提到的用函数指针的方式去调用这些函数,代码如下:

typedef void(*p_fun)();

void Print_fun(p_fun* ppfun)
{
	int i = 0;
	for (; ppfun[i] != NULL; i++)
	{
		ppfun[i]();
	}
}
void test()
{
	D d;
	Print_fun((p_fun*)(*(int*)&d));
	cout << "-------------------------------" << endl;
	Print_fun((p_fun*)*((int*)&d+3));
}

这里我是直接通过指针的偏移调用到每一个函数,是因为我这里在内存中知道了有多少个函数指针,观察输出结果,如下图:

因此我们可以得到在菱形非虚拟继承下的内存布局是下图这样的关系:

在第一个继承的类的虚表中,保存了被子类 D 重写的函数test1()、test2()、test4()、test6(),和未被改写的函数test3()。第二个继承的类的虚表中,保存了被子类 D 重写的函数test1()和未被改写的函数test5(),另外还有额外保存的一份test4()。很明显,test1()函数被保存了两份,成员变量_b_val也保存了两份,在实际的开发过程中,这必然会带来大量的数据冗余和二义性的问题,造成调用不明确。

我们尝试用虚继承的方式去解决

代码如下:

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

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

class B2 : virtual public B
{
public:
	B2()
		:_b2_val(3){}
	virtual void test1()
	{
		cout << "B2::test1()" << endl;
	}
	virtual void test4()
	{
		cout << "B2::test4()" << endl;
	}
	virtual void test5()
	{
		cout << "B2::test5()" << endl;
	}
protected:
	int _b2_val;
};

class D: public B1, public B2
{
public:
	D()
		:_d_val(4){}
	virtual void test1()
	{
		cout << "D::test1()" << endl;
	}
	virtual void test2()
	{
		cout << "D::test2()" << endl;
	}
	virtual void tets4()
	{
		cout << "D::test4()" << endl;
	}
	virtual void test6()
	{
		cout << "D::test6()" << endl;
	}
protected:
	int _d_val;
};
void test()
{
	D d;
	cout << sizeof(d) << endl;//----------->40
}

代码说明:和第一个例子一样,不同之处在于类 B1 和 B2 虚拟继承了公共子类 B。

接下来,切换到监视窗口,单步调试,如下图:

说实话,刚开始看到这里的时候,内心是很拒绝的,因为感觉又多出来好多虚表指针,而且更乱了,我们对类 D 实例化出的对象求 sizeof,得到的结果是40 ,比之前未使用虚继承的时候占用的空间更多。不过按照上面给出的箭头缕缕,可以发现,尽管在原来的基础上多出了三个__vfptr,但庆幸的是三个指向了同一块空间,而且这一次,我们成员变量并没有出现出现多次的情况。

仔细观察的话,会发现绿色箭头指向的虚表指针,下面还跟了一个地址,再次走进去看看,如图:

可以看到,通过偏移,我们的指针确实是可以找到我们监视窗口中出现了三次一样的__vfptr。表明在VS环境下,我们虚继承解决菱形继承带来问题,实际上是通过指针的偏移实现的。

为了确认,我们使用我们的函数指针对函数进行调用,代码如下:

typedef void(*p_fun)();

void Print_fun(p_fun* ppfun)
{
	int i = 0;
	for (; ppfun[i] != NULL; i++)
	{
		ppfun[i]();
	}
}
void test()
{
	D d;
	Print_fun((p_fun*)(*(int*)&d));
	cout << "-------------------------------" << endl;
	//Print_fun((p_fun*)*((int*)&d+3));//测试有误,更改如下
	((p_fun*)*((int*)&d+3))[0]();
	cout << "-------------------------------" << endl;
	Print_fun((p_fun*)(*(int*)&d+8));

打印结果如图:

至此,关于虚继承对菱形继承带来问题的解决已经全部说明完毕,在VS环境下,通过指针的偏移解决了代码的冗余和二义性问题,实际开发过程中,怎么说呢。。能避免尽量避免,这并不是一种很好的做法。引入效率的下降是它的死穴,以空间和时间的代价去解决自己制造的麻烦并不是一个明智之举。如果关于虚继承,还是有不理解的地方,建议先看http://11331490.blog.51cto.com/11321490/1841930,多态的实现是C++一个很出色的地方,但有时候引入的问题还是要值得深究。

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

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

时间: 2024-10-22 20:50:06

探索C++对象模型(二)的相关文章

【深度探索C++对象模型】第二章 构造函数语意学(上)

第二章 构造函数语意学(The Semantics of Constructors) -- 本书作者:Stanley B.Lippman 一.前言 首先让我们来梳理一个概念: 默认构造函数(Default Constructor) : 是在没有显示提供初始化式时调用的构造函数.它由不带任何参数的构造函数,或是为所有形参提供默认实参的构造函数定义.如果定义的某个类的成员变量没有提供显示的初始化式时,就会调用默认构造函数(Default Contructor). 如果用户的类里面,没有显示的定义任何

【深度探索C++对象模型】第一章 关于对象

第一章 关于对象(Object Lessons) -- 本书作者:Stanley B.Lippman 一.前言 什么是 C++ 对象模型:简单的说,就是 C++ 中面向对象的底层实现机制. 本书组织: 第 1 章,关于对象(Object Lessons),介绍 C++ 对象的基础概念,给读者一个粗略的了解. 第 2 章,构造函数语意学(The Semantics of Constructors),构造函数什么时候会被编译器合成?它给我们的程序效率带来了怎样的影响? 第 3 章,Data语意学(T

深度探索C++对象模型第6章 执行期语意学

(一)对象的构造和析构(Object Construction and Destruction) 一般而言我们会把object尽可能放置在使用它的那个程序区段附近,这么做可以节省非必要的对象产生操作和摧毁操作. 全局对象 如果我们有以下程序片段: Matrix identity main() { //identity 必须在此处被初始化 Matrix m1=identity; ... return 0; } C++保证,一定会在main()函数中第一次用到identity之前,把identity

【深度探索C++对象模型】data语义学

class X{}; class Y :public virtual X{}; class Z :public virtual X{}; class A :public Y, public Z{}; void main() { cout << sizeof(X) << " " << sizeof(Y) << " " << sizeof(Z) << " " << s

深度探索C++对象模型的读书心得

参考:深度探索C++对象模型 (stanley B.Lippman著 侯捷翻译) 1. Page9 : C++对象模型,说明每一个类仅有一个虚函数表Vtbl,而类的每一个对象都有指向其表的指针. 2. Page30:引用也需要与一个指针(大小为4BYTE)相同的空间. 3. Page28: 指针类型会教导编译器如何解释某个特定地址中的内存内容及大小. 4. Page39: explicit关键字能够制止"单一参数的constructor被当做一个Conversion运算符" 5. Pa

[读书系列] 深度探索C++对象模型 初读

2012年底-2014年初这段时间主要用C++做手游开发,时隔3年,重新拿起<深度探索C++对象模型>这本书,感觉生疏了很多,如果按前阵子的生疏度来说,现在不借助Visual Studio之类的工具的话,写C++代码感觉要比较费劲,最近重读了这本书后,感觉要写点东西下来(因为懒得用笔作笔记,太慢了-_-!)加深下印象. 以前还是新手的时候,总是认为: 1.class如果没有定义任何constructor的话,编译器会自动合成一个default constructor(我习惯叫缺省的构造函数)出

《深度探索c++对象模型》chapter1关于对象对象模型

在c++中,有2种class data member:static和nostatic,以及3钟class member function:static,nostatic和virtual.已知下面这个class Point声明: class Point { public: Point(float xval); virtual ~Point(); float x() const; static int PointCount(); protected: virtual ostream& print(o

柔性数组-读《深度探索C++对象模型》有感 (转载)

最近在看<深度探索C++对象模型>,对于Struct的用法中,发现有一些地方值得我们借鉴的地方,特此和大家分享一下,此间内容包含了网上搜集的一些资料,同时感谢提供这些信息的作者. 原文如下: 例如,把单一元素的数组放在一个struct的尾端,于是每个struct objects可以拥有可变大小的数组.    code: struct mumble { //stuff char pc[1];    };        //从档案或标准输入装置中取得一个字符串,然后为struct本身和该字符串配置

【深度探索c++对象模型】Function语义学之虚函数

虚函数的一般实现模型:每一个class有一个virtual table,内含该class之中有作用的virtual function的地址,然后每个object有一个vptr,指向virtual table. 识别class是否支持多态,唯一恰当的方法是看它是否有virtual function,只要class拥有virtual function,它就需要额外的执行期信息. 考虑ptr->z(),ptr是基类指针,z是虚函数,为了找到并调用z()的适当实体,我们需要两项信息: 1.ptr所指对象

《深度探索c++对象模型》chapter3 Data语意学

一个空的class:如 class X{} ; sizeof(X)==1; sizeof为什么为1,他有一个隐晦的1 byte,那是被编译器安插进去的一个char,这使得class2的两个objects得以在内存中配置独一无二的地址: X a,b; if(&a==&b) cerr<<"yipes!"<<endl; class X{}; class Y:public virtual X{}; class Z:public virtual X{};