您的位置:首页 > 编程语言 > C语言/C++

C++ 对象内存布局及其大小

2009-10-24 08:51 405 查看
(1)

空类的大小为1字节,这是一个占位符。

(2)

普通成员函数,静态成员函数,及静态成员变量皆不会影响对象的大小。

普通成员函数,静态成员函数,及静态成员变量皆不会在类的对象中有所表示,成员函数和对象的关联由编译器在编译时处理,正如我们会在后面看到的那样,编译器会在编译时决议出正确的普通成员函数地址,并将对象的地址以this指针的方式,做为第一个参数传递给普通成员函数,以此来进行关联静态成员函数类似于全局函数,和具体的对象关联。静态成员变量也一样。静态成员函数和静态成员变量和普通的全局函数及全局变量不同之处在于它们多了一层名字限定。

(3)

普通继承,子类的对象布局为父类中的数据成员加上子类中的数据成员,多层继承时,顶层类在前,多重继承时则最左父类在前。

(4)

在虚拟继承的菱型继承中,共同的父类在内存中只有一份,且放在最后面;但不是虚继承的V型继承中,则保存多份,从上倒下,从左到右保存;即普通继承的布局,顶层类在前面;多重继承时则按从左到右的顺序排。而虚继承则要求将共享的父类放到整个对象布局的最后。

(5)

当一个类中有虚函数时,编译器会为该类产生一个虚函数表,并在它的每一个对象中插入一个指向该虚函数表的指针,通常这个指针是插在对象的起始位置。所谓的虚函数表实际就是一个指针数组,其中的指针指向真正的函数起始地址。

(6)

虚表是类级别的,类的所有对象共享同一个虚表。

(7)

2对象地址不同,虚表指针(vpadr)位于对象的起始位置,所以它的地址和对象相同.

(8)

从2个父类(有虚函数)继承时,不修改继承来的虚函数,子类要有两个虚表,而不是将它们合并为一个。主要是在处理类型的动态转换时这种对象布局更方便调整指针。

(9)

由于使用了虚继承,父类的成员变量被放到了最后面。

(10)

构造一个对象时,首先会按对象的大小得到一块内存(在heap上或在stack上),然后会把指向这块内存的指针做为this指针来调用类的构造函数,对这块内存进行初始化。如果对象有父类就会先调用父类的构造函数(并依次递归),如果有多个父类(多重继承)会依次对父类的构造函数进行调用,并会适当的调整this指针的位置。在调用完所有的父类的构造函数后,再执行自己的代码。

(11)

C++标准规范。在12.7.3条中有明确的规定。这是一种特例,在这种情况下,即在构造子类时调用父类的构造函数,而父类的构造函数中又调用了虚成员函数,这个虚成员函数即使被子类重写,也不允许发生多态的行为。即,这时必须要调用父类的虚函数,而不子类重写后的虚函数。

C++对象布局

转自: http://blog.csdn.net/Vitin/archive/2007/06/09/1645134.aspx

有以下几点需要考虑:
1.非静态成员变量的大小
2.有继承关系的,其基类的大小
3.有虚函数的,一般有一个虚函数表指针(注意,只是一般,虽然现在所有的编译器都是这样实现虚函数的,但它并不是C++标准的一部分)。
4.有虚继承的,对每一个虚继承基类,一般有一个虚基类索引(指针或偏移量,“一般”的理由同第二点)。这些索引可以直接放在对象的存储空间内,如果是偏移量的话,也可以作为一项放在虚函数表中。(后面一种情况占了大多数,因为它更节省空间)
5.如果对象本身需要空间为空,即不符合1-4任何一点的,也要为它分配1字节,以避免不同对象指向同一个地址。
6.按字长对齐,这是为了硬件效率而做的,也不是必须的(但是貌似所有的编译器都做了这一点)

除了以上六点,各编译器还可以在优化策略上做各种选择,如基类子对象的位置,虚表指针的位置(开头或结尾居多),虚基类子对象的位置,虚基类索引在类或虚表中的位置,而所有这些位置又都影响了最后一点:对虚表指针共用和不能共用的情况(共用的情况:如B继承A,则A和B有可能可以共用一个虚表指针,指向B的虚表,这需要A的虚表项在B的虚表中位置一致,此时虽然指向B的虚表,但在A看来,和A的虚表无区别。这还需要虚表指针的位置策略与基类子对象的位置策略不冲突(如基类子对象固定在开头,虚表指针则固定在结尾,那么除非无成员对象,否则不能共用)。在多继承中,同一层次上最多只能和一个基类共用,因为不同基类的空间不能重叠)。所有这些策略都由编译器自行决定。结合开始六点空间需求的来源,就形成了千变万化的空间大小了。

为了不局限编译器开发者的创新能力,C++标准没有对所有这些硬性规定,所以在编码时尽量不要对对象的布局和大小做任何假设,这也是sizeof存在的原因之一。我们一般只要有一个定性的认识——如虚函数往往意味着更大的空间,再如指向基类的指针和指向派生类的指针未必存储了同一个地址(特别是多继承/虚继承的时候)等等——就足以应付绝大多数的情况了。

另:Inside the C++ Object Model真的是一本很好的书,这里所说的一切均出自它。此外还有The Design and Evolution of C++也很有用。想学好C++的人都应该读它们。

转自:http://dev.yesky.com/254/2191254.shtml

C++对象布局及多态实现探索之内存布局

前言

  本文通过观察对象的内存布局,跟踪函数调用的汇编代码。分析了C++对象内存的布局情况,虚函数的执行方式,以及虚继承,等等。

  写这篇文章源于我在论坛上看到的一个贴子。有人问VC使用了哪种方式来实现虚继承。当时我写了一点代码想验证一下,结果发现情况比我想象的要复杂。所以我就干脆认真把相关的问题都过了一遍,并记录成本文。

  我对于C++对象模型的知识主要来自于Lippman的书《Inside the C++ Object Model》,中译版为候捷翻的《深度探索C++对象模型》,中英版我都看过,不过我还是推荐中译版,因为中译版的确翻得不错,而且候捷加入了很多的图,并修正了原版中的一些错误。

  我所使用的编译器是VC7.1,文中的代码我都在VC7.1上验证通过。如果在其他的编译器下运行需要作相应的调整,即使是VC7.0和VC6也是如此。不同编译器产生的汇编代码也不一样,如果你在不同编译器上编译文中的代码生成出的汇编代码和我所列出的不同,也不足为奇。如果你想在其他的编译器上验证这些代码请自行做相应的改动。

  另外我发现VC7.1在实现虚继承时所用的方法和Lippman在书中提到的微软所用的方法不同,不过那时还没有VC7.1。有趣的是,Lippman在写那本书时,是在迪斯尼工作,应该是做和三维影片的渲染软件相关的事。而现在他已经到了微软,相信应该是主导VC7.1编译器的设计工作。

  在后文中可以看到列出的很多汇编代码,有些明显效率很低。这可能是因为我没有打开编译器的优化开关。打开优化开关,设置不同的优化选项后,编译器可能产生出高效得多的汇编代码。有兴趣的朋友可以自行试试,并和文中列出的汇编代码做一下比较。

  为了便于分析和观察对象的内存布局,我把代码生成时的结构成员对齐选项设置为1字节,默认为8字节。如果你在自己的工程下编译文中的代码,请做同样的设置。因为我写了一些函数打印对象中的布局信息,如果对象选项不是1字节,运行这些代码会出现指针异常错误。

  普通类对象的内存布局

  首先我们从普通类对象的内存布局开始。C000为一个空类,定义如下:

struct C000
{};
  运行如下代码打印它的大小及对象中的内容。

PRINT_SIZE_DETAIL(C000)
  结果为:

The size of C000 is 1
The detail of C000 is cc
  可以看到它的大小为1字节,这是一个占位符。我们可以看到它的值是0xcc。在debug模式下,这表示是由编译器插入的调试代码所初始化的内存。在release模式下可能是个随机值,我测试时值为0x00。

  定义两个类,C010和C011如下:

struct C010
{
 C010() : c_(0x01) {}
 void foo() { c_ = 0x02; }
 char c_;
};
struct C011
{
 C011() : c1_(0x02), c2_(0x03) {}
 char c1_;
 char c2_;
};
  运行如下代码打印它们的大小及对象中的内容。

PRINT_SIZE_DETAIL(C010)
PRINT_SIZE_DETAIL(C012)
  结果为:

The size of C010 is 1
The detail of C010 is 01
The size of C011 is 2
The detail of C011 is 02 03
  我们从对象的内存输出中可以看到,它们的值就是我们在构造函数中赋的值,C010为0x01,C011为0x0203。大小分别为1、2。

  定义C012类。

