浅析C++当中的对象模型


我们来分析一下C++当中的对象模型。

什么是虚函数表?

C++的对象模型,我们首先需要分析的是虚函数表。
虚函数,这里我们就要牵扯到多态。简单的说就是父类指针或者引用调用重写的虚函数,当指向父类的时候,调用父类的虚函数,当指向子类的时候,这时候会调用子类的虚函数。

首先,我们在这里需要了解一个概念,虚函数表。

虚函数呢,就是通过虚函数表来实现的,在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证你所调用的时候调用实际的函数。所以虚函数表对我们来说是比较重要的,通过虚函数表,我们就可以看到内存当中所实际调用的指针。


虚函数底层实现机制

虚函数表:类的虚函数表是一块连续的内存,每个内存单元中记录一个JMP指令的地址。

  编译器会为每个有虚函数的类创建一个虚函数表,该虚函数表将被该类的所有对象共享。类的每个虚函数占据虚函数表中的一块。如果类中有N个虚函数,那么其虚函数表将有N*4字节的大小。

  在有虚函数的类的实例中分配了指向这个表的指针的内存,所以,当用父类的指针来操作一个子类的时候,这张虚函数表就显得尤为重要了,它就像一个地图一样,指明了实际所应该调用的函数。

  编译器应该是保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能——如果有多层继承或是多重继承的情况下)。 这意味着可以通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。

  ->有虚函数或虚继承的类实例化后的对象大小至少为4字节(确切的说是一个指针的字节数;说至少是因为还要加上其他非静态数据成员,还要考虑对齐问题);没有虚函数和虚继承的类实例化后的对象大小至少为1字节(没有非静态数据成员的情况下也要有1个字节来记录它的地址)。

  有纯虚函数的类为抽象类,不能定义抽象类的对象,它的子类要么实现它所有的纯虚函数变为一个普通类,要么还是一个抽象类。

  特别的:

  (1)当存在类继承并且析构函数中有必须要进行的操作时(如需要释放某些资源,或执行特定的函数)析构函数需要是虚函数,否则若使用父类指针指向子类对象,在delete时只会调用父类的析构函数,而不能调用子类的析构函数,从而造成内存泄露或达不到预期结果;

  (2)内联函数不能为虚函数:内联函数需要在编译阶段展开,而虚函数是运行时动态绑定的,编译时无法展开;

  (3)构造函数不能为虚函数:构造函数在进行调用时还不存在父类和子类的概念,父类只会调用父类的构造函数,子类调用子类的,因此不存在动态绑定的概念;但是构造函数中可以调用虚函数,不过并没有动态效果,只会调用本类中的对应函数;

  (4)静态成员函数不能为虚函数:静态成员函数是以类为单位的函数,与具体对象无关,虚函数是与对象动态绑定的。


接下来,我们通过代码的实例来观察虚函数表
首先我们写一个类,

class Base
{
public:
    virtual void fun()
    {
        cout << "Base::fun" << endl;
    }
    virtual void show()
    {
        cout << "Base::show" << endl;
    }
    virtual void play()
    {
        cout << "Base::play" << endl;
    }
private:
    int _b;

};

另外给上测试函数,创建一个这个类的对象。

void test()
{
    Base b;
}

这时,我们打开见识窗口,去看这个对象,在这里我们可以看到:
这里写图片描述
在这里我们可以看到有一个所谓的__vfptr,这个就是一个指向虚函数表的指针。而它所指向的内容我们可以发现,是类中的三个虚函数的一个地址。这三个虚函数的地址就构成了所说的虚函数表。
然后我们来查看内存,这里我们接下来需要看的是内存中存放虚函数表的部位。
这里写图片描述
这里红框中就是我们所说的虚函数表,要记得虚函数表在最后会保存一个0x00000000来作为结束的标志(VS中)如果是在linux下,这个值是如果1,表示还有下一个虚函数表,如果值是0,表示是最后一个虚函数表。

