C++继承关系中Data Member和Functions Member的内存布局(基于C++ Object Model描述)

C++继承关系中的动态内存分配从四种情况分析考虑:

1. 单一继承且无虚函数的对象模型

例:
class A
{
public:
    int foo( )  {    return  val ;    }
    static int staFun( )  {     return  staVal ;  }
    static int staVal ;
private:
    int val ;
    char bit1 ;
} ;
 
class B : public A
{
public:
    char foo( )  {    return  bit2;    }
private:
    char bit2 ;
};


内存布局:

静态数据成员:

它被编译器提出于class之外,并被视为一个global变量(但只在class生命范围之内可见)

每个静态数据成员只有一个实体,存放在程序的数据段之中,

经由‘.’运算符,对一个静态数据成员进行存取操作,只是语法上的一种便宜行事而已。静态数据成员其实并不在class object之中,因此存取它并不需要通过class object。

 

虽然你可以不靠class object 来存取一个静态成员,但其存取函数却得绑定于一个class object之上。(若静态成员的访问控制为protected或private,则必须通过存取函数来访问)

 

【注意:】类的静态数据成员,必须要在全局下进行定义,然后才能使用。无论它的访问控制是什么,必须在main函数之前,对静态数据成员定义。

例如:

int A::staVal = 0 ;

否则,编译器会报错:未定义的引用。

因为编译器把涉及静态数据成员的使用,都转换为直接使用,而静态成员在class中声明,外部无法看见。故需要在外部进行定义,使其后的代码可见。

 

成员函数的处理:

C++的设计准则之一:非静态成员函数至少和一般的外部函数有相同的存储效率

C++编译器会把成员函数内化为一般的函数

①改写函数原型,安插一个额外的参数this指针。用以提供一个存取管道,使类对象得以调用该函数

int A::foo (A* const this)

若该成员函数是const,则变成:

int A::foo (const A* const this)

②对函数体中 类对象的非静态数据成员的存取操作,改为经由this指针来存取

int A::foo (A* const this){    return  this->val ;   }

③将成员函数重新写成一个外部函数,对函数名称进行处理,使它在程序中成为独一无二的。

以上的转换操作结束之后,每个调用操作都要转换。

A objA ;
A * ptr = & objA ;

ptr->foo( ) ; 
objA.foo() ;
//分别被转换为:
foo_intA( ptr ) ;
foo_intA( & objA ) ; 

静态成员函数:

静态成员函数的主要特征是它没有this指针

故其:

①它不能够直接存取其class中的非静态成员

②它不能够直接被声明为const、virtual

③它不需要经由class object才被调用——虽然大部分时候它是这样被调用的。

 

对静态成员函数的调用

objA.staFun( ) ;
ptr->staFun( ) ;
//会转换为:
staFun_staticintvoid( ) ;
staFun_staticintvoid( ) ;

静态成员函数由于缺乏 this 指针,因此差不多等同于外部函数。只是它作用于类的静态数据成员上


2. 单一继承且有虚函数的对象模型

例:

class A
{
public:
    virtual int foo( )  {    return  val ;    }
    virtual int funA( ) {}
private:
    int val ;
    char bit1 ;
} ;
 
class B : public A
{
public:
    virtual int foo( )  {    return  bit2;    }
    virtual int funB( ) {}
private:
    char bit2 ;
};

内存布局:

一个class只要有一个虚函数,那么每一个class object被安插上一个由编译器内部产生的指针,指向该表格(virtual table)。

virtual table 的第一项是表示class的类型。