struct C012
{
 static int sfoo() { return 1; }
 int foo() { return 1; }
 char c_;
 static int i_;
};
int C012::i_ = 1;
  在这个类中我们加入了一个静态数据成员,一个普通成员函数和一个静态成员函数。

  运行如下代码打印它的大小及对象中的内容。

PRINT_SIZE_DETAIL(C012)
  结果为:

The size of C012 is 1
The detail of C012 is cc
  可以看到它的大小还是1字节,值为0xcc是因为我们没有初始化它,原因前面说过了。

  从上面的结果我们可以映证,普通成员函数,静态成员函数,及静态成员变量皆不会在类的对象中有所表示,成员函数和对象的关联由编译器在编译时处理,正如我们会在后面看到的那样,编译器会在编译时决议出正确的普通成员函数地址,并将对象的地址以this指针的方式,做为第一个参数传递给普通成员函数,以此来进行关联。静态成员函数类似于全局函数,不和具体的对象关联。静态成员变量也一样。静态成员函数和静态成员变量和普通的全局函数及全局变量不同之处在于它们多了一层名字限定。

  普通继承类对象的内存布局

  下面看看普通继承类对象的内存布局。

  定义一个空类C014从C011继承,再定义C015也是一个空类从C010和C011继承。

struct C010
{
 C010() : c_(0x01) {}
 void foo() { c_ = 0x02; }
 char c_;
};
struct C011
{
 C011() : c1_(0x02), c2_(0x03) {}
 char c1_;
 char c2_;
};
struct C014 : private C011
{};
struct C015 : public C010, private C011
{};
  运行如下代码打印它们的大小及对象中的内容。

PRINT_SIZE_DETAIL(C014)
PRINT_SIZE_DETAIL(C015)
  结果为:

The size of C014 is 2
The detail of C014 is 02 03
The size of C015 is 3
The detail of C015 is 01 02 03
  C014的大小为2字节,也就是C011的大小,对象的内存值也是在C011的构造函数中初始化的两个值0x0203。C015的大小为3字节,也就是C010和C011的大小之和,对象的内存值为0x010203。

  这里我们可以发现父类的成员变量悉数被子类继承,并且于继承方式(公有或私有)无关,如C015是私有继承自C011。继承方式只影响数据成员的“能见度”。子类对象中属于从父类继承的成员变量由父类的构造函数初始化。通常会调用默认构造函数,除非子类在它的构造函数初始化列表中显式调用父类的非默认构造函数。如果没有指定,而父类又没有缺省构造函数,则会产生编译错误。

  我们可以再加一层继承来验证一下。定义类C016,从C015继承,并有自己的4字节int成员变量。

struct C016 : C015
{
 C016() : i_(1) {}
 int i_;
};
  运行如下代码打印它的大小及对象中的内容。

PRINT_SIZE_DETAIL(C016)
  结果为:

The size of C016 is 7
The detail of C016 is 01 02 03 01 00 00 00
  它的大小为7字节,也就是C015的大小(也即是C010和C011的大小和)加上自身的4字节int变量之和。同样对象的内存输出也验证了这一点,前三个字节为从父类继承的,后4个字节为自身的int变量,值为1。

  因此关于普通继承,子类的对象布局为父类中的数据成员加上子类中的数据成员,多层继承时(如C016),顶层类在前,多重继承时则最左父类在前。

C++对象布局及多态实现之带虚函数的类

转自:http://dev.yesky.com/290/2204790.shtml

如果类中存在虚函数时,情况会怎样呢?我们知道当一个类中有虚函数时,编译器会为该类产生一个虚函数表,并在它的每一个对象中插入一个指向该虚函数表的指针,通常这个指针是插在对象的起始位置。所谓的虚函数表实际就是一个指针数组,其中的指针指向真正的函数起始地址。我们来验证一下,定义一个无成员变量的类C040,内含一个虚函数。

struct C040
{
 virtual void foo() {}
};
  运行如下代码打印它的大小及对象中的内容。

PRINT_SIZE_DETAIL(C040)
  结果为:

The size of C040 is 4
The detail of C040 is 40 b4 45 00
  果然它的大小为4字节,即含有一个指针,指针指向的地址为0x0045b440。

  同样再定义一个空类C050,派生自类C040。

struct C050 : C040
{};
  由于虚函数会被继承,且维持为虚函数。那么类C050的对象中同样应该含有一个指向C050的虚函数表的指针。

  运行如下代码打印它的大小及对象中的内容。

PRINT_SIZE_DETAIL(C050)
  结果为:

The size of C050 is 4
The detail of C050 is 44 b4 45 00
  果然它的大小也为4字节,即含有一个指向虚函数表(后称虚表)的指针(后称虚表指针)。

  虚表是类级别的,类的所有对象共享同一个虚表。我们可以生成类C040的两个对象,然后通过观察对象的地址、虚表指针地址、虚表地址、及虚表中的条目的值(即所指向的函数地址)来进行验证。

  运行如下代码:

C040 obj1, obj2;
PRINT_VTABLE_ITEM(obj1, 0, 0)
PRINT_VTABLE_ITEM(obj2, 0, 0)
  结果如下:

obj1 : objadr:0012FDC4 vpadr:0012FDC4 vtadr:0045B440 vtival(0):0041D834
obj2 : objadr:0012FDB8 vpadr:0012FDB8 vtadr:0045B440 vtival(0):0041D834
  (注:第一列为对象名,第二列(objadr)为对象的内存地址,第三列(vpadr)为虚表指针地址,第四列(vtadr)为虚表的地址,第五列(vtival(n))为虚表中的条目的值,n为条目的索引,从0开始。后同)

  果然对象地址不同,虚表指针(vpadr)位于对象的起始位置,所以它的地址和对象相同。两个对象的虚表指针指向的是同一个虚表,因此(vtadr)的值相同,虚表中的第一条目(vtival(0))的值当然也一样。

接下来,我们再观察类C040和从它派生的类C050的对象,这两个类各有自己的虚表,但由于C050没有重写继承自C040的虚函数,所以它们的虚表中的条目的值,即指向的虚函数的地址应该是一样的。

  运行如下代码:

C040 c040;
C050 c050;
PRINT_VTABLE_ITEM(c040, 0, 0)
PRINT_VTABLE_ITEM(c050, 0, 0)
  结果为:

c040 : objadr:0012FD4C vpadr:0012FD4C vtadr:0045B448 vtival(0):0041D834
c050 : objadr:0012FD40 vpadr:0012FD40 vtadr:0045B44C vtival(0):0041D834
  果然这次我们可以看到虽然前几列皆不相同,但最后一列的值相同。即它们共享同一个虚函数。

  定义一个C043类,包含两个虚函数。再定义一个C071类,从C043派生,并重写继承的第一个虚函数。

struct C043
{
 virtual void foo1() {}
 virtual void foo2() {}
};
struct C071 : C043
{
 virtual void foo1() {}
};
  我们可以预料到,C043和C071各有一个包含两个条目的虚表,由于C071派生自C043,并且重写了第一个虚函数。那么这两个类的虚表的第一个条目值是不同的,而第二项应该是相同的。运行如下代码。

C043 c043;
C071 c071;
PRINT_SIZE_DETAIL(C071)
PRINT_VTABLE_ITEM(c043, 0, 0)
PRINT_VTABLE_ITEM(c071, 0, 0)
PRINT_VTABLE_ITEM(c043, 0, 1)
PRINT_VTABLE_ITEM(c071, 0, 1)
  结果为:

The size of C071 is 4
The detail of C071 is 5c b4 45 00
c043 : objadr:0012FCD4 vpadr:0012FCD4 vtadr:0045B450 vtival(0):0041D4F1
c071 : objadr:0012FCC8 vpadr:0012FCC8 vtadr:0045B45C vtival(0):0041D811
c043 : objadr:0012FCD4 vpadr:0012FCD4 vtadr:0045B450 vtival(1):0041DFE1
c071 : objadr:0012FCC8 vpadr:0012FCC8 vtadr:0045B45C vtival(1):0041DFE1
  观察第1、2行的最后一列,即两个类的虚表的第一个条目,由于C071重写了foo1函数,所以这个值不一样。而第3、4行的最后一列为两个类的虚表的第二个条目,由于C071并没有重写它,所以这两个值是相同的。和我们之间的猜测是一致的。

  接下来我们看看多重继承。定义两个类,各含一个虚函数,及一个数据成员。再从这两个类派生一个空子类。

struct C041
{
 C041() : c_(0x01) {}
 virtual void foo() { c_ = 0x02; }
 char c_;
};
struct C042
{
 C042() : c_(0x02) {}
 virtual void foo2() {}
 char c_;
};
struct C051 : public C041, public C042
{};
  运行如下代码:

PRINT_SIZE_DETAIL(C041)
PRINT_SIZE_DETAIL(C042)
PRINT_SIZE_DETAIL(C051)
  结果为:

