C++这门语言,几乎每个学校在大一的时候,都会去学习。但是其内在的对象布局,以及virtual机制,我们又了解多少呢。为了仔细了解了解,前几天决定找点书来啃啃。然后就听闻网上的很多人推荐<深度探究C++对象模型>,就开始啃......但是可能是书比较老的缘故,很多说的很稳的道理,一实验就翻车了。所以,为了下次不翻车,就根据这本书中对象布局的讲述结构,以及查阅各种资料,在电脑实验,写下这篇博客。如果你想很清楚的弄明白C++多态,继承(单,多,虚),以及virtual机制。那么就值得一看。
实验环境:win10__Dev C++5.7.1__gcc 4.8.1__32-bit
C++对象模型
在谈对象布局之前,我们需要知道。在C++中,有俩种类成员数据:static,nonstatic。以及三种类成员函数:static, nonstatic, virtual。那么在一个类,是如何错综复杂的摆放,上述的俩种数据成员,三种函数成员呢。其实说到底,就是分析下面五种模型。
· 无继承有虚函数
· 单继承无虚函数
· 单继承有虚函数(存在多态)
· 多重继承
· 虚拟继承(菱形继承)
0x01.无继承有虚函数
考虑如下代码。
class Point { public: Point(float val); virtual ~Point(); float x() const; static int PointCount(); private: virtual ostream& print(ostream& os) const; float _x; static int _point_count; }; |
成员摆放规则
· 非静态数据成员:保存在每一个对象里
· 静态数据成员:不在对象内
· 成员函数、静态函数:不在对象内
· 虚函数:每个类产生一个虚函数表(virtual table, vtbl),里面存放着一堆指向虚函数的指针。并且每个对象里,安插一个指针(vptr)指向该虚表。也就是说,多个对象共享一张虚表。
class Point { public: Point(){ } virtual ~Point() { } private: }; /************************ * 在C++中,虚表并不一定是对象内排列的第一个,所以我们通过构造一个 * 没有数据成员,但是却存在虚函数的类,使虚表排列在第一个。(现今, * 绝大多数编译器实现虚表时,都会把它放在最前面)至于成员分布,C++ * Standard要求:较晚出现的members,在对象中具有较高的地址。 ************************/ int main(){ Point a, b; cout << "a对象地址:" << &a << "\t" << "虚表地址:" << *(int*)&a << "\t" << endl; cout << "b对象地址:" << &b << "\t" << "虚表地址:" << *(int*)&b << "\t" << endl; return 0; } // 输出: // a对象地址:0x29fe8c 虚表地址:4738776 // b对象地址:0x29fe88 虚表地址:4738776 |
0x02.单继承无虚函数
考虑如下代码。
class Point2d { public: Point2d():_x(0.0), _y(0.0) { } // 其他函数 protected: float _x; float _y; }; class Point3d:public Point2d { public: Point3d():Point2d(), _z(0.0){ } // 其他函数 protected: float _z; }; |
这种模型相比较简单一些,就是把数据成员排列。至于排列顺序,C++Standard已经有了相对的要求。见0x01 代码注释。
0x03.单继承有虚函数(存在多态)
考虑如下代码。
class Point2d { public: Point2d():_x(0.0), _y(0.0) { } virtual ~Point2d(){ } virtual void func1() { cout << "Point2d::func1" << endl;} virtual void reload() { cout << "Point2d::reload" << endl;} // 其他函数 protected: float _x; float _y; }; class Point3d:public Point2d { public: Point3d():Point2d(), _z(0.0){ } virtual ~Point3d(){ } virtual void func2() { cout << "Point3d::func2" << endl;} void reload() { cout << "Point3d::reload" << endl;} // 其他函数 protected: float _z; }; |
注意:无论在派生类中有没有使用virtual函数,派生类中的虚表指针(vptr)都是指向另外一张表,与基类虚表(vtable)没有任何关系,不过除基类中的虚函数会被派生类的虚表所引用。但是如果出现虚函数被覆盖的情况,那么派生类虚表中的指针指向覆盖后的函数。如上图reload与func1的区别。通过派生类对虚函数的覆盖,然后向下转型,同一函数,不同体现,就是多态实现原理。测试代码如下:
/******************************* * 虚表(vtable)地址: *(int *)&a * 第一个虚函数地址:*(int *)*(int *)&a * 第 i个虚函数地址:*(int *)*(int *)&a+i * 但是在上图中,第一个却不是虚函数,当一个类的析够函数被定义为虚函 * 数,它会在虚表中占俩个格子。调用vtable[0],vtable[1]都是析够函数 * 。如果这个类是单个虚表的话,在vtable结束的地方,会补一个0. */ typedef void(*fun)(void); int main(){ Point2d a; Point3d c; cout << "a对象虚表地址:" << *(int*)&a << endl; cout << "-------------------------------" << endl; for(int i = 0; *((int *)*(int *)&a+i) != 0; i++){ cout <<"第" << i<<"个虚函数地址: "<< *((int *)*(int *)&a+i) << endl; } cout << endl << endl; cout << "c对象虚表地址:" << *(int*)&c << endl; cout << "-------------------------------" << endl; for(int i = 0; *((int *)*(int *)&c+i) != 0; i++){ cout <<"第" << i<<"个虚函数地址: "<< *((int *)*(int *)&c+i) << endl; } cout << endl; ((fun)*((int *)*(int *)&a+2))(); // 调用a.vtable[2] ((fun)*((int *)*(int *)&c+2))(); // 调用c.vtable[2] ((fun)*((int *)*(int *)&c+3))(); // 调用c.vtable[3] return 0; } // 输出结果: // a对象虚表地址:4738904 // ------------------------------- // 第0个虚函数地址: 4318248 // 第1个虚函数地址: 4318216 // 第2个虚函数地址: 4318032 // 第3个虚函数地址: 4318080 // // c对象虚表地址:4738928 // ------------------------------- // 第0个虚函数地址: 4318500 // 第1个虚函数地址: 4318468 // 第2个虚函数地址: 4318032 // 第3个虚函数地址: 4318376 // 第4个虚函数地址: 4318328 // // Point2d::func1 // Point2d::func1 // Point3d::reload |