如果你还不相信的话,那么我们可以对那块地址拿过来调用,看是否是调用了那三个函数,输出那三个函数中的内容就可以了,在这我们需要添加一部分代码。

typedef void(*ptr)();
void PrintVfptr(ptr * pfun)
{
    int i = 0;
    for (i = 0; pfun[i] != NULL; i++)
    {
        pfun[i]();
    }
}
void test()
{
    Base b;
    PrintVfptr((ptr*)(*(int *)&b));


}

结果:这里写图片描述
这里我们就可以清楚的知道虚函数表中所保存的到底是什么。

接下来,我们对各种继承的情况进行统一的分析。

单继承(无虚函数覆盖)

class Base
{
public:
    virtual void show()
    {
        cout << "Base::show()" << endl;
    }
    virtual void play()
    {
        cout << "Base::play()" << endl;
    }
    virtual void fun()
    {
        cout << "Base::fun()" << endl;
    }
private:
    int _b;
};

class Derive:public Base
{
public:
    virtual void show1()
    {
        cout << "Base::show()" << endl;
    }
    virtual void play1()
    {
        cout << "Base::play()" << endl;
    }
    virtual void fun1()
    {
        cout << "Base::fun()" << endl;
    }
private:
    int _d;
};

首先我们分析这一层继承关系,在这一层当中没有任何一个虚函数发生了覆盖的情况。

接下来我们查看监视窗口和内存窗口。
这里写图片描述
这里写图片描述
透过监视我们可以看到虚函数表中有Base的三个函数的地址,然后通过内存我们可以知道虚函数表中有6个地址,那么这6个地址是不是所有的虚函数的地址呢?接下来我们进行调用查看下。
添加测试代码:

typedef void(*ptr)();
void PrintVfptr(ptr * pfun)
{
    int i = 0;
    for (i = 0; pfun[i] != NULL; i++)
    {
        pfun[i]();
    }
}
void test()
{
    Derive d;
    PrintVfptr((ptr*)(*(int *)&d));

}

这里写图片描述
这里的6个输出刚好对应我们的6个函数,所以也就可以清楚的认识到虚函数表中的内容都是这些函数的地址。

通过这个例子,我们也就很容易能够认识到两点。
1)虚函数按照其声明顺序放于表中。
2)父类的虚函数在子类的虚函数前面。

单继承(有虚函数覆盖)

我们接下来看下有虚函数覆盖时的情况。

首先给出我们的演示代码:

class Base
{
public:
    virtual void show()
    {
        cout << "Base::show" << endl;
    }
    virtual void fun()
    {
        cout << "Base::fun" << endl;
    }
    virtual void play()
    {
        cout << "Base::play" << endl;
    }
private:
    int _b;
};

class Derive:public Base
{
public:
    virtual void show()
    {
        cout << "Derive::show" << endl;
    }
    virtual void fun1()
    {
        cout << "Derive::fun1" << endl;
    }
    virtual void play()
    {
        cout << "Derive::play" << endl;
    }
private:
    int _b;
};

接下来查看内存和监视窗口
这里写图片描述
这里写图片描述
在这里我们就可以清楚的看到,发生了覆盖,虚函数表当中监视窗口所显示的是覆盖了的show()函数,父类没有覆盖的虚函数fun()函数,还有被覆盖了的play()函数。
通过内存,我们可以看到其中应该有4个函数地址,3个函数地址是我们上面所说的,那么第四个函数地址是谁的呢?我们可以通过调用来进行查看。


typedef void(*ptr)();
void PrintVfptr(ptr * pfun)
{
    int i = 0;
    for (i = 0; pfun[i] != NULL; i++)
    {
        pfun[i]();
    }
}
void test()
{
    Derive d;
    PrintVfptr((ptr*)(*(int *)&d));
}

这里写图片描述
这可以清楚的看的到,前3个函数和我们分析的是一样的,最后一个函数是子类中的独有的虚函数,所以在这里我们也就可以得到三条规律。
1)覆盖的函数被放到了虚表中原来父类虚函数的位置。
2)没有被覆盖的函数依旧存放在原来的位置。
3)最后将子类独有的虚函数放到下面。

多继承(无虚函数覆盖)

关于多继承:



class Base1
{
public:
    virtual void show()
    {
        cout << "Base1::show" << endl;
    }
    virtual void play()
    {
        cout << "Base1::play" << endl;
    }
    virtual void fun()
    {
        cout << "Base1::fun" << endl;
    }
};

class Base2
{
public:
    virtual void show()
    {
        cout << "Base2::show" << endl;
    }
    virtual void play()
    {
        cout << "Base2::play" << endl;
    }
    virtual void fun()
    {
        cout << "Base2::fun" << endl;
    }
};
class Base3
{
public:
    virtual void show()
    {
        cout << "Base3::show" << endl;
    }
    virtual void play()
    {
        cout << "Base3::play" << endl;
    }
    virtual void fun()
    {
        cout << "Base3::fun" << endl;
    }
};

class Derive:public Base1, public Base2,public Base3
{
public:
    virtual void show1()
    {
        cout << "Derive::show1" << endl;
    }
    virtual void play1()
    {
        cout << "Derive::play1" << endl;
    }

};

在这里的的内存和监视窗口:
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里遇到了点问题,我认为可能是因为编译器版本的问题,在旧版的VC6.0上我曾经试过,和我预想的是一样的,所以,如果下去你试的时候不要惊讶出现这种结果。
我们根据监视窗口的内容依然可以能够看出来,虚函数表依赖于继承关系,有多少继承关系,就会有多少虚函数表。接下来我们就调用一下虚函数表中的地址,看看输出和我们预想是否一样。
正因为编译器带来的一些问题,我们想要解决它,所以需要在测试函数来下功夫。

typedef void(*ptr)();
void PrintVfptr1(ptr * pfun)
{
    int i = 0;
    for (i = 0; i<5; i++)
    {
        pfun[i]();
    }
}
void PrintVfptr2(ptr * pfun)
{
    pfun[0]();
    pfun[1]();
    pfun[2]();
}
void PrintVfptr3(ptr * pfun)
{
    pfun[0]();
    pfun[1]();
    pfun[2]();


}
void test()
{
    Derive d;
    PrintVfptr1((ptr*)(*(int *)&d));
    cout << "**************************" << endl;
    PrintVfptr2((ptr*)(*((int *)(&d) + 1)));
    cout << "**************************" << endl;
    PrintVfptr3((ptr*)(*((int *)(&d) + 2)));
}
int main()
{
    test();

    system("pause");
    return 0;
}

最后得到的输出结果。
这里写图片描述从输出结果我们可以得到最终和我们预想是一样的,,在这里面因为没有虚函数的覆盖,我们可以得到以下规律。

1) 每个父类都有自己的虚表。
2) 子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)

这样,就解决了多继承中父类和子类的对应问题了。

多继承(有虚函数覆盖)

对于多继承有虚函数的覆盖,我们给出示例代码。

class Base1
{
public:
    virtual void show()
    {
        cout << "Base1::show" << endl;
    }
    virtual void play()
    {
        cout << "Base1::play" << endl;
    }
    virtual void fun()
    {
        cout << "Base1::fun" << endl;
    }
};

class Base2
{
public:
    virtual void show()
    {
        cout << "Base2::show" << endl;
    }
    virtual void play()
    {
        cout << "Base2::play" << endl;
    }
    virtual void fun()
    {
        cout << "Base2::fun" << endl;
    }
};
class Base3
{
public:
    virtual void show()
    {
        cout << "Base3::show" << endl;
    }
    virtual void play()
    {
        cout << "Base3::play" << endl;
    }
    virtual void fun()
    {
        cout << "Base3::fun" << endl;
    }
};

class Derive:public Base1, public Base2,public Base3
{
public:
    virtual void show()
    {
        cout << "Derive::show" << endl;
    }
    virtual void play()
    {
        cout << "Derive::play" << endl;
    }
    virtual void fun1()
    {
        cout << "Derive::fun1" << endl;
    }
};

typedef void(*ptr)();
void PrintVfptr(ptr * pfun)
{
    int i = 0;
    for (i = 0; pfun[i]!=NULL; i++)
    {
        pfun[i]();
    }
}
void test()
{
    Derive d;
    PrintVfptr((ptr*)(*(int *)&d));
    cout << "**************************" << endl;
    PrintVfptr((ptr*)(*((int *)(&d) + 1)));
    cout << "**************************" << endl;
    PrintVfptr((ptr*)(*((int *)(&d) + 2)));
}
int main()
{
    test();

    system("pause");
    return 0;
}

在这里面我们依然和上面的方式一样,查看监视窗口和内存窗口
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里我们可以清楚的看到监视窗口中有三个_vfptr指针分别指向各自对应的虚函数表, 因为十三个继承关系,所以也就有了三个虚函数表。
然后我们可以清晰的看到在对应的虚函数表中储存了地址,这些地址就是对应的函数的地址,当然,当我们进行调用的时候,那么我们就可以进行函数的调用。我们从 监视窗口可以看到每个虚函数表中的show()函数和play()函数被覆盖。这里就是修改了虚函数中所对应的函数的地址,由父类地址改为了子类的地址,所以最终完成了覆盖。
结果:
这里写图片描述
结果也和我们在监视窗口中看到的是一样的。
这样,我们就可以任一静态类型的父类来指向子类,并调用子类的f()了。

重复继承



class B
{
public:
    B()
        :_b(0)
    {}
    virtual void show()
    {
        cout << "B::show" << endl;
    }
    virtual void play()
    {
        cout << "B::play" << endl;
    }
    int _b;
};
class B1:public B
{
public:
    B1()
        :_b1(1)
    {}
    virtual void show()
    {
        cout << "B1::show" << endl;
    }
    virtual void play()
    {
        cout << "B1::play" << endl;
    }
    int _b1;

};
class B2:public B
{
public:
    B2()
        :_b2(2)
    {}
    virtual void show()
    {
        cout << "B2::show" << endl;
    }
    virtual void play()
    {
        cout << "B2::play" << endl;
    }
    int _b2;

};


class D :public B1, public B2
{
public:
    D()
    :_d(3)
    {}
    virtual void show()
    {
        cout << "D::show" << endl;
    }
    virtual void play()
    {
        cout << "D::play" << endl;
    }
    virtual void fun()
    {
        cout << "D::fun" << endl;
    }
    int _d;

};
typedef void(*ptr)();
void printvfptr(ptr * pfun)
{
    int i = 0;
    for (i = 0;i<3; i++)
    {
        pfun[i]();
    }
}
void printvfptr1(ptr * pfun)
{
    int i = 0;
    for (i = 0; i<2; i++)
    {
        pfun[i]();
    }
}

void test()
{
    D d;
    printvfptr((ptr*)(*(int *)&d));
    cout << "**************************" << endl;
    printvfptr1((ptr*)(*((int *)(&d) + 3)));
    cout << "**************************" << endl;

}

int main()
{

    test();

    system("pause");
    return 0;
}

代码如上

接下来依然进行我们所需要的信息的分析,内存监视窗口。
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
其中依然存在结束标志不为NULL的问题,我们这里根据继承关系自己推断出其中包含的多少个地址就好了。

接下来查看运行结果。
可以清楚的看到两个虚函数表当中存储的内容。
但是如果你观察仔细的话,会发现其中B类的成员_b在内存当中保存了两份。他们是这样的,B1和B2继承来自B,然后D继承自B1和B2,所以最终得到的结果就是D中保存了两份的B中的成员变量。所以这个时候就会产生数据冗余和二义性的问题,因为这个时候当你使用d中的_b的时候,这时候并不知道你所指的是哪一个。
这里写图片描述