The size of C041 is 5
The detail of C041 is 64 b3 45 00 01
The size of C042 is 5
The detail of C042 is 68 b3 45 00 02
The size of C051 is 10
The detail of C051 is 6c b4 45 00 01 68 b4 45 00 02
  注意,首先我们观察C051的对象输出,发现它的大小为10字节,这说明它有两个虚表指针,从导出的内存数据我们可以推断,首先是一个虚表指针,然后是从C041继承的成员变量,值也是我们在C041的构造函数中赋的值0x01,然后又是一个虚表指针,再是从C042继承的成员变量,值为0x02。

  为了验证,我们再运行如下代码:

C041 c041;
C042 c042;
C051 c051;
PRINT_VTABLE_ITEM(c041, 0, 0)
PRINT_VTABLE_ITEM(c042, 0, 0)
PRINT_VTABLE_ITEM(c051, 0, 0)
PRINT_VTABLE_ITEM(c051, 5, 0)
  注意最后一行的第二个参数,5。它是从对象起始地址开始到虚表指针的偏移值(按字节计算),从上面的对象内存输出我们看到C041的大小为5字节,因此C051中第二个虚表指针的起始位置距对象地址的偏移为5字节。输出的结果为:

  (注:这个偏移值是通过观察而判断出来的,并不通用,而且它依赖于我们前面所说的编译器在生成代码时所用的结构成员对齐方式,我们将这个值设为1。如果设为其他值会影响对象的大小及这个偏移值。参见第一篇起始处的说明。下同。)

c041 : objadr:0012FB88 vpadr:0012FB88 vtadr:0045B364 vtival(0):0041DF1E
c042 : objadr:0012FB78 vpadr:0012FB78 vtadr:0045B368 vtival(0):0041D43D
c051 : objadr:0012FB64 vpadr:0012FB64 vtadr:0045B46C vtival(0):0041DF1E
c051 : objadr:0012FB64 vpadr:0012FB69 vtadr:0045B468 vtival(0):0041D43D
  这下我们可以看到C051的两个虚表指针指向两个不现的虚表(第3、4行的vtadr列),而虚表中的条目的值分别等于C041和C042(即它的两个父类)的虚表条目的值(第1、3行和2、4行的vtival列的值相同)。

为什么子类要有两个虚表,而不是将它们合并为一个。主要是在处理类型的动态转换时这种对象布局更方便调整指针,后面我们看到这样的例子。

  如果子类重写父类的虚函数会怎么样?前面的类C071我们已经看到过一次了。我们再定义一个从C041和C042派生的类C082,并重写这两个父类中的虚函数,同时再增加一个虚函数。

struct C041
{
 C041() : c_(0x01) {}
 virtual void foo() { c_ = 0x02; }
 char c_;
};
struct C042
{
 C042() : c_(0x02) {}
 virtual void foo2() {}
 char c_;
};
struct C082 : public C041, public C042
{
 C082() : c_(0x03) {}
 virtual void foo() {}
 virtual void foo2() {}
 virtual void foo3() {}
 char c_;
};
  运行和上面类似的代码:

PRINT_SIZE_DETAIL(C082)
C041 c041;
C042 c042;
C082 c082;
PRINT_VTABLE_ITEM(c041, 0, 0)
PRINT_VTABLE_ITEM(c042, 0, 0)
PRINT_VTABLE_ITEM(c082, 0, 0)
PRINT_VTABLE_ITEM(c082, 5, 0)
  结果为:

The size of C082 is 11
The detail of C082 is 70 b3 45 00 01 6c b3 45 00 02 03
c041 : objadr:0012FA74 vpadr:0012FA74 vtadr:0045B364 vtival(0):0041DF1E
c042 : objadr:0012FA64 vpadr:0012FA64 vtadr:0045B368 vtival(0):0041D43D
c082 : objadr:0012FA50 vpadr:0012FA50 vtadr:0045B370 vtival(0):0041D87A
c082 : objadr:0012FA50 vpadr:0012FA55 vtadr:0045B36C vtival(0):0041D483
  果然C082的两个虚表中的条目值都和父类的不一样了(vtival列),指向了重写后的新函数地址。观察C082的大小和对象内存,我们可以知道它并没有为新定义的虚函数foo3生成新的虚表。那么foo3的函数地址到底是加到了类的第一个虚表,还是第二个虚表中?在调试状态下,我们在“局部变量”窗口中展开c082对象。我们可以看到两个虚表及其中的条目,但两个虚表都只能看到第一个条目。这应该是VC7.1IDE的一个小BUG。看来我们只有另想办法来验证。我们先把两个虚表中的第二个条目位置上的值打印出来。运行如下代码。

PRINT_VTABLE_ITEM(c082, 0, 1)
PRINT_VTABLE_ITEM(c082, 5, 1)
  结果如下:

c082 : objadr:0012FA50 vpadr:0012FA50 vtadr:0045B370 vtival(1):0041D32F
c082 : objadr:0012FA50 vpadr:0012FA55 vtadr:0045B36C vtival(1):0041D87A
  然后我们调用一下foo3函数:

c082.foo3();
  查看它的汇编代码:

004225F3 lea ecx,[ebp+FFFFFB74h]
004225F9 call 0041D32F
  第2条call指令后的地址就是foo3的函数地址了(实际上是一个跳转指令),对照前面的输出我们就可以知道,子类新定义的虚函数对应的虚表条目加入到了子类的第一个虚表中,并位于继承自父类的虚表条目之后。

C++对象布局及多态实现之动态和强制转换

转自:http://dev.yesky.com/145/2206145.shtml

为了验证前面提到过的类型动态转换(即dynamic_cast转换),以及对象类型的强制转换。我们利用前面定义的C041、C042及C082类来进行验证。

  运行下列代码:

c082.C041::c_ = 0x05;
PRINT_VTABLE_ITEM(c041, 0, 0)
PRINT_DETAIL(C041, ((C041)c082))
PRINT_VTABLE_ITEM(((C041)c082), 0, 0)
PRINT_VTABLE_ITEM(c082, 5, 0)
C042 * pt = dynamic_cast<C042*>(&c082);
PRINT_VTABLE_ITEM(*pt, 0, 0)
  第2行和第5行是为了对照方便而打印原对象中的信息。第3、4行把C082对象类型进行强制转换并分别打印转换后的对象内存信息及虚表信息。第6行我们用dynamic_cast进行了一次动态类型转换,从子类的指针转型为右父类的指针,再把指针指向的对象的信息打印出来。

  结果为:

c041 : objadr:0012FA74 vpadr:0012FA74 vtadr:0045B364 vtival(0):0041DF1E
The detail of C041 is 64 b3 45 00 05
((C041)c082) : objadr:0012F2A3 vpadr:0012F2A3 vtadr:0045B364 vtival(0):0041DF1E
c082 : objadr:0012FA50 vpadr:0012FA55 vtadr:0045B36C vtival(0):0041D483
*pt : objadr:0012FA55 vpadr:0012FA55 vtadr:0045B36C vtival(0):0041D483
  首先我们比较最后两行,从objadr列我们可以知道pt指向的并不是c082对象的起始地址,而是指向了c082的第2个虚表指针的所在地址(因为最后一行的objadr值等于倒数第2行的vpadr的值)。倒数第二行的vpadr值是c082对象的第2个虚表指针(我们在输出时指定了偏移值5)。而这个地址正是c082对象中属于从C042类继承而来的部分,即在进行动态类型转换时,除了改变类型信息,编译器还调整了指针的位置,以确保转换语义的正确性。所以我们可以知道,对指向有复杂继承结构的类对象的指针进行类型转换(一般在继承树中向上或向下转换)时,必须使用dynamic_cast,它会正确的处理指针位置的调整,如果转换是非法的,它会返回一个NULL指针。使用dynamic_cast时记得要做这个检查,文中为了简略把这些检查省去了。这种检查可以通过宏来定义,以便于在release版中去掉,提高效率。

  再将((C041)c082)和c082两行的输出进行对照,可以发现对对象进行向上的类型强制转换实际上编译器生成了一个新的临时对象,因为它们的objadr列不一样了,这表明它们已经不是同一个对象。再观察 c041、((C041)c082)及c082三行的vtadr和vtival(0),前两行相比是一样的,而后两行相比就不一样了。这也说明编译器在处理强制转换时,实际上是new了一个新的C041对象出来。因为对象的强制类型转换不象指针的动态类型转换,指针的动态类型转换同时要确保多态的语义,所以只需要调整指针位置。而对象强制类型转换,还要调整虚表中的条目值,因为对象类型转换不需要多态的行为。c082类的第一个虚表的第一个条目中存放的是 C082::foo()函数的地址,做了对象类型转换后,应该调整为C041::foo()才对,做这种调整过于复杂,所以编译器干脆new了一个新的 C041的临时对象出来。对比这三行的最后二列即知。我不知道这是否是C++标准规范中定义的行为,改天查到我再更新上来。

  在new出新对象的同时,编译器还将原对象中属于父类部分的数据成员的值拷贝了过来。注意代码的第1行,c082.C041::c_ = 0x05;,我们先把c082对象中从C041类继承过来的数据成员的值改写为0x05,原来是的值是0x01,由C041的构造函数初始化。我们观察输出的第2行,上面说了这个被打印的对象并非c082而是编译器new出的来的临时对象,可以注意到对象的最后一字节为0x05,即数据成员的值。所以我们知道编译器除了new出新的临时对象外,还把原对象中相应的数据成员的值也复制了过来。

  这和我以前的认识有点偏差,直观上我一直以为这种转换不会产生新的对象,不过仔细想想编译器的这种作法也是对的,如果不产生新的对象,就意味着它要象前述的那样动态的改变虚表中条目的值。但new出临时对象,也意味着使用下列的语句调用,可能产生意想不到的结果。

