现在的位置: 首页 > 综合 > 正文

C++对象模型(二)

2019年03月13日 ⁄ 综合 ⁄ 共 2615字 ⁄ 字号 评论关闭

第三章 数据语义学

       本章着重介绍了C++的类数据对象的布局。

对于一般的类来说,影响内存布局的几个因素主要有:

1、  语言本身的负担,如vptr等;

2、  编译器对特殊情况的处理;

3、  字节对齐的限制。

  这些都影响了类的大小。

  C++标准没有明确规定编译器各种数据的编排规则,只是规定“较晚出现的成员具有较高的地址”。所以,在一个public里声明八个数据和声明八个public的数据占用的空间是一样的。不用在意成员在类中的顺序。

  下面通过一些比较将本段讲的一些内容反映出来。

  1、静态数据成员和一般类成员的区别:

  静态数据成员是储存在数据区的,而一般类成员存储在栈中;

  如果两个类中声明了相同的静态成员,编译器会自动mangling

  普通成员调用依赖于类对象的存在,而静态成员不是;

对于一个普通的类成员,其声明如下:

 
Point3d Point3D::translate(const Point3d &pt)

{

  x+=pt.x;

  y+=pt.y;

  z+=pt.z;

}

通过编译器的转换,变为如下形式:

 
Point3d Point3D::translate(Point3d *const this,
const Point3d &pt)

{

  This->x+=pt.x;

  This->y+=pt.y;

  This->z+=pt.z;

}

就是说,每个成员函数都加了一个this指针,以实现对类成员的调用。

2、直接取类数据于指针取类数据的区别:

对一个非静态成员进行存取操作,就是将类对象的地址加上数据成员的偏移地址。

对于一般的类来说,通过a.get()a->get()取是没有区别的。但是对于有虚拟基类的情况,就有区别了。普通的取法在编译的时候,各个成员的offset就已经定了,直接取出即可;对于指针来说,由于编译的时候不能确定指针到底指向哪个类型,所以只有执行期才能进行存取操作。

类设计时的问题:考虑效率,或者类结构清晰性之间的矛盾。

对于有三个数据成员的类,到底是放到一个类中呢还是使用两个继承呢?

Class A

{

  ……

  Private:

  Int x,

Char y, z;

};

或者:

Class A

{

  ……

  Private:

  Int x;

};

Class B : public A

{

  ……

  Private:

  Char y;

};

Class C : public B

{

  ……

  Private:

  Char z;

};

这两种设计各有优越性,第一钟效率较高,第二种比较清晰。

第一种占用的空间:411=6=8;

第二种占用的空间:

A: 4; B:13 = 4; C:13 = 4。共占用12字节。

为什么第二种方法中不将前一个类中未占用的补齐呢?作者还解释了这样做的无奈。由于要考虑到mebemvise的拷贝构造方式,如果补齐,当A类向B类或C类拷贝时,就会出现数据错误。

 

多重继承的数据成员布局

       这里解释了这样的问题:当一个类从多个类继承时,并且这些类可能从其他类中继承的情况下,类的数据结构如何布局?

       答案是,先将基类的成员依次排列,最后放本类的数据成员。

       问题是有多个基类,这些基类的顺序是怎样的呢?C++标准并没有定义。但是一般来说,编译器会把先定义的基类的数据成员放在前面。

       由此,写代码的时候,如果需要将派生类转换为基类类型时,必须注意以下两点:

1、 
如果在派生类中,该基类成员排在所有数据最前面,则直接转换,不需要任何代价;

2、 
如果在派生类中,该基类成员没有排在最前面,则编译器会自动转换,加上一定的位移。这样做是有代价的。

 

虚拟继承的数据成员布局

       当一个类A继承自两个基类BC,而BC都继承自同一个父类D时,会遇到这样的问题:类A中可能有两个类D的对象。要解决这个问题,引入了虚拟继承的概念。

       传统的解决方法是,编译器对每一个基类对象加一个指针。但是,如果继承链过长,会导致指针增多;且类对象的负担不能固定。

       目前的编译器想到了很多解决的方法来解决这个问题。

       微软用的方法是利用了virtual base class table,将虚拟基类的指针放在该表中,通过该表得到指针实现基类的访问。

       Solaris的做法有些不一样。虽然也是用了virtual base class
table
Solaris巧妙的使用了索引,正索引将访问到虚函数,而负索引则取到想要的虚拟基类的位移。注意是位移。

       最后说明,基于以上的分析,取虚拟基类的数据成员代价是比较高的。最好声明的虚拟基类没有任何数据成员。

 

对象成员的效率

       对变量、数组、继承、虚拟继承的应用,使用相同的算法,效率是不一样的。

很明显,变量、数据、继承(包括多重继承,但作者不确定,要视编译器而确定),效率都是一样的。虚拟继承的效率令人失望。

 

指向数据成员的指针

使用指向数据成员的指针,关键是看这个类的vptr放在类的哪个部位。一般放在头部或尾部。

       对于某些编译器,往往还用在位移上加一个字节。加这个字节的目的是,区分没有指向任何数据成员的指针和已经指向一个数据成员的指针。因为,如果没有这个字节,当指针指向第一个类成员时,有可能和空指针的内容相同。

VC编译器中,没有该附加的字节。

#include <stdio.h>

 

class Pointnd

{

public:

       virtual
test(){int i = 0;};

//     static
Pointnd origin;

       float
x, y, z;

};

 

void main()

{

       Pointnd
origin;

       printf("::x
= %p/n", &Pointnd::x);

       printf("::y
= %p/n", &Pointnd::y);

       printf("::z
= %p/n", &Pointnd::z);

       printf("origin.x
= %p/n", &origin.x);

       printf("origin.y
= %p/n", &origin.y);

       printf("origin.z
= %p/n", &origin.z);

}

 

输出结果:

::x = 00000004

::y = 00000008

::z = 0000000C

origin.x = 0012FF74

origin.y = 0012FF78

origin.z = 0012FF7C

抱歉!评论已关闭.