知其所以然,C++系列1 - C++对象模型初探

目录

1. 本文目的:

2. 注意:

3. 开始探索:

3.1 最简单的例子走起:直接从带虚函数的类开始。

3.2 虚函数表

3.3 虚函数的排列顺序

3.4 vtable[-1]指向type_info

4. 结论


1. 本文目的:

当你写完一个类时,有没有好奇它会被编译器翻译成啥哪?你那看似富丽堂皇的房子,其实在装修前都是一堆灰突突的水泥而已,在C++的世界里编译器就是那个技术高超的装修工,他这搭一管子那扯一杆线使得房子住起来很舒服,本文就是想看看这些管子和线在C++的世界里是如何部署的。

2. 注意:

本文例子都是在Linux上64位环境下使用g++编译,Windows上与之不同。

编译&运行:g++ SingleVirtualClass.cpp -std=c++11; ./a.out

3. 开始探索:

3.1 最简单的例子走起:直接从带虚函数的类开始。

#include<iostream>
using namespace std;

class Base{
public:
        int i;
        int j;

        virtual void print(){cout<<"I am print. addr of i= "<<hex<<&i<<endl;}

        Base(int _i, int _j):i(_i),j(_j){}
};
int main(){
        Base base(1,2);

        cout<<"offset of i: "<<(char*)&(base.i)-(char*)&base<<endl; //1. offset of i: 8
        cout<<"offset of j: "<<(char*)&(base.j)-(char*)&base<<endl; //2. offset of j: 12

}

可以看到 i相对base的偏移是8,j是12,那么前8个字节是什么东东?其实是vptr(指向虚函数表的指针)。

3.2 虚函数表

为了实现C++的多态,C++使用了一种动态绑定的技术。这个技术的核心是虚函数表。

如果类有虚函数,则每个实例化的对象除了存储成员变量外还在偏移为0处存储一个指针指向虚函数表,虚函数表中存储的是每个虚函数的地址。让我们扩充上面的例子通过虚函数表间接调用虚函数。

/*zhaimin.cn@gmail.com*/
#include<iostream>
#include<typeinfo>
using namespace std;

class Base{
public:
        int i;
        int j;

        virtual void print(){cout<<"I am print. addr of i= "<<hex<<&i<<endl;}

        Base(int _i, int _j):i(_i),j(_j){}
};

int main(){
        Base base(1,2);

        cout<<"offset of i: "<<(char*)&(base.i)-(char*)&base<<endl; //offset of i: 8
        cout<<"offset of j: "<<(char*)&(base.j)-(char*)&base<<endl; //offset of j: 12

        void** vtable = *(void***)&base; //取出前8个字节的值
        typedef void (*Func)(void*);
        Func f1 = (Func)vtable[0];//vtable[0]就是函数print的地址
        f1(&base); //I am print. addr of i= ...

3.3 虚函数的排列顺序

如果有多个虚函数,按声明顺序排列。

让我们添加三个虚函数,并依次调用虚函数表中的每一项,看输出是否与声明顺序一致。

/*zhaimin.cn@gmail.com*/
#include<iostream>
#include<typeinfo>
using namespace std;
/*********************************************
&base --> |vptr| --> |print  | --> def of print
          |i   |     |print2 | --> def of print2
          |j   |     |print3 | --> def of print3
***********************************************/

class Base{
public:
        int i;
        int j;

private:
        virtual void print(){cout<<"I am print. addr of i= "<<hex<<&i<<endl;}

public:
        virtual void print2(){cout<<"I am print2"<<endl;}
        virtual void print3(){cout<<"I am print3. i="<<i<<endl;}

        Base(int _i, int _j):i(_i),j(_j){}
};
int main(){
        Base base(1,2);

        cout<<"offset of i: "<<(char*)&(base.i)-(char*)&base<<endl; //offset of i: 8
        cout<<"offset of j: "<<(char*)&(base.j)-(char*)&base<<endl; //offset of j: 12

        void** vtable = *(void***)&base;
        typedef void (*Func)(void*);

        Func f1 = (Func)vtable[0];
        f1(&base); //I am print. addr of i=
        Func f2 = (Func)vtable[1];
        f2(0);     //I am print2
        Func f3 = (Func)vtable[2];
        f3(&base); //I am print3. i=1
}

上面的输出不但说明虚函数表是按声明顺序排列各个虚函数的地址,而且:

如果你注意到把print()声明为private后依然可以间接调用它,这说明了C++所谓的private、public、protected权限只是在编译阶段验证的,运行时并不验证。

3.4 vtable[-1]指向type_info

如果你看过C++对象模型相关的资料就会知道虚函数表前面还有一项,它指向type_info. 让我们继续通过扩充上面的例子来核实这个结论。

/*zhaimin.cn@gmail.com*/
#include<iostream>
#include<typeinfo>
using namespace std;
/*********************************************
&base --> |vptr| --> |print  | --> def of print
          |i   |     |print2 | --> def of print2
          |j   |     |print3 | --> def of print3
***********************************************/

class Base{
public:
        int i;
        int j;

private:
        virtual void print(){cout<<"I am print. addr of i= "<<hex<<&i<<endl;}

public:
        virtual void print2(){cout<<"I am print2"<<endl;}
        virtual void print3(){cout<<"I am print3. i="<<i<<endl;}

        Base(int _i, int _j):i(_i),j(_j){}
};
int main(){
        Base base(1,2);

        cout<<"offset of i: "<<(char*)&(base.i)-(char*)&base<<endl; //offset of i: 8
        cout<<"offset of j: "<<(char*)&(base.j)-(char*)&base<<endl; //offset of j: 12

        void** vtable = *(void***)&base;
        typedef void (*Func)(void*);

        Func f1 = (Func)vtable[0];
        f1(&base); //I am print. addr of i=
        Func f2 = (Func)vtable[1];
        f2(0);     //I am print2
        Func f3 = (Func)vtable[2];
        f3(&base); //I am print3. i=1

        Base b1(1,2);
        Base b2(3,4);
        void** vtable1 = *(void***)&b1;
        type_info* ti1 = (type_info*)vtable1[-1];
        void** vtable2 = *(void***)&b2;
        type_info* ti2 = (type_info*)vtable2[-1];
        cout<<"addr of type_info for b1: "<<hex<<ti1<<endl; //They points to the same type_info instance.
        cout<<"addr of type_info for b2: "<<hex<<ti2<<endl;
        cout<<typeid(b1).name()<<endl;

}

最后三行的输出是一样的,不但说明vtable[-1]确实指向type_info实例,而且打印的地址相同说明了每个类在全局就一个描述这个类的type_info实例。

4. 结论

/*********************************************
                     |type_info*
&base --> |vptr| --> |print  | --> def of print
          |i   |     |print2 | --> def of print2
          |j   |     |print3 | --> def of print3
***********************************************/

本节只探讨有虚函数但无继承情况下的C++对象模型,下一节看看有继承时又是如何布局的哪?

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

深山老宅

鸡蛋不错的话,要不要激励下母鸡

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值