((C041)c082).somefun();
  如果somefun函数会改变对象的状态,那么上边的代码执行后,c082的状态并不会被改变。因为somefun实际改变的是临时对象,在执行完后该临时对象就扔掉了。这和直观的认识有所差异,一般会认为这个调用会作用于c082对象上。为了验证我们声明以下两个类。

struct C010
{
C010() : c_(0x01) {}
void foo() { c_ = 0x02; }
char c_;
};
struct C013 : public C010
{
C013() : c1_(0x01) {}
void foo() { c1_ = 0x02; }
char c1_;
};
  两个类为继承关系,各有一个同名的普通成员函数,该函数改写类的相应成员变量。我们做以下的调用:

C013 obj;
obj.foo();
((C010)obj).foo();
  第1个foo调用,改变的是c1_值,最后一行的调用改变的是c_的值。直观上容易认为上述代码执行后obj.c_和obj.c1_的值均为0x02。但我们在调试环境的局部变量窗口中把obj对象展开可以发现obj.c1_为0x02,但obj.c_为0x01。原因就是前面所说的((C010) obj)实际产生了一个临时对象,所以最后一行的调用没有作用到obj对象上。

  更进一步的想想,如果我们在一个类上运用了单件 (singleton)模式,而这个类又有一个继承结构,当在程序中想利用对对象进行向上转型来调用父类的方法时,应该会出现编译时错误,因为父类临时对象无法构造。在这里有个前提,父类的构造函数应该用protected进行保护,而不是用private,否则子类根本无法构造。这种我没有验证了,因为用这种方法调用实在是比较蠢的作法,但不排除这种可能性。向上例中最后一行正确的调用方法应该是:

obj.C010::foo();
  这样就可以调用到父类中被覆盖的函数,而且也是作用在正确的对象上。

C++对象布局及多态实现之成员函数的调用

转自:http://dev.yesky.com/450/2210450.shtml

从这部分开始我们除了利用内存的信息打印来进行探索外,更多的会通过跟踪和观察编译器产生的汇编代码来理解编译器对这些语言特性的实现方式。汇编方面知识的讨论超出了本文的范围,我只对和我们讨论相关的汇编代码进行解析。理解本文要讨论的知识并不需要有很完整的汇编知识,但必须了解起码的概念。

  下面我们看看引入虚继承后的影响。为了有所对比我们首先看看普通成员函数的调用情况。

  执行如下代码,它包括了对象的普通成员函数调用,类的静态成员函数调用、通过指针调用普通成员函数:

C010 obj;
PRINT_OBJ_ADR(obj)
obj.foo();
C012::sfoo();
C010 * pt = &obj;
pt->foo();

  结果如下:

obj's address is : 0012F843

  这是obj对象的内存地址。

  首先我们看看对象的普通成员函数调用,obj.foo();,对应的汇编代码为:

00422E09 lea ecx,[ebp+FFFFF967h]
00422E0F call 0041E289

  第1行把对象的地址存入ecx寄存器,执行完这行指令后,我们要以看到ecx中的值为0x0012F843,就是前面打印出的值。如果函数需要传递参数,我们还会在前面看到一些push指令。在第2行我们可以看到call的是一个直接的地址,这也就是静态绑定。即函数的调用地址在编译时已经被编译器决议。

  跟踪进去我们要以看到是一条跳转指令,继续执行可以看到真正的函数代码部分,如下(注:为了讨论方便我在第行前面加了一个行号):

01 00425FE0 push ebp
02 00425FE1 mov ebp,esp
03 00425FE3 sub esp,0CCh
04 00425FE9 push ebx
05 00425FEA push esi
06 00425FEB push edi
07 00425FEC push ecx
08 00425FED lea edi,[ebp+FFFFFF34h]
09 00425FF3 mov ecx,33h
10 00425FF8 mov eax,0CCCCCCCCh
11 00425FFD rep stos dword ptr [edi]
12 00425FFF pop ecx
13 00426000 mov dword ptr [ebp-8],ecx
14 00426003 mov eax,dword ptr [ebp-8]
15 00426006 mov byte ptr [eax],2
16 00426009 pop edi
17 0042600A pop esi
18 0042600B pop ebx
19 0042600C mov esp,ebp
20 0042600E pop ebp
21 0042600F ret

  我们看看第7行,把ecx寄存器入栈,后面4行初始化了函数的堆栈中的保存局部变量的部分。第12行弹出ecx值,到这里时ecx的值保持为在函数调用前存入的对象内存地址,第13行就是保存this指针的值,作为一个局部变量。这样我们就知道了VC7.1不是象传递普通函数那样通过压栈来传递this 指针,而是通过ecx寄存器来传递。第14、15行利用这个this指针给对象的成员变量进行了赋值。

  再看看静态成员函数调用的汇编代码:

00422E14 call 0041DD84

  非常直接,因为它不需要处理this指针,跟踪到函数的汇编代码,可以看到同样不需要处理this指针。具体的代码这里就不列出来了。

  再看看通过指针调用普通成员函数pt->foo();,产生的汇编代码如下:

00422E25 mov ecx,dword ptr [ebp+FFFFF958h]
00422E2B call 0041E289

  和通过对象调用普通成员函数的代码差不多。不过存对象地址到ecx寄存器地,是通过解引用pt指针来找到对象地址的。

C++对象布局及多态实现探索之虚函数调用

转自:http://dev.yesky.com/448/2211448.shtml

我们再看看虚成员函数的调用。类C041中含有虚成员函数,它的定义如下:

struct C041
{
C041() : c_(0x01) {}
virtual void foo() { c_ = 0x02; }
char c_;
};
  执行如下代码:

C041 obj;
PRINT_DETAIL(C041, obj)
PRINT_VTABLE_ITEM(obj, 0, 0)
obj.foo();
C041 * pt = &obj;
pt->foo();
  结果如下:

The detail of C041 is 14 b3 45 00 01
obj : objadr:0012F824 vpadr:0012F824 vtadr:0045B314 vtival(0):0041DF1E
  我们打印出了C041的对象内存布局及它的虚表信息。

  先看看obj.foo();的汇编代码:

004230DF lea ecx,[ebp+FFFFF948h]
004230E5 call 0041DF1E
  和前一篇文章中看过的普通的成员函数调用产生的汇编代码一样。这说明了通过对象进行函数调用,即使被调用的函数是虚函数也是静态绑定,即在编译时决议出函数的地址。不会有多态的行为发生。

  我们跟踪进去看看函数的汇编代码。

01 004263F0 push ebp
02 004263F1 mov ebp,esp
03 004263F3 sub esp,0CCh
04 004263F9 push ebx
05 004263FA push esi
06 004263FB push edi
07 004263FC push ecx
08 004263FD lea edi,[ebp+FFFFFF34h]
09 00426403 mov ecx,33h
10 00426408 mov eax,0CCCCCCCCh
11 0042640D rep stos dword ptr [edi]
12 0042640F pop ecx
13 00426410 mov dword ptr [ebp-8],ecx
14 00426413 mov eax,dword ptr [ebp-8]
15 00426416 mov byte ptr [eax+4],2
16 0042641A pop edi
17 0042641B pop esi
18 0042641C pop ebx
19 0042641D mov esp,ebp
20 0042641F pop ebp
21 00426420 ret
  值得注意的是第14、15行。第14行把this指针的值移到eax寄存器中,第15行给类的第一个成员变量赋值,这时我们可以看到在取变量的地址时用的是[eax+4],即跳过了对象布局最前面的4字节的虚表指针。

  接下来我们看看通过指针进行的虚函数调用pt->foo();,产生的汇编代码如下:

01 004230F6 mov eax,dword ptr [ebp+FFFFF900h]
02 004230FC mov edx,dword ptr [eax]
03 004230FE mov esi,esp
04 00423100 mov ecx,dword ptr [ebp+FFFFF900h]
05 00423106 call dword ptr [edx]
  第1行把pt指向的地址移入eax寄存器,这样eax中就保存了对象的内存地址,同时也是类的虚表指针的地址。第2行取eax中指针指向的值(注意不是 eax的值)到edx寄存器中,实际上也就是虚表的地址。执行完这两条指令后,我们看看eax和edx中的值,果然和我们前面打印的obj的虚表信息中的 vpadr和vtadr的值是一样的,分别为0x0012F824和0x0045B314。第4行同样用ecx寄存器来保存并传递对象地址,即 this指针的值。第5行的call指令,我们可以看到目的地址不象通过对象来调用那样,是一个直接的函数地址。而是将edx中的值做为指针来进行间接调用。前面我们已经知道edx中存放的实际是虚表的地址,我们也知道虚表实际是一个指针数组。这样第5行的调用实际就是取到虚表中的第一个条目的值,即 C041::foo()函数的地址。如果被调用的虚函数对应的虚表条目的索引不是0,将会看到edx后加上一个索引号乘4后的偏移值。继承跟踪可以发现, ptr[edx]的值为0x0041DF1E,也和我们打印的vtival(0)的值相同。前面已经提到过,这个地址实际也不是真正的函数地址,是一个跳转指令,继续执行就到了真正的函数代码部分(即前面列出的代码)。

  我们在上面看到的这个过程,就是动态绑定的过程。因为我们是通过指针来调用虚成员函数,所以会产生动态绑定,即使指针的类型和对象的类型是一样的。为了保证多态的语义,编译器在产生call指令时,不象静态绑定时那样,是在编译时决议出一个确定的地址值。相反它是通过用发出调用的指针指向的对象中的虚指针,来迂回的找到对象所对应类型的虚表,及虚表中相应条目中存放的函数地址。这样具体调用哪个函数就与指针的类型是无关的,只与具体的对象相关,因为虚指针是存放在具体的对象中,而虚表只和对象的类型相关。这也就是多态会发生的原因。

  请回忆一下前面讨论过的C071类,当子类重写从父类继承的虚函数时,子类的虚表内容的变化,及和父类虚表内容的区别(请参照第二篇中打印的子类和父类的虚表信息)。具体的通过指向子类对象的父类指针来调用被子类重写过的虚函数时的调用过程,请有兴趣的朋友自己调试一下,这里不再列出。

  另外前面在《C++对象布局及多态实现之动态和强制转换》中我们讨论了指针的类型动态转换。我们在这里再利用C041、C042及C051类,来看看指针的类型动态转换。这几个类的定义请参见前文。类C051从C041和C042多重继承而来,且后两个类都有虚函数。执行如下代码:

C051 obj;
C041 * pt1 = dynamic_cast<C041*>(&obj);
C042 * pt2 = dynamic_cast<C042*>(&obj);
pt1->foo();
pt2->foo2();
  第一个动态转型对应的汇编代码为:

00404B59 lea eax,[ebp+FFFFF8ECh]
00404B5F mov dword ptr [ebp+FFFFF8E0h],eax
  因为不需要调整指针位置,所以很直接,取出对象的地址后直接赋给了指针。

  第二个动态转型牵涉到了指针位置的调整,我们来看看它的汇编代码:

01 00404B65 lea eax,[ebp+FFFFF8ECh]
02 00404B6B test eax,eax
03 00404B6D je 00404B7D
04 00404B6F lea ecx,[ebp+FFFFF8F1h]
05 00404B75 mov dword ptr [ebp+FFFFF04Ch],ecx
06 00404B7B jmp 00404B87
07 00404B7D mov dword ptr [ebp+FFFFF04Ch],0
08 00404B87 mov edx,dword ptr [ebp+FFFFF04Ch]
09 00404B8D mov dword ptr [ebp+FFFFF8D4h],edx
  代码要复杂的多。&obj运算后得到的是一个指针,前三行指令就是判断这个指针是否为NULL。奇怪的是第4行并没有根据eax中的地址(即对象的起始地址)来进行指针的位置调整,而是直接把[ebp+FFFFF8F1h]的地址取到ecx寄存器中。第1行指令中的[ebp+ FFFFF8ECh]实际是得到对象的地址,ebp所加的那个数实际是个负数(补码)也就是对象的偏移地址。对比两个数发现相差5字节,这样实际上第4行是直接得到了指针调整后的地址,即将指针指向了对象中的属于C042的部分。后面的代码又通过一个临时变量及edx寄存器把调整后的指针值最终存到了 pt2指针中。

  这些代码实际可以优化成二行:

lea eax, [ebp+FFFFF8F1h]
mov dword ptr [ebp+FFFFF8d4h], eax
  我们曾提到C051类有两个虚表,相应对象中有也两个虚表指针,之所以不合并为一个,就是为了处理指针的类型动态转换。结合前面对于多态的讨论,我们就可以理解得更清楚了。pt2->foo2();调用时,对象的类型还是C051,但经过指针动态转换pt2指向了对象中属于C042的部分的起始,也就是第二个虚表指针。这样在进行函数调用时就不需要再做额外的处理了。我们看看pt1->foo();及pt2->foo2 ();产生的汇编码即知。

01 00404B93 mov eax,dword ptr [ebp+FFFFF8E0h]
02 00404B99 mov edx,dword ptr [eax]
03 00404B9B mov esi,esp
04 00404B9D mov ecx,dword ptr [ebp+FFFFF8E0h]
05 00404BA3 call dword ptr [edx]
06 00404BA5 cmp esi,esp
07 00404BA7 call 0041DDDE
08 00404BAC mov eax,dword ptr [ebp+FFFFF8D4h]
09 00404BB2 mov edx,dword ptr [eax]
10 00404BB4 mov esi,esp
11 00404BB6 mov ecx,dword ptr [ebp+FFFFF8D4h]
12 00404BBC call dword ptr [edx]
13 00404BBE cmp esi,esp
14 00404BC0 call 0041DDDE
  前7行为pt1->foo();,后7行为pt2->foo2();。唯一不同的是指针指向的地址不同,调用机制是一样的。

C++对象布局及多态之虚成员函数调用

转自:http://dev.yesky.com/237/2218237.shtml

在构造函数中调用虚成员函数,虽然这是个不很常用的技术,但研究一下可以加深对虚函数机制及对象构造过程的理解。这个问题也和一般直观上的认识有所差异。先看看下面的两个类定义。

struct C180
{
 C180() {
  foo();
  this->foo();
 }
 virtual foo() {
  cout << "<< C180.foo this: " << this << " vtadr: " << *(void**)this << endl;
 }
};
struct C190 : public C180
{
 C190() {}
 virtual foo() {
  cout << "<< C190.foo this: " << this << " vtadr: " << *(void**)this << endl;
 }
};
  父类中有一个虚函数,并且父类在它的构造函数中调用了这个虚函数,调用时它采用了两种方法一种是直接调用,一种是通过this指针调用。同时子类又重写了这个虚函数。

  我们可以来预测一下如果构造一个C190的对象会发生什么情况。

  我们知道,在构造一个对象时,首先会按对象的大小得到一块内存(在heap上或在stack上),然后会把指向这块内存的指针做为this指针来调用类的构造函数,对这块内存进行初始化。如果对象有父类就会先调用父类的构造函数(并依次递归),如果有多个父类(多重继承)会依次对父类的构造函数进行调用,并会适当的调整this指针的位置。在调用完所有的父类的构造函数后,再执行自己的代码。

  照上面的分析构造C190时也会调用C180的构造函数,这时在C180构造函数中的第一个foo调用为静态绑定,会调用到C180::foo()函数。第二个foo调用是通过指针调用的,这时多态行为会发生,应该调用的是C190::foo()函数。

  执行如下代码:

C190 obj;
obj.foo();
  结果为:

<< C180.foo this: 0012F7A4 vtadr: 0045C404
<< C180.foo this: 0012F7A4 vtadr: 0045C404
<< C190.foo this: 0012F7A4 vtadr: 0045C400
  和我们的分析大相径庭。前2行是构造C190时的输出,后1行是我们用静态绑定方式调用的C190::foo()函数。第2行的输出说明多态行为并没有象预期的那样发生。而且比较输出的最后一列,发现在调用C180的构造函数时对象对应的虚表和构造后对象对应的虚表不是同一个。其实这正是奥秘的所在。

  为此我查了一下C++标准规范。在12.7.3条中有明确的规定。这是一种特例,在这种情况下,即在构造子类时调用父类的构造函数,而父类的构造函数中又调用了虚成员函数,这个虚成员函数即使被子类重写,也不允许发生多态的行为。即,这时必须要调用父类的虚函数,而不子类重写后的虚函数。

  我想这样做的原因是因为在调用父类的构造函数时,对象中属于子类部分的成员变量是肯定还没有初始化的,因为子类构造函数中的代码还没有被执行。如果这时允许多态的行为,即通过父类的构造函数调用到了子类的虚函数,而这个虚函数要访问属于子类的数据成员时就有可能出错。

  我们看看VC7.1生成的汇编代码就可以很容易的理解这个行为了。

  这是C190的构造函数:

01 00426FE0 push ebp
02 00426FE1 mov ebp,esp
03 00426FE3 sub esp,0CCh
04 00426FE9 push ebx
05 00426FEA push esi
06 00426FEB push edi
07 00426FEC push ecx
08 00426FED lea edi,[ebp+FFFFFF34h]
09 00426FF3 mov ecx,33h
10 00426FF8 mov eax,0CCCCCCCCh
11 00426FFD rep stos dword ptr [edi]
12 00426FFF pop ecx
13 00427000 mov dword ptr [ebp-8],ecx
14 00427003 mov ecx,dword ptr [ebp-8]
15 00427006 call 0041D451
16 0042700B mov eax,dword ptr [ebp-8]
17 0042700E mov dword ptr [eax],45C400h
18 00427014 mov eax,dword ptr [ebp-8]
19 00427017 pop edi
20 00427018 pop esi
21 00427019 pop ebx
22 0042701A add esp,0CCh
23 00427020 cmp ebp,esp
24 00427022 call 0041DDF2
25 00427027 mov esp,ebp
26 00427029 pop ebp
27 0042702A ret
  开始部分的指令在前面几篇中陆续解释过,这里不再详述。我们看看第15是对父类的构造函数C180::C180()的调用,根据前文的说明,我们知道此时ecx中放的是this指针,也就是C190对象的地址。这时如果跳到this指针批向的地址看看会发现值为0xcccccccc即没有初始化,虚表指针也没有被初始化。那么我们跟着跳到C180的构造函数看看。

01 00427040 push ebp
02 00427041 mov ebp,esp
03 00427043 sub esp,0CCh
04 00427049 push ebx
05 0042704A push esi
06 0042704B push edi
07 0042704C push ecx
08 0042704D lea edi,[ebp+FFFFFF34h]
09 00427053 mov ecx,33h
10 00427058 mov eax,0CCCCCCCCh
11 0042705D rep stos dword ptr [edi]
12 0042705F pop ecx
13 00427060 mov dword ptr [ebp-8],ecx
14 00427063 mov eax,dword ptr [ebp-8]
15 00427066 mov dword ptr [eax],45C404h
16 0042706C mov ecx,dword ptr [ebp-8]
17 0042706F call 0041DA8C
18 00427074 mov ecx,dword ptr [ebp-8]
19 00427077 call 0041DA8C
20 0042707C mov eax,dword ptr [ebp-8]
21 0042707F pop edi
22 00427080 pop esi
23 00427081 pop ebx
24 00427082 add esp,0CCh
25 00427088 cmp ebp,esp
26 0042708A call 0041DDF2
27 0042708F mov esp,ebp
28 00427091 pop ebp
29 00427092 ret
  看看第15行,在this指针的位置也就是对象的起始处,填入了一个4字节的值0x0045C404,其实这就是我们前面的打印过的C180的虚表地址。第16、17行和18、19行分别调用了两次foo()函数,用的都是静态绑定。这个就有点奇怪,因为对后一个调用我们使用了this指针,照理应该是动态绑定才对。可这里却是静态绑定,为什么编译器要做这个优化?我们继承往后看。

  这个函数执行完后,我们再回到C190构造函数中,我们接着看C190构造函数汇编代码的第17行,这里又在对象的起始处重新填入了0x0045C400,覆盖了原来的值,而这个值就是我们前面打印过的真正的C190的虚表地址。

  也就是说VC7.1是通过在调用构造函数的真正代码前把对象的虚指针值设置为指向对应类的虚表来实现C++规范的相应语义。C++标准中只规定了行为,并不规定具体编译器在实现这一行为时所用的方法。象我们上面看到的,即使是通过this指针调用,编译器也把它优化为静态绑定,也就是说即使不做这个虚指针的调整也不会有错。之所以要调整我想可能是防止在被调用的虚成员中又通过this指针来调用其他的虚函数,不过谁会这么变态呢?

  还有值得一提的是,VC7.1中有一个扩展属性可以用来抑制编译器产生对虚指针进行调整的代码。我们可以在C180类的声明中加入这个属性。

struct __declspec(novtable) C180
{
 C180() {
  foo();
  this->foo();
 }
 virtual foo() {
  cout << "<< C180.foo this: " << this << " vtadr: " << *(void**)this << endl;
 }
};
  这样再执行前面的代码,输出就会变成:

<< C180.foo this: 0012F7A4 vtadr: CCCCCCCC
<< C180.foo this: 0012F7A4 vtadr: CCCCCCCC
<< C190.foo this: 0012F7A4 vtadr: 0045C400
  由于编译器抑制了对虚指针的调整所以在调C180的构造函数时虚指针的值没有初始化,这时我们才看到多亏编译器把第二个通过this指针对foo的调用优化成了静态绑定,否则由于虚指针没有初始化一定会出现一个指针异常的错误,这就回答我们上面的那个问题。

  在这种情况下产生的汇编代码我就不列了,有兴趣的朋友可以自己去看一看。另外对于析构函数的调用,也请有兴趣的朋友自行分析一下。

  另外这个属性在ATL的代码中大量的使用。在ATL中接口一般为纯虚基类,如果不用这个优化属性,由于在子类即实现类的构造函数中要调用父类的构造函数,而编译器产生的父类构造函数又要设置虚指针的值。所以编译器必须要把父类的虚表构建出来。而实际上这个虚表是没有任何意义的,因为ATL的纯虚接口类的虚函数都是无实现的。这样不仅仅是多了几行无用的设值指令,同时也浪费了空间。有兴趣的朋友可以自行验证一下。

C++对象布局及多态实现探索之虚继承

转自:http://dev.yesky.com/212/2219712.shtml

下面我们来看虚继承。首先看看这C020类,它从C010虚继承:}

struct C010
{
 C010() : c_(0x01) {}
 void foo() { c_ = 0x02; }
 char c_;
};
struct C020 : public virtual C010
{
 C020() : c_(0x02) {}
 char c_;
};
  运行如下代码,查看对象的内存布局:

PRINT_SIZE_DETAIL(C020)
  结果为:

The size of C020 is 6
The detail of C020 is c0 c2 45 00 02 01
  很明显对象的起始处是一个指针,然后是子类的成员变量,接下来是父类的成员变量。和以前的讨论不同的是由于使用了虚继承,父类的成员变量被放到了最后面。

  运行如下的代码:

C020 c020;
c020.C010::c_ = 0x04;
  由于子类中的变量和父类中的变量重名,所以我们必须用这种方式来访问属于父类的成员变量,普通情况下不需要这种写法。我们看看后面这行代码对应的汇编代码:

0042387E mov eax,dword ptr [ebp+FFFFF82Ch]
00423884 mov ecx,dword ptr [eax+4]
00423887 mov byte ptr [ebp+ecx+FFFFF82Ch],4
  前面说过对象的起始是一个指针,第1行指令取到这个指针的值,第2行把这个指针指向的地址后移4字节后的值(做为一个4字节的值)取出来。执行完这句我们看看ecx寄存器,可知取出来的值为5。最后一行是真正的赋值指令,它通过在对象的起始处(即[ebp+FFFFF32Ch])加上ecx中的值做偏移值(即5)来得到赋值的目的地址。接合前面的对象布局输出,我们可以发现从对象起始地址开始加5字节的偏移值,刚好得到父类的成员变量的地址。这样我们可以大致分析出直接虚继承的子类的对象布局。

|子类5            |父类1    |
|偏移值指针4,5|子类成员变量1|父类成员变量1|

  (注:第一个数字为所在区域的长度(字节数),偏移值指针后的第二个数字为该指针指向的偏移值。后同。)

  通过查看内存可以发现偏移值指针指向的内存前4字节为0,我不知道它的具体的用途是什么。接下来的4字节是一个32位的整数,也就是真正的偏移值。即从子类的起始位置到被虚继承的父类的起始位置的偏移值,在我们前面的例子中这个值为5(一个指针加一个char成员变量)。

  通过这个分析我们可以看到在虚承继的情况下,通过子类的对象访问父类的普通成员变量的效率是相当低的。如果必须用到虚继承,也应该尽量不要在父类中放置普通成员变量(静态成员变量不受影响)。

  另外为什么微软不把偏移值直接放到子类中,而是采用偏移值指针。我想是因为采用指针的方式更为灵活,即使以后需要扩展也不影响类对象的布局。
  按下来我们再看看这几行代码:

