浅析C++ 对象布局

发表于:2017-3-28 10:34

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:Torival    来源:51Testing软件测试网采编

  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
21/212>
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号