探索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)的值

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

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

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

为了验证,这里我重新封装一个函数指针,通过偏移,看能不能输出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。

接下来查看 __vfptr 指向的空间

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

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));
}

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

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

即有下图关系:

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

首先给出测试代码,如下

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。

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

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

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

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

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

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

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 << "-----------------------------------------" <<endl;
	Print_fun((p_fun*)(*((int*)&d+2)));
	cout << "-----------------------------------------" << endl;
	Print_fun((p_fun*)(*((int*)&d+4)));
	cout << "-----------------------------------------" << endl;
}

打印结果如下:

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








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

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

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

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

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

时间: 2024-11-06 11:04:25

探索c++对象模型(一)的相关文章

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

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

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

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

[读书系列] 深度探索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++对象模型】第一章 关于对象

第一章 关于对象(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++对象模型】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{};

C++的黑科技(深入探索C++对象模型)

周二面了腾讯,之前只投了TST内推,貌似就是TST面试了 其中有一个问题,“如何产生一个不能被继承的类”,这道题我反反复复只想到,将父类的构造函数私有,让子类不能调用,最后归结出一个单例模式,但面试官说,单例模式作为此题的解答不够灵活,后来面试官提示说,可以用友元+虚继承,可以完美实现这样一个类 当然那时我还不太明白,友元与虚继承我都极少接触过,只是知道有这些东西,回头搜了一下“不能被继承的类”的做法,具体如下: 1,声明一个类,CNoHeritance,构造函数为private,并声明友元类C