PRINT_OBJ_ADR(c020);
C010 * pt = &c020;
PRINT_PT(pt);
pt->c_ = 0x03;
  第2行声明了一个父类指针,并让它指向一个子类的对象。第3行打印出这个指针的值。运行结果为:

c020's address is : 0012F708
pt's value is : 0012F70D
  我们可以看到赋值后的指针的值并不等于赋给它的对象地址值。也就是说在这个赋值过程中编译器进行了额外的工作,即调整了指针的值。我们看看第2行对应的汇编代码,看看编译器究竟做了些什么?

01 004238EA lea eax,[ebp+FFFFF82Ch]
02 004238F0 test eax,eax
03 004238F2 jne 00423900
04 004238F4 mov dword ptr [ebp+FFFFF014h],0
05 004238FE jmp 00423916
06 00423900 mov ecx,dword ptr [ebp+FFFFF82Ch]
07 00423906 mov edx,dword ptr [ecx+4]
08 00423909 lea eax,[ebp+edx+FFFFF82Ch]
09 00423910 mov dword ptr [ebp+FFFFF014h],eax
10 00423916 mov ecx,dword ptr [ebp+FFFFF014h]
11 0042391C mov dword ptr [ebp+FFFFF820h],ecx
  喔!比想象的要复杂的多。一行简单的指针赋值语句却产生了这么多的汇编代码。这行代码本身的语义是取对象的地址赋给一个指针,对于编译器来说它把这做为指针到指针的赋值来处理。由于牵涉到了向上的类型转换,同时又有虚继承存在。根据前面的布局分析,在虚继承的情况下,父类位于对象布局的后部。因此在这里要做一个指针位置的调整。由于调整要根据源指针来进行计算,所以先要对源指针的合法性进行检查,以避免运行时的指针异常错误。前3行的汇编指令就是在做这件事,检查源指针是否为NULL。如果为NULL则执行4、5、10、11行,最终给pt赋0。如果不为NULL跳至第6行执行到最后。重要的是第6、7、8行代码,它们通过偏移值指针找到偏移值,并以此来调整指针的位置,让目的指针最终指向对象中的父类部分的数据成员。

  对比一下普通的指针赋值,我们可以对上面赋值的复杂性和低效有更深的认识。

C010 * pt1 = NULL;
C010 * pt2 = pt1;
  这两行相应的汇编代码为:

0042397D mov dword ptr [ebp+FFFFF814h],0
00423987 mov eax,dword ptr [ebp+FFFFF814h]
0042398D mov dword ptr [ebp+FFFFF808h],eax
  第1行是普通的赋值,编译器并不做任何的检查,即使源指针为NULL。因为它不需要根据源指针(本处为NULL)做任何计算。第2个赋值也很直接,只是通过eax做了一个中转。这里我们就可以看到前面的虚继承下的子类指针到父类指针的赋值是我么的低效。在程序中应尽量的避免这种代码。

C++对象布局及多态探索之菱形结构虚继承

转自:http://dev.yesky.com/142/2229142.shtml

这次我们看看菱形结构的虚继承。虚继承的引入本就是为了解决复杂结构的继承体系问题。上一篇我们在讨论虚继承时用的是一个简单的继承结构,只是为了打个铺垫。

  我们先看看这几个类,这是一个典型的菱形继承结构。C100和C101通过虚继承共享同一个父类C041。C110则从C100和C101多重继承而来。

struct C041
{
 C041() : c_(0x01) {}
 virtual void foo() { c_ = 0x02; }
 char c_;
};
struct C100 : public virtual C041
{
 C100() : c_(0x02) {}
 char c_;
};
struct C101 : public virtual C041
{
 C101() : c_(0x03) {}
 char c_;
};
struct C110 : public C100, public C101
{
 C110() : c_(0x04) {}
 char c_;
};
  运行如下代码:

PRINT_SIZE_DETAIL(C110)
  结果为:

The size of C110 is 16
The detail of C110 is 28 c3 45 00 02 1c c3 45 00 03 04 18 c3 45 00 01
  我们可以象上一篇一样,画出对象的内存布局。

|C100,5 |C101,5 |C110,1 |C041,5 |
|ospt,4,11 |m,1 |ospt,4,6 |m,1 |m,1 |vtpt,4 |m1 |
  (注:为了不折行,我用了缩写。ospt代表偏移值指针、m代表成员变量、vtpt代表虚表指针。第一个数字是该区域的大小,即字节数。只有偏移值指针有第二个数字,第二个数字就是偏移值指针指向的偏移值的大小。)

  可以看到对象的内存布局中只有一个C041,即祖父类的部分只有一份,且放在最后面。这就是菱形继承。对比前面几篇的讨论,我们可以知道,如果没有用虚继承机制,那么在C041对象的内存布局中会出现两份C041部分,这也就是所谓的V型继承。相应的对象布局为:C041+C100+C041+C101 +C110。在V型继承中是不能直接从C110,即孙子类,直接转型到C041,即祖父类的。因为在对象的布局中有两份祖父类的实体,一份从C100而来,一份从C101而来。编译器在决议时会存在二义性,它不知道转型后到底用哪一份实体。虽然可以通过先转型到某一父类,然后再转型到祖父类来解决。但使用这种方法时,如果改写了祖父类的成员变量的内容,runtime是不会同步两个祖父类实体的状态,因此可能会有语义错误。

  我们再分析一下上面的内存布局。普通继承的布局,顶层类在前面。多重继承时则按从左到右的顺序排。从C100和C101到C110的继承是普通继承,所以遵循这个原则,先是左父类再右父类,接下去是子类。而虚继承则要求将共享的父类放到整个对象布局的最后(即使虚父类没有被真正的共享也是如此,前在一篇的C020类就是这样。不知道打开优化开关后会不会有变化。)所以在上例中的祖父类也是被置于最后的。

  我们再看看对成员的访问情况。运行以下代码并查看相应的汇编代码。

C110 c110;
c110.c_ = 0x51;
c110.C100::c_ = 0x52;
c110.C101::c_ = 0x52;
c110.C041::c_ = 0x53;
c110.foo();
  对应的汇编代码为:

01 00423993 push 1
02 00423995 lea ecx,[ebp+FFFFF7F0h]
03 0042399B call 0041DE60
04 004239A0 mov byte ptr [ebp+FFFFF7FAh],51h
05 004239A7 mov byte ptr [ebp+FFFFF7F4h],52h
06 004239AE mov byte ptr [ebp+FFFFF7F9h],52h
07 004239B5 mov eax,dword ptr [ebp+FFFFF7F0h]
08 004239BB mov ecx,dword ptr [eax+4]
09 004239BE mov byte ptr [ebp+ecx+FFFFF7F4h],53h
10 004239C6 mov eax,dword ptr [ebp+FFFFF7F0h]
11 004239CC mov ecx,dword ptr [eax+4]
12 004239CF lea ecx,[ebp+ecx+FFFFF7F0h]
13 004239D6 call 0041DF32
  前3行是对象的初始化,调用了对象的构造函数。4、5、6行是对子类、左右父类的成员变量的赋值。我们可以看到是直接写的,因为这一层的继承是普通继承。第7、8、9行是对祖父类成员变量的赋值,和上篇讨论过的一样,是通过偏移值指针指向的偏移值来间接访问的。最后的4行指令是对成员函数的调用。我们可以看到调用的函数地址是直接给出的(最后一行),因为我们是通过对象来调用,即使是虚函数调用也不会有多态的行为。但是得到this指针的方式却是颇为间接,即第10、11、12行。因为这个函数在祖父类中定义,那么它操作的数据成员应该是祖父类的。因此编译器要调整this指针的位置。而祖父类又是被虚继承,因此要通过偏移值指针指向的偏移值来进行调整。

  再观察一下第9行和第12行,可以看到计算出来的地址值是不一样的。这是因为第9行为给祖父类的成员变量赋值,而祖父类中有虚表指针存在,所以在得到对象的起始地址后,编译器给它加了4字节的偏移量以跳过虚指针。实际的得到地址的运算为: [ebp+ecx+FFFFF7F0h+4h],编译器在生成代码时会直接把最后一步运算做掉。

  我们再看一个例子,这个例子的继承结构和上一篇中是一样的,也是菱形结构。不同的是,每一个类都重写了顶层类声明的虚函数。代码如下:

struct C041
{
 C041() : c_(0x01) {}
 virtual void foo() { c_ = 0x02; }
 char c_;
};
struct C140 : public virtual C041
{
 C140() : c_(0x02) {}
 virtual void foo() { c_ = 0x11; }
 char c_;
};
struct C141 : public virtual C041
{
 C141() : c_(0x03) {}
 virtual void foo() { c_ = 0x12; }
 char c_;
};
struct C150 : public C140, public C141
{
 C150() : c_(0x04) {}
 virtual void foo() { c_ = 0x21; }
 char c_;
};
  首先我们运行下面的代码,看看它们的内存布局。