钻石型多重虚拟继承



class B
{
public:
    B()
        :_b(0)
    {}
    virtual void show()
    {
        cout << "B::show" << endl;
    }
    virtual void play()
    {
        cout << "B::play" << endl;
    }
    int _b;
};
class B1:virtual public B
{
public:
    B1()
        :_b1(1)
    {}
    virtual void show()
    {
        cout << "B1::show" << endl;
    }
    virtual void play()
    {
        cout << "B1::play" << endl;
    }
    int _b1;

};
class B2:virtual public B
{
public:
    B2()
        :_b2(2)
    {}
    virtual void show()
    {
        cout << "B2::show" << endl;
    }
    virtual void play()
    {
        cout << "B2::play" << endl;
    }
    int _b2;

};


class D :public B1, public B2
{
public:
    D()
    :_d(3)
    {}
    virtual void show()
    {
        cout << "D::show" << endl;
    }
    virtual void play()
    {
        cout << "D::play" << endl;
    }
    virtual void fun()
    {
        cout << "D::fun" << endl;
    }
    int _d;

};
typedef void(*ptr)();
void printvfptr(ptr * pfun)
{
    int i = 0;
    for (i = 0;pfun[i]!=NULL; i++)
    {
        pfun[i]();
    }
}

void test()
{
    D d;
    printvfptr((ptr*)(*(int *)&d));
    cout << "**************************" << endl;
    printvfptr((ptr*)(*((int *)(&d) + 7)));
    cout << "**************************" << endl;

}
int main()
{

    test();

    system("pause");
    return 0;
}

这就是对于钻石型继承为了解决它的数据冗余和二义性问题,引入了虚继承。

这里写图片描述
通过这幅图,我们可以知道这里只有两个虚表,在这里,这个0x008fdd40其实就是一个公共的虚表来进行保存。
这里写图片描述
通过内存,我们可以清晰的看到,这里0x008fdd30和0x008fdd40是保存虚函数表的指针,下面还有两个指针0x008fdd54和0x008fdd5c。
通过内存我们来看这四个指针指向的内容。
这里写图片描述
这里写图片描述
这两个是虚函数表中保存的内容,因为这里采用了虚继承,我们可以知道这里的地址被重写为派生类的函数的地址。而且这里只保存了一份。
这里写图片描述
这里写图片描述
这里可以清楚的看到,在这这又两个值。十六进制的18和十六进制的10,转换下来是十进制的24和16。这两个值其实就是寻找公共部分的偏移量,这个时候我们再借住内存就可以看懂了。
这里写图片描述
这里我们也就了解了所有的机制,接下来我们就可以通过测试看最终的输出的结果。
这里写图片描述
最终和我们在内存中所见是一样的,只有一份公共的然后其中一个保存自有的。

安全性

通过上面的讲述,相信我们对虚函数表有一个比较细致的了解了。水可载舟,亦可覆舟。下面,让我们来看看我们可以用虚函数表来干点什么坏事吧。
一、通过父类型的指针访问子类自己的虚函数
我们知道,子类没有重载父类的虚函数是一件毫无意义的事情。因为多态也是要基于函数重载的。
任何妄图使用父类指针想调用子类中的未覆盖父类的成员函数的行为都会被编译器视为非法,所以,这样的程序根本无法编译通过。但在运行时,我们可以通过指针的方式访问虚函数表来达到违反C++语义的行为。(关于这方面的尝试,通过阅读后面附录的代码,相信你可以做到这一点)

二、访问non-public的虚函数
另外,如果父类的虚函数是private或是protected的,但这些非public的虚函数同样会存在于虚函数表中,所以,我们同样可以使用访问虚函数表的方式来访问这些non-public的虚函数,这是很容易做到的。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值