因为,基类指针的特殊性,它可以指向基类对象,也可以指向派生类对象。故:ptr->foo( ) ;这种调用,我们需要知道ptr所指对象的真实类型。(就算不知道ptr所指对象的类型,也可以正确调用fun函数,但是由于fun函数有编译器插入的this指针,this指针要与ptr指向的对象地址正确对应,以正确访问对象中的成员变量,但ptr中却没有这样的信息

virtual table 之后的表格是class中的每个虚函数地址。

 

一个class只会有一个virtual table。派生类的virtual table是在基类的virtual table上增加,修改的。

派生类中的虚函数会改写(overriding)与基类中同名且参数相同的虚函数,把virtual table表中相应的基类虚函数地址改写为相应派生类虚函数的地址。

函数:

以上工作都是由编译器完成的。执行期要做的就是在特定的virtual table表项中激活相应的虚函数,然后根据virtual table首项的类型信息,正确执行此虚函数

 

例如:

ptr->normalize( ) ;

一般而言,我并不知道ptr所指对象的真正类型。然而我知道。经由ptr可以存取到该对象的virtual table。虽然我不知道哪一个foo( )实体会被调用,但我知道每一个foo( )函数的地址都放在虚表的第二项。

故:根据以上信息,编译器可以将该调用转化为:

( *ptr->vptr[1] )( ptr ) ;

  1. vptr是由编译器产生的指向虚函数表的指针,安插在每一个有虚函数,或继承了虚函数的类的对象中,事实上也会被mangled,因为在一个复杂的class派生体系中,可能存在多个vptrs。
  2. 1 表示virtual table slot的索引值,关联到normalize函数
  3. 第2个ptr表示this指针

       如果normalize函数中又调用了一个同一个类中的虚函数virtual float Point3d::magnitude(),它在normalize()虚函数中被调用,则会进行一定的优化:

    因为已经对normilize由虚拟机制决议妥当,所以这个虚函数可以被显示调用,在显示调用下,可以压制虚拟机制,避免不必要的寻址和转换,提高效率。 显示调用应该如下所示

//virtual float Point3d::magnitude();    //声明

float mag = Point3d::magnitude();        //带class声明,显示调用格式

来源: <http://blog.csdn.net/xlf13872135090/article/details/38426955>



【注意:】基类指针虽然可以指向派生类,但是它实际上指向的是派生类中的基类部分。(这就不违反指针的特性了,指针类型与其指向范围是一致的)故基类指针不能访问派生类的成员。(但基类指针可以通过访问派生类的虚函数,间接操作派生类成员


 

3. 多重继承

​例:
class A
{
public:
    A() {}
    virtual ~A() {}
    virtual int foo( )  {   return  val ;  }
    virtual int funA( ) {}
private:
    int val ;
    char bit1 ;
} ;
 
class B :
{
public:
    B() {}
    virtual ~B() {}
    virtual int foo( )  {   return  bit2;  }
    virtual int funB( ) {}
private:
    char bit2 ;
};
 
class Derived : public A, public B
{
public:
    Derived() {}
    virtual ~Derived() {}
    virtual int foo( )  {   return  bit3;  }
    virtual int funDerived( ) {}
private:
    char bit3 ;
};

内存布局:

 

 

注意:在多重继承下,若有n个基类,则派生类中有n个virtual table.

针对每一个virtual table,派生类对象中有对应的vptr。这些vptrs将在构造函数中被设立初值。

 

派生类的虚函数会覆盖(改写)其每个基类virtualtable中相应的虚函数索引值

 

多重继承最左端的基类,在派生类中作为主要实体,其virtualtable为主要表格,其它基类的virtual table为次要表格。

当你将Derived对象地址指定给一个Base1指针或Derived指针时,被处理的virtualtable是主要表格vptr_Base1。

故主要表格要包含Derived的所有虚函数(包括继承得来的虚函数)。所以其中有Base1、Base2、Derived中的虚函数。

而其它次要表格中的项目数不变,只是有些虚函数的索引值被重写

 

涉及多重继承的指针的转换

多重继承的问题主要发生于:派生类对象其第二或后继的基类对象之间的转换。

【对一个多重派生对象,将其地址指定给“最左端(也就是第一个)”base class的指针,情况将和单一继承时相同,因为二者都指向相同的起始地址。需付出的成本只有地址的指定操作而已。至于第二个或后继的base class的地址指定操作,则需要将地址修改:加上(或减去)介于中间的base class subobject(s)大小

例如:

Derived dObj ;
A* pA = &dObj ;

只需要简单地拷贝地址就行了。

 

而:

Derived* pd ;
B* pB = pd ;

需要这样的内部转化:

//虚拟C++码
pB = pd ? (B*)( (char*)pd + sizeof(A) ) : 0  ;


4. 含虚继承的多重继承

1、虚基类无数据成员

例:

class A {  } ;
class B : public virtual A {  } ;
class C : public virtual A {  } ;
class D : public B, public C { };



 

【注意】class A { };实际上并不是空的,它有一个隐晦的1字节,那是被编译器安插进去的一个char。这是为了使得class A的对象得以在内存中配置独一无二的地址。

当语言支持虚基类时,就会导致一些额外负担。在派生类中会有一个额外指针,指针指向一个相关表格,表格中存放的或者是虚基类对象,或者是其偏移量。

若虚基类为空,则表格中存放的是虚基类对象(即一个安插字节)

 

 

VC++编译器的优化:

VC++特别对 空virtual baseclass做了处理。空虚基类的派生类中只有一个4字节的指针。没有安插char,也没有字节填充。

(因为安插char的目的是为了空类实例化的对象在内存中有地址,而现在其派生类对象中已经有个指针了,其可以取地址,故不需要安插char了

 

 

注意:如果虚基类中有数据成员,则两种编译器(“有特殊处理者”和“无特殊处理者”)就会产生完全相同的对象布局。

 

2、虚基类有数据成员

例:

class A
{
public:
     …
private:
    int x, y ;
} ;
 
class B : public virtual A
{
public:
    …
private:
    int valB ;
} ;
 
class C : public virtual A
{
public:
    …
private:
    int valC ;
} ;
 
class D : public B, public C
{
public:
    …
private:
    int valD ;
};


最终的派生类对象底部是共享虚基类的部分,派生类class B、class C部分的指针指向的虚函数表的前面增加了一项:offset从对象的开头算起,到共享虚基类部分的字节数这样可以快速地访问到共享虚基类的成员。

 

问:

①为什么虚继承不像一般继承那样,把基类成员放在顶部,把新增派生类成员放在尾部?

我想是:因为在最后的多重继承时,要求最终的派生类对象中只有一份基类成员,故最终派生类会从其各个父类中提取它们各自的派生数据成员。若按一般继承那样的对象模型,很容易找到父类中的派生类成员,但难以确定边界,故难以提取数据。为了提取数据方便,把基类成员放在尾部。

 

②为什么虚继承的派生类中有两个虚指针?

我想是:因为要实现多态。当基类指针指向派生类中的基类部分时,必须要有指向虚表的指针,才能实现多态。

 

注意:class D对象模型与一般多重继承的派生类对象的布局相似,多重继承最左边的基类部分的虚表是“主要表格”。故class B、class C、class A部分的虚表各不相同。它们都是为了实现多态

 

【编程风格】一般而言,virtual base class最有效的一种运用形式是:一个抽象的virtual base class,没有任何数据成员

来源: <http://blog.csdn.net/yang_yulei/article/details/8738802>

来源: <http://blog.csdn.net/yang_yulei/article/details/8741547>

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值