PRINT_SIZE_DETAIL(C041)
PRINT_SIZE_DETAIL(C140)
PRINT_SIZE_DETAIL(C141)
PRINT_SIZE_DETAIL(C150)
  结果为:

The size of C041 is 5
The detail of C041 is f0 c2 45 00 01
The size of C140 is 14
The detail of C140 is 48 c3 45 00 02 00 00 00 00 44 c3 45 00 01
The size of C141 is 14
The detail of C141 is 58 c3 45 00 03 00 00 00 00 54 c3 45 00 01
The size of C150 is 20
The detail of C150 is 74 c3 45 00 02 68 c3 45 00 03 04 00 00 00 00 64 c3 45 00 01
  和前面的布局不同之处在于,共享部分和前面的非共享部分之间多了4字节的0值。只有共享部分有虚表指针,这是因为派生类都没有定义自己的虚函数,只是重写了顶层类的虚函数。我们分析一下C150的对象布局。

|C140,5 |C141,5 |C150,1 |zero,4 |C041,5 |
|ospt,4,15 |m,1 |ospt,4,10 |m,1 |m,1 |4 |vtpt,4 |m1 |
  (注:为了不折行,我用了缩写。ospt代表偏移值指针、m代表成员变量、vtpt代表虚表指针。第一个数字是该区域的大小,即字节数。只有偏移值指针有第二个数字,第二个数字就是偏移值指针指向的偏移值的大小。)

  再看函数的调用:

C150 obj;
PRINT_OBJ_ADR(obj)
obj.foo();
  输出的对象地址为:

obj's address is : 0012F624
  最后一行函数调用的代码对应的汇编代码为:

00423F74 lea ecx,[ebp+FFFFF757h]
00423F7A call 0041DCA3
  单步执行后,我们可以看到ecx中的值为:0x0012F633,这个地址也就是obj对象布局中的祖父类部分的起始地址。通过上面的布局分析我们知道 C150起始的偏移值指针指向的值为15,即对象起始到共享部分(祖父类部分)的偏移值。上面输出的obj起始地址为0x0012F624加上十进制的 15后,正好是我们看到的ecx中的值0x0012f633。

由于函数调用是作用于对象上,我们看到第二行的call指令是直接到地址的。

  在这里令人困惑的问题是,我们知道ecx是用来传递this指针的。在前一篇中,我们分析了在C110对象上的foo方法调用。在那个例子中,由于 foo是顶层类中定义的虚函数,并且没有被下面的派生类重写,因此通过子类对象调用这个方法时,编译器产生的代码是通过子类起始的偏移指针指向的偏移值来计算出祖父类部分的起始地址,并将这个地址做为this指针所指向的地址。但是在C150类中,foo不再是从祖父类继承的,而是被子类自己所重写。照理这时的this指针应该指向子类的起始地址,也就是0x0012F62E,而不是ecx中的值0x0012F633。

  我们跟进去看看C150::foo()的汇编代码,看它是怎样通过指向祖父类部分的this指针,来定位到子类的成员变量。

01 00426C00 push ebp
02 00426C01 mov ebp,esp
03 00426C03 sub esp,0CCh
04 00426C09 push ebx
05 00426C0A push esi
06 00426C0B push edi
07 00426C0C push ecx
08 00426C0D lea edi,[ebp+FFFFFF34h]
09 00426C13 mov ecx,33h
10 00426C18 mov eax,0CCCCCCCCh
11 00426C1D rep stos dword ptr [edi]
12 00426C1F pop ecx
13 00426C20 mov dword ptr [ebp-8],ecx
14 00426C23 mov eax,dword ptr [ebp-8]
15 00426C26 mov byte ptr [eax-5],21h
16 00426C2A pop edi
17 00426C2B pop esi
18 00426C2C pop ebx
19 00426C2D mov esp,ebp
20 00426C2F pop ebp
21 00426C30 ret
  果然,由于此时指针指向的不是子类的起始部分(而是祖父类的起始部分),因为是通过减于一个偏移值为向前定位成员变量的地址的。注意第15行,这时 eax中存放的是this指针的值,写入值的地址是[eax-5],结合前面的对象布局和对象的内存输出,我们可以知道this指针的值(此时指向祖父类 C041的起始部分)减去5个字节(4字节的0值和1字节的成员变量值)后,刚好是子类C150的起始地址。

  为什么不直接用子类的地址而是通过祖父类的起始地址间接的进行定位?这牵涉到编译内部的实现限制和对一系统问题的全面的理解。只是通过分析现象很难找到答案。

  我们再通过指针来调用一次。

C150 * pt = &obj;
pt->foo();
  第二行代码对应的汇编指令为:

01 00423F8B mov eax,dword ptr [ebp+FFFFF73Ch]
02 00423F91 mov ecx,dword ptr [eax]
03 00423F93 mov edx,dword ptr [ecx+4]
04 00423F96 mov eax,dword ptr [ebp+FFFFF73Ch]
05 00423F9C mov ecx,dword ptr [eax]
06 00423F9E mov eax,dword ptr [ebp+FFFFF73Ch]
07 00423FA4 add eax,dword ptr [ecx+4]
08 00423FA7 mov ecx,dword ptr [ebp+FFFFF73Ch]
09 00423FAD mov edx,dword ptr [ecx+edx]
10 00423FB0 mov esi,esp
11 00423FB2 mov ecx,eax
12 00423FB4 call dword ptr [edx]
13 00423FB6 cmp esi,esp
14 00423FB8 call 0041DDF2
  喔!更加迂回了。这段代码非常的低效,里面很多明显的冗余指令,如第1、4、6行,2、5行等,如果打开了优化开关可能这段指令的效率会好很多。

  第9行通过祖父类的虚表指针得到了函数地址,第11行同样把祖父类部分的起始地址0x0012F633做为this指针指向的地址存入ecx。

  最后我们做个指针的动态转型再调用一次:

C141 * pt1 = dynamic_cast<C141*>(pt);
pt1->foo();
  第1行代码对应的汇编指令如下:

01 00423FBD cmp dword ptr [ebp+FFFFF73Ch],0
02 00423FC4 je 00423FD7
03 00423FC6 mov eax,dword ptr [ebp+FFFFF73Ch]
04 00423FCC add eax,5
05 00423FCF mov dword ptr [ebp+FFFFF014h],eax
06 00423FD5 jmp 00423FE1
07 00423FD7 mov dword ptr [ebp+FFFFF014h],0
08 00423FE1 mov ecx,dword ptr [ebp+FFFFF014h]
09 00423FE7 mov dword ptr [ebp+FFFFF730h],ecx
  这里实际做了一个pt是否为零的判断,第4条指令把pt指向的地址后移了5字节,最后赋给了pt1。这样pt1就指向了右父类部分的地址位置,也就是C141的起始位置。

  第2行代码对应的汇编指令为:

01 00423FED mov eax,dword ptr [ebp+FFFFF730h]
02 00423FF3 mov ecx,dword ptr [eax]
03 00423FF5 mov edx,dword ptr [ecx+4]
04 00423FF8 mov eax,dword ptr [ebp+FFFFF730h]
05 00423FFE mov ecx,dword ptr [eax]
06 00424000 mov eax,dword ptr [ebp+FFFFF730h]
07 00424006 add eax,dword ptr [ecx+4]
08 00424009 mov ecx,dword ptr [ebp+FFFFF730h]
09 0042400F mov edx,dword ptr [ecx+edx]
10 00424012 mov esi,esp
11 00424014 mov ecx,eax
12 00424016 call dword ptr [edx]
13 00424018 cmp esi,esp
14 0042401A call 0041DDF2
  由于是通过偏移值指针进行运算,最后在调用时ecx和edx的值和前面通过pt指针调用时是一样的,这也是正确的多态行为。

/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

C++对象布局及多态探索之菱形结构虚继承

这次我们看看菱形结构的虚继承。虚继承的引入本就是为了解决复杂结构的继承体系问题。

C++对象布局及多态实现探索之虚继承

下面我们来看虚继承。

C++对象布局及多态之虚成员函数调用

在构造函数中调用虚成员函数,虽然这是个不很常用的技术,

C++对象布局及多态实现探索之虚函数调用

通过对象进行函数调用,即使被调用的函数是虚函数也是静态绑定,即在编译时决议出函数的地址。

C++对象布局及多态实现之成员函数的调用

从这部分开始我们除了利用内存的信息打印来进行探索外,

C++对象布局及多态实现之动态和强制转换

为了验证前面提到过的类型动态转换(即dynamic_cast转换),以及对象类型的强制转换

C++对象布局及多态实现之带虚函数的类

如果类中存在虚函数时,情况会怎样呢?我们知道当一个类中

C++对象布局及多态实现探索之内存布局

本文通过观察对象的内存布局,跟踪函数调用的汇编代码。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: