② 继承类对象:box和sphere(分别为盒子和球体)的存储格局分别如下面两张表所示。

box

offset

description

+0x0

int color

+0x4

int width

+0x8

int height

+0xC

int depth

sphere

offset

description

+0x0

int color

+0x4

int radius

下面分析一下函数主体main()。

指令清单51.11 MSVC 2008带参数/Ob0的优化

PUBLIC _main
_TEXT SEGMENT
_s$ = -24 ; size = 8
_b$ = -16 ; size = 16
_main PROC
    sub   esp, 24
    push  30
    push  20
    push  10
    push  1
    lea   ecx, DWORD PTR _b$[esp+40]
    call  ??0box@@QAE@HHHH@Z ; box::box
    push  40
    push  2
    lea   ecx, DWORD PTR _s$[esp+32]
    call  ??0sphere@@QAE@HH@Z ; sphere::sphere
    lea   ecx, DWORD PTR _b$[esp+24]
    call  ?print_color@object@@QAEXXZ ; object::print_color
    lea   ecx, DWORD PTR _s$[esp+24]
    call  ?print_color@object@@QAEXXZ ; object::print_color
    lea   ecx, DWORD PTR _b$[esp+24]
    call  ?dump@box@@QAEXXZ ; box::dump
    lea   ecx, DWORD PTR _s$[esp+24]
    call  ?dump@sphere@@QAEXXZ ; sphere::dump
    xor   eax, eax
    add   esp, 24
    ret   0
_main ENDP

继承类必须在其基(父)类字段的后面加入自己的字段,因此基类和继承类的类成员函数可以共存。

当程序调用类成员对象object::print_color()时,指向对象box和sphere的指针是通过this指针传递的。由于在所有继承类和基类中color字段的偏移量固定为0(offset+0x0),所有类对象的类成员函数object::print_color都可以正常运行。

因此,无论是基类还是继承类调用object::print_color(),只要该方法所引用的字段的相对地址固定不变,那么该方法就可以正常运行。

假如基于box类创建一个继承类,那么编译器就会在变量depth的后面追加您所添加新的变量,以确保基类box的各字段的相对地址在其继承类中固定不变。

因此,当父类为box类的各继承类在调用各自的方法box::dump()时,它们都能检索到color、width、height以及depths字段的正确地址。因为各字段的相对地址不会发生变化。

GCC生成的指令代码与MSVC生成的代码几乎相同。唯一的区别是:GCC不会使用ECX寄存器传递this指针,它会以函数的第一个参数的传递方式传递this指针。

51.1.3 封装

封装(encapsulation)的作用是:把既定的数据和方法限定为类的私有信息,使得其他调用方只能访问类所定义的公共方法和公共数据、不能直接访问被封装起来的私有对象。

在指令层面,到底有没有划分私有对象和公开对象的界限呢?

其实完全没有。

我们来看看这一个简单的例子:

#include <stdio.h>

class box
{
    private:
        int color, width, height, depth;
    public:
        box(int color, int width, int height, int depth)
        {
            this->color=color;
            this->width=width;
            this->height=height;
            this->depth=depth;
        };
        void dump()
        {
            printf ("this is box. color=%d, width=%d, height=%d, depth=%d\n", color, width,height, depth);
        };
};

我们启用MSVC 2008的优化选项/Ox和/Ob0编译上述程序,再查看类函数box::dump()的代码。

?dump@box@@QAEXXZ PROC ; box::dump, COMDAT
; _this$ = ecx
    mov   eax, DWORD PTR [ecx+12]
    mov   edx, DWORD PTR [ecx+8]
    push  eax
    mov   eax, DWORD PTR [ecx+4]
    mov   ecx, DWORD PTR [ecx]
    push  edx
    push  eax
    push  ecx
; 'this is box. color=%d, width=%d, height=%d, depth=%d', 0aH, 00H
    push OFFSET ??_C@_0DG@NCNGAADL@this?5is?5box?4?5color?$DN?$CFd?0?5width?$DN?$CFd?0@
    call _printf
    add  esp, 20
    ret  0
?dump@box@@QAEXXZ ENDP ; box::dump

下面这个表格显示了类的变量在内存中的偏移量的分布情况。

offset

description

+0x0

int color

+0x4

int width

+0x8

int height

+0xC

int depth

所有字段都是无法被其他函数直接访问的私有变量。但是,既然我们知道了这个对象的内存存储格局,能不能写出一个修改这些变量的程序呢?

为此,我们可以构造一个名称为hack_oop_encapsulation()的函数。如果不做调整的话,直接访问既定字段的源程序大致会是:

void hack_oop_encapsulation(class box * o)
{
    o->width=1; // that code cant be compiled':
                // "error C2248: 'box::width' : cannot access private member declared in class 'box'"
};

当然,上述代码不可能被成功编译出来。然而,只要把box的数据类型强制转换为整型数组的话,我们就可以通过编译并且直接修改相应字段。

void hack_oop_encapsulation(class box * o)
{
    unsigned int *ptr_to_object=reinterpret_cast<unsigned int*>(o);
    ptr_to_object[1]=123;
};

