bosthhe1 / cpushpush

0 stars 0 forks source link

虚函数的更多底层理解+虚表打印 #24

Open bosthhe1 opened 1 year ago

bosthhe1 commented 1 year ago
namespace hxh
{
    struct person
    {
        virtual person& fun(){ cout << "person::fun()" << endl; return *this; }
        int a = 0;
    };
    class student : public person//b继承了a
    {
    public:
        student& fun(){ cout << "student::fun()" << endl; return *this; }
        int b = 0;
    };
}
void fun1(hxh::person& a)
{
    a.fun();
}
int main()
{
    hxh::person a;
    hxh::student b;
    return 0;
}

首先我们需要明白虚函数和普通函数不一样,虚函数的调用时一个指针指向一个虚函数的数组,然后虚函数的数组是一个指针数组,指针数组里面的指针才指向对应的虚函数,所以在含有虚函数的类,实例化会多一个虚表指针,会比一般类多4个字节,同时也需要注意的是,同类型的对象,指向的虚表时同一个虚表,不同类型对象不管子类有没有重写或者虚函数,子类和父类都会建立自己的虚表。即使子类虚函数都是父类继承的,虽然虚函数的地址相同,但是虚表是各自一份 大致思路是下图这样 image 而重写在底层的叫法叫覆盖,编译器在底层子类和父类构成重写的函数,子类的虚函数就会覆盖父类的函数 image 我们调试以上代码发现,子类对象存有虚表指针和父类对象的虚表指针是不一样的,所以会调用不同类型的虚函数

bosthhe1 commented 1 year ago

为什么我们将子类的引用或者指针传给父类,父类去调用的时候,会调用到子类的虚表,是因为以引用为例,引用时对子类的一部分取别名,但那一部分还是子类的,所以调用的时候,还是会去调用子类的虚函数,那为什么不是引用或者指针就不能构成重写了呐?是因为为了防止编译器紊乱,如果允许子类直接赋给父类,父类就可以调用子类的虚函数,那么就是父类继承子类,会把关系搞乱,所以不支持除了引用或者指针,其他都不能构成重写 image

bosthhe1 commented 1 year ago
namespace hxh
{
    struct person
    {
        virtual person& fun(){ cout << "person::fun()" << endl; return *this; }
        virtual person& count(){ cout << "person::count()" << endl; return *this; }
        int a = 0;
    };
    class student : public person
    {
    public:
        student& fun(){ cout << "student::fun()" << endl; return *this; }
        int b = 0;
    };
}
int main()
{
    hxh::person a;
    hxh::student b;
    return 0;
}

我们看到以上的代码,我们将fun虚函数重写,但是count虚函数没有重写 image 我们通过调试看到,没有重写的部分继承下来,虚表中的函数地址是一样的,都是父类虚函数的地址,重写的部分则被子类覆盖

bosthhe1 commented 1 year ago
namespace hxh
{
    struct person
    {
        virtual person& fun(){ cout << "person::fun()" << endl; return *this; }
        virtual person& count(){ cout << "person::count()" << endl; return *this; }
        int a = 0;
    };
    class student : public person
    {
    public:
        virtual student& fun(){ cout << "student::fun()" << endl; return *this; }
        virtual student& sum(){ cout << "student::sum()" << endl; return *this; }
        int b = 0;
    };
}
int main()
{
    hxh::person a;
    hxh::student b;
    return 0;
}

在这种情况下,当子类存在新的虚函数,编译器没有将新的虚函数打印出来,但是我们看到底层,新的虚函数对应的地址还是在内存中存在 image

bosthhe1 commented 1 year ago
class Base1 {
public:
    virtual void func1() { cout << "Base1::func1" << endl; }
    virtual void func2() { cout << "Base1::func2" << endl; }
private:
    int b1;
};
class Base2 {
public:
    virtual void func1() { cout << "Base2::func1" << endl; }
    virtual void func2() { cout << "Base2::func2" << endl; }
private:
    int b2;
};
class Derive : public Base1, public Base2 {
public:
    virtual void func1() { cout << "Derive::func1" << endl; }
    virtual void func3() { cout << "Derive::func3" << endl; }
private:
    int d1;
};
int main()
{
    Derive a;
    return 0;
}

