C++ 类虚函数实现原理的验证(指向包含类虚函数地址的数组的指针)

/*
 * @Author: Gentleave
 * @Date: 2020-11-30 11:23:47
 * @LastEditor: Gentleave
 * @LastEditTime: 2020-11-30 14:10:01
 * @Description:  
 *      这个程序用于测试我的设想:
 *          设计一个类,测试内容:当把成员函数声明为Vritual时会不会添加一个隐形的指向函数数组的指针。
            设计一个类,测试内容:让基类的指针或引用指向其派生类, 当通过基类指针使用派生类的函数或虚函数会发生什么?
 */

#include <iostream>
#include <stdio.h>

using namespace std;

class Base{
    private:
        int private_data;        
    public:
        int base_data;          // 为了测试数据的位置,把数据公有
        Base();
        Base(int d);
        void reset_bdata(int d);
        virtual void disp() const;      //这是个虚的函数
        virtual ~Base(){}
};

Base::Base() : base_data(0){ private_data = 5; }
Base::Base(int d) : base_data(d){ private_data = 5; }
void Base::reset_bdata(int d)
{
    this->base_data = d;
}
void Base::disp() const
{
    cout << "class Base data = " << base_data << endl; 
}

class Derive : public Base{
    private:
    public:
        int derive_data; // 为了测试数据的位置,把数据公有
        Derive();
        Derive(int dd);
        Derive(int dd, int bd);

        void reset_ddata(int dd);
        virtual void disp() const;      // 虚函数被重新定义
        virtual void dual_disp() const;         // 这是一个在派生类新定义的方法,同时也是虚的
        ~Derive(){}
};

Derive::Derive() : Base() {}
Derive::Derive(int dd) : derive_data(dd), Base() {}
Derive::Derive(int dd, int bd) : derive_data(dd), Base(bd) {}

void Derive::reset_ddata(int dd)
{
    this->derive_data = dd;
}

void Derive::disp() const
{
    cout << "class derive data = " << derive_data << endl; 
}

void Derive::dual_disp() const
{
    Base::disp();
    Derive::disp();
}

int main() {
    
    Base bd(10);
    Derive dd(20, 10);

    Base & pdd = dd;     // 基类引用指向派生类
    Base & pbd = bd;     // 基类引用指向基类

/* ------------------这一段测试虚函数的有效性------------ */
    cout << "test virtual valid." << endl;

    bd.disp();
    pbd.disp();

    cout << endl;
    
    dd.disp();
    pdd.disp();

    cout << endl;

/* --------------------测试虚函数是否占了类的大小------- */

    cout << "test class size." << endl;


    cout << "sizeof(Base): " << sizeof(Base) << ", sizeof(int)" << sizeof(int) << endl;
    cout << "&bd: " << &bd << ", &(bd.base_data): " << &(bd.base_data) << endl;

    cout << "sizeof(Derive): " << sizeof(Derive) << ", sizeof(int)" << sizeof(int) << endl;
    cout << "&dd: " << &dd << ", &(dd.base_data): " << &(dd.base_data) << ", &(dd.derive_data): " << &(dd.derive_data) << endl;

    /* 
    这是打印出的数据
    test class size.
    sizeof(Base): 12, sizeof(int)4
    &bd: 0x61fefc, &(bd.base_data): 0x61ff04
    sizeof(Derive): 16, sizeof(int)4
    &dd: 0x61feec, &(dd.base_data): 0x61fef4, &(dd.derive_data): 0x61fef8
    
    由打印的数据可以看出类对象的成员分布:
        类成员变量会占用一定的内存,而普通的类方法则不占用对象的内存,类方法是所有类对象共有的
        Base类使用了两个int成员变量,其size应该是8才对, 但因为方法中声明了虚函数,导致类在编译时
        使用了C++PrimerPlus书中所说的动态联编(dynamic binding), 这会使类对象创建时多一个指向
        虚函数指针数组的指针,看样子这个指针的位置在对象的最开头处, 看样子这个指针的大小也是4字节。
    
    下面手动测试一下看能不能通过这个虚函数的指针数组得到虚函数的地址
    */
    cout << "sizeof(ptr): " << sizeof(Base*) << endl;

    int * arr = (int*)(&bd);
    /* 以16进制 打印对象的内容 */
    cout << std::hex << "bd context: " <<  arr << "::" << arr[0] << ", " << arr[1] << ", " << arr[2] << endl;

    int * p_virtual_array = (int*)(*((int*)(&bd)));
    cout << std::hex << "p_v_a context: " << p_virtual_array << "::" << p_virtual_array[0] << ", " 
                                        << p_virtual_array[1] << ", " << p_virtual_array[2] << ", " 
                                        << p_virtual_array[3] << ", " << p_virtual_array[4] << ", " 
                                        << p_virtual_array[5] << ", " << p_virtual_array[6] << ", " 
                                        << p_virtual_array[7] << ", " << p_virtual_array[8] << endl;

    typedef void (*Ppvoid)(void *);
    cout << "bd virtual array addr: " << p_virtual_array << 
            ", Base disp() addr: " << (int *)(Ppvoid)(&Base::disp) <<
            ", Base reset_bdata() addr: " << (int *)(Ppvoid)(&Base::reset_bdata) <<
            endl;
    
    cout << endl << endl;

    arr = (int*)(&dd);
    /* 以16进制 打印对象的内容 */
    cout << std::hex << "dd context: " <<  arr << "::" << arr[0] << ", " << arr[1] << ", " << arr[2] << endl;

    p_virtual_array = (int*)(*((int*)(&dd)));
    cout << std::hex << "p_v_a context: " << p_virtual_array << "::" << p_virtual_array[0] << ", " 
                                        << p_virtual_array[1] << ", " << p_virtual_array[2] << ", " 
                                        << p_virtual_array[3] << ", " << p_virtual_array[4] << ", " 
                                        << p_virtual_array[5] << ", " << p_virtual_array[6] << ", " 
                                        << p_virtual_array[7] << ", " << p_virtual_array[8] << endl;

    typedef void (*Ppvoid)(void *);
    cout << "dd virtual array addr: " << p_virtual_array << 
            ", Derive disp() addr: " << (int *)(Ppvoid)(&Derive::disp) <<
            ", Derive dual_disp() addr: " << (int *)(Ppvoid)(&Derive::dual_disp) <<
            ", Derive reset_ddata() addr: " << (int *)(Ppvoid)(&Derive::reset_ddata) <<
            endl;
    /* 
    这是上面那一坨程序执行打印的内容:
        bd context: 0x61fef4::4063e8, 5, a
        p_v_a context: 0x4063e8::40147e, 404a54, 404a2c, 0, 4063c4, 40155c, 404aac, 404a84, 4015a2
        bd virtual array addr: 0x4063e8, Base disp() addr: 0x40147e, Base reset_bdata() addr: 0x401466


        dd context: 0x61fee4::4063fc, 5, a
        p_v_a context: 0x406418::40155c, 404acc, 404aa4, 4015a2, 3a434347, 694d2820, 2e57476e, 2067726f, 736f7243
        dd virtual array addr: 0x406418, Derive disp() addr: 0x40155c, Derive dual_disp() addr: 0x4015a2, Derive reset_ddata() addr: 0x401544
    可以看到: 类的首4字节确实存储着一个数字地址,分别是:4063e8(bd) 4063fc(dd)
    , 通过强制转换之后,可以得到其指针数组指向的内容:40147e(bd) 40155c(dd) 就是各自虚函数disp的地址
    跟直接调用&Base::disp 得到的地址比对,确实一样! good!
    还有就是,在Derive类中新增了一个虚函数,dual_disp(), 可以看到它的地址是:0x4015a2,去p_v_a数组中可以看到,
    它在数组偏移3个指针的位置。也就是说,每实现一个虚函数都会占用虚函数指针数组的3*4=12个字节的空间,可能这跟编译器实现有关。
    第一个指针数据是函数地址没错, 但后面两个指针的内容是什么就不得而知了
     */
/* ---------------------测试当通过基类指针使用派生类的函数或虚函数会发生什么--------- */

    dd.dual_disp();

    /* 事实证明, 下面这条语句被视为错误, 也就是说当把派生类赋给基类指针或引用时,它就发生退化了, 
        指向派生类的基类指针和引用只能 使用基类的方法,而不能使用派生类的方法  */
    // pdd.dual_disp();


}


posted @ 2020-11-30 14:13  Gentleaves  阅读(103)  评论(0编辑  收藏  举报