上述函数的功能十分简单:它将输入数据视为整型数组,然后将数组的第二个元素、一个整型int值修改为123 。

?hack_oop_encapsulation@@YAXPAVbox@@@Z PROC ; hack_oop_encapsulation
    mov eax, DWORD PTR _o$[esp-4]
    mov DWORD PTR [eax+4], 123
    ret 0
?hack_oop_encapsulation@@YAXPAVbox@@@Z ENDP ; hack_oop_encapsulation

接下来,我们验证一下它的功能。

int main()
{
    box b(1, 10, 20, 30);

    b.dump();

    hack_oop_encapsulation(&b);

    b.dump();

    return 0;
};

运行的结果为:

this is box. color=1, width=10, height=20, depth=30
this is box. color=1, width=123, height=20, depth=30

我们可以看到,封装只能够在编译阶段保护类的私有对象。虽然C++编译器禁止外部代码直接访问那些被明确屏蔽的内部对象,但是通过适当的hack技术,我们确实能够突破编译器的限制策略。

51.1.4 多重继承

多重继承,指的是一个类可以同时继承多个父类的字段和方法。

我们还是写个简单的例子:

#include <stdio.h>

class box
{
    public:
        int width, height, depth;
        box() { };
        box(int width, int height, int depth)
        {
            this->width=width;
            this->height=height;
            this->depth=depth;
        };
        void dump()
        {
            printf ("this is box. width=%d, height=%d, depth=%d\n", width, height, depth);
        };
        int get_volume()
        {
            return width * height * depth;
        };
};

class solid_object
{
    public:
        int density;
        solid_object() { };
        solid_object(int density)
        {
            this->density=density;
        };
        int get_density()
        {
            return density;
        };
        void dump()
        {
            printf ("this is solid_object. density=%d\n", density);
        };
};

class solid_box: box, solid_object
{
    public:
        solid_box (int width, int height, int depth, int density)
        {
            this->width=width;
            this->height=height;
            this->depth=depth;
            this->density=density;
        };
        void dump()
        {
            printf ("this is solid_box. width=%d, height=%d, depth=%d, density=%d\n", width,↙
↘ height, depth, density);
        };
        int get_weight() { return get_volume() * get_density(); };
};

int main()
{
    box b(10, 20, 30);
    solid_object so(100);
    solid_box sb(10, 20, 30, 3);

    b.dump();
    so.dump();
    sb.dump();
    printf ("%d\n", sb.get_weight());

    return 0;
};

在启用其优化选项(/Ox和/Ob0)后,我们使用MSVC编译上述程序,重点观察box::dump()、solid_object::dump()以及solid_box::dump()这3个类成员函数。

指令清单51.12 带/Ob0参数的MSVC 2008优化程序

?dump@box@@QAEXXZ PROC ; box::dump, COMDAT
; _this$ = ecx
    mov   eax, DWORD PTR [ecx+8]
    mov   edx, DWORD PTR [ecx+4]
    push  eax
    mov   eax, DWORD PTR [ecx]
    push  edx
    push  eax
; 'this is box. width=%d, height=%d, depth=%d', 0aH, 00H
    push  OFFSET ??_C@_0CM@DIKPHDFI@this?5is?5box?4?5width?$DN?$CFd?0?5height?$DN?$CFd@
    call  _printf
    add   esp, 16
    ret   0
?dump@box@@QAEXXZ ENDP ; box::dump

指令清单51.13 带/Ob0参数的MSVC 2008优化程序

?dump@solid_object@@QAEXXZ PROC ; solid_object::dump, COMDAT
; _this$ = ecx
    mov   eax, DWORD PTR [ecx]
    push  eax
; 'this is solid_object. density=%d', 0aH
    push  OFFSET ??_C@_0CC@KICFJINL@this?5is?5solid_object?4?5density?$DN?$CFd@
    call  _printf
    add   esp, 8
    ret   0
?dump@solid_object@@QAEXXZ ENDP ; solid_object::dump

指令清单51.14 带/Ob0参数的MSVC 2008优化程序

?dump@solid_box@@QAEXXZ PROC ; solid_box::dump, COMDAT
; _this$ = ecx
    mov   eax, DWORD PTR [ecx+12]
    mov   edx, DWORD PTR [ecx+8]
    push  eax
    mov   eax, DWORD PTR [ecx+4]
    mov   ecx, DWORD PTR [ecx]
    push  edx
    push  eax
    push  ecx
; 'this is solid_box. width=%d, height=%d, depth=%d, density=%d', 0aH
    push  OFFSET ??_C@_0DO@HNCNIHNN@this?5is?5solid_box?4?5width?$DN?$CFd?0?5hei@
    call  _printf
    add   esp, 20
    ret   0
?dump@solid_box@@QAEXXZ ENDP ; solid_box::dump

上述3个类对象的内存分布如下:

① 类box。如下表所示。

offset

description

+0x0

width

+0x4

height

+0x8

depth

② 类solid_object。如下表所示。

offset

description

+0x0

density

③ 类solid_box,可以看成是以上两个类的联合体。如下表所示。

offset

description

+0x0

width

+0x4

height

+0x8

depth

+0xC

density