我们看到多继承Derive类继承了Base1和Base2,所以子类里面会有两个虚函数表,且两个虚函数表中的func1(),都会被覆盖 image 我们看到内存中Base1表和Base2表,按照道理来说他们中的fun1()虚函数都应该指向同一块地址,但是内存上却不是显示同一块地址,其实这里是编译器对继承的第二个类的虚函数表进行了封装,且在这里需要注意,子类的个有的虚函数(fun3()),只会将fun3放入第一个继承的虚表中(Base1),第二个虚表中没有

bosthhe1 commented 1 year ago

在多继承这里构成重写,其实对于Base1和Base2底层都是指向同一块地址,但是为什么虚函数表中的地址不相同,是因为编译器对Base2的指针进行了封装多层,为什么需要多层封装呢?是需要修饰this指针,如果一直跳转,最终也是跳转到对应的虚函数 image 这里需要注意的点是:在调用类对象或者类对象函数的时候寄存器里面存的是this指针。

bosthhe1 commented 1 year ago
typedef void(*VF_prt)();

class Base1 {
public:
    virtual void func1() { cout << "Base1::func1" << endl; }
    virtual void func2() { cout << "Base1::func2" << endl; }
private:
    int b1 = 1;
};

class Derive : public Base1{
public:
    virtual void func1() { cout << "Derive::func1" << endl; }
    virtual void func2() { cout << "Derive::func2" << endl; }
private:
    int d1 = 3;
};

void func4(VF_prt* P)
{
    for (int i = 0; P[i] != nullptr; i++)
    {
        cout << "P[" << i << "]:" << &P[i] << endl;
    }
}

int main()
{
    Derive d;
        //下面两种方法都可以,主要思想就是将头四个字节/八个字节的数据取出来,然后将传递过去
    func4((VF_prt*)*(int*)&d);//32位下可以,64位下不行
    cout << endl;
    func4(*(VF_prt**)&d);
    return 0;
}
bosthhe1 commented 1 year ago

image 这里我们需要将2传过去,如果是(VF_prt)&d,就是将1传过去,对方看的的还是d的地址,如果是(VF_prt)(int)&d传过去,对方看的是ptr的地址 这里就是为什么要进行一次解引用的原因,因为我们需要将虚表指针取出来,所以需要将前4个字节的虚表指针拿到,所以解引用的效果就是取到前4个字节,(VF_prt)(int)&d中的(int*)&d这就是拿到前4个字节,然后再强转为函数指针传过去

bosthhe1 commented 1 year ago

需要注意的是,构造函数不能是虚函数,构造函数里面的都是该类本身的函数,因为构造函数没有虚函数的概念,且需要体会到虚函数是接口继承

class A1
{
public:
    void foo(){ printf("foo"); }
    virtual void bar(){printf("bar");}
    A1(){ bar(); }//构造函数没有虚函数概念,所以这里的bar(),调用的是A1()自己的bar

};
class B1:public A1
{
    void foo(){ printf("b_foo"); }
    virtual void bar(){printf("b_bar");}
};
int main()
{
    Base1* b = new Derive;
    b->test();
}

image

class Base1 {
public:
    virtual void func1(int a = 1) { cout << a<<"Base1::func1" << endl; }//这里将func1()的接口继承下去a = 1也被继承下去
    virtual void test()
    { func1();}//此时体现的是接口继承
private:
    int b1 = 1;
};
class Derive : public Base1{
public:
    virtual void func1(int a = 0) { cout << a << "Derive::func1" << endl; }//这里的a是被继承下来的1,不是0
private:
    int d1 = 3;
};
int main()
{
    A1* p = new B1;
    p->foo();
    p->bar();
}

image