C++对象模型详解 虚函数详解(二)

2、 虚函数详解

2.1 虚函数表指针的位置

  • 虚函数表指针位于对象内存的起始位置

在这里插入图片描述

// ModelTest2.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

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

class A
{
public:
    int m_a;
    virtual    void testfunc() 
    {

    }
private:

};
using namespace std;

int main()
{
    std::cout << "Hello World!\n";
    A a;
    std::cout << "A 的占内存字节大小:" << sizeof(A) << std::endl;
    char* pl = reinterpret_cast<char* > (&a);    //类型转换,这属于硬转,a是对象首地址
    char* p2 = reinterpret_cast< char* > (&(a.m_a));//
    if (pl == p2)   //说明a.m_a和a位置相同,则成员变量i在a对象内存的上面位置,那么虚函数表//指针在下面位置
    {
        cout << "虚函数表指针位于对象内存的末尾" << endl;
    }
    else
    {
        cout<<"虚函数表指针位于对象内存的开头"<<endl;//本条件会成立
    }
}

在这里插入图片描述

2.2 虚函数如何手动调用

// ModelTest2.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <iostream>
#include <stdio.h>
using namespace std;

class Base
{
public:
    virtual    void f1()
    {
        cout << "Base::f1()" << endl;
    };


    virtual    void f2()
    {
        cout << "Base::f2()" << endl;
    };

    virtual    void f3()
    {
        cout << "Base::f3()" << endl;
    };

private:

};

class MyClass: public    Base
{
public:
    void f2()
    {
        cout << "MyClass::f2()" << endl;
    }

private:

};



typedef void(*Func) (void);

int main()
{
    std::cout << "Hello World!\n";

    cout << "BASE LEN:" << sizeof(Base) << endl;

    cout << "MyClass LEN:" << sizeof(MyClass) << endl;

    Base* p0 = new Base();
    long* pvptr0 = (long*)p0;    //指向对象p0的指针转成1ong*型。大家注意,目前p0对象里只有虚函数表指针
    long* vptr0 = (long*)(*pvptr0);  //(*pvptr)表示pvptr指向的对象,也就是Base 对象本身这个对象4字节, 这个4字节是虚函数表地址。

    for (int i=0; i<4;i++ )
    {
        printf("vptr0[%d] = 0x: %p\r\n", i, vptr0[i]);
    }
    Func f00 = (Func)vptr0[0];    //虚函数表的第一个

    Func f01 = (Func)vptr0[1];    //虚函数表的第二个

    Func f02 = (Func)vptr0[2];    //虚函数表的第三个
    
    Func f03 = (Func)vptr0[3];    //虚函数表的第三个

    f00();
    f01();
    f02();
    //f3();    //异常

    std::cout << "****************************************\n";
    MyClass* p = new MyClass();
    long* pvptr = (long*)p;    //指向对象p的指针转成1ong*型。大家注意,目前p对象里只有虚函数表指针
    long* vptr = (long*)(*pvptr);  //(*pvptr)表示pvptr指向的对象,也就是Derive 对象本身这个对象4字节, 这个4字节是虚函数表地址。

    for (int i = 0; i < 4; i++)
    {
        printf("vptr[%d] = 0x: %p\r\n", i, vptr[i]);
    }
    Func f0 = (Func)vptr[0];    //虚函数表的第一个

    Func f1 = (Func)vptr[1];    //虚函数表的第二个

    Func f2 = (Func)vptr[2];    //虚函数表的第三个

    Func f3 = (Func)vptr[3];    //虚函数表的第三个

    f0();
    f1();
    f2();
}

在这里插入图片描述
在这里插入图片描述

结论点:

  • 包含虚函数的类才有虚函数表,同属一个类的对象共享这个虚函数表,但是每一个对象都有自己的指向虚函数表的指针,指针不同但是指针指向的地址是相同的
  • 父类中存在虚函数,子类一定有虚函数表,不管子类是否重写,不管子类是否去掉重写的 virtual
  • 如果子类中 完全没有新的虚函数, 可以认为父类和子类的虚函数表的内容相同,是内容相同,两个虚函数表的内存位置是不同的, 即两个内容完全相同的表存在不同的位置
  • 超出虚函数表的内存地址未知的 就是和空指针类似
// ModelTest2.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <iostream>
#include <stdio.h>
using namespace std;

class Base
{
public:
    virtual    void f1()
    {
        cout << "Base::f1()" << endl;
    };


    virtual    void f2()
    {
        cout << "Base::f2()" << endl;
    };

    virtual    void f3()
    {
        cout << "Base::f3()" << endl;
    };

private:

};

class MyClass: public    Base
{
public:
    void f2()
    {
        cout << "MyClass::f2()" << endl;
    }

private:

};



typedef void(*Func) (void);

int main()
{
    std::cout << "Hello World!\n";

    cout << "BASE LEN:" << sizeof(Base) << endl;

    cout << "MyClass LEN:" << sizeof(MyClass) << endl;

    MyClass  myclass;
        long* pvptr_v = (long*)(&myclass);    //指向对象p的指针转成1ong*型。大家注意,目前p对象里只有虚函数表指针
        long* vptr_v = (long*)(*pvptr_v);  //(*pvptr_v)表示pvptr指向的对象,也就是Derive 对象本身这个对象4字节, 这个4字节是虚函数表地址。
    
        for (int i = 0; i < 4; i++)
        {
            printf("vptr_v[%d] = 0x: %p\r\n", i, vptr_v[i]);
        }
    Func v0 = (Func)vptr_v[0];    //虚函数表的第一个

    Func v1 = (Func)vptr_v[1];    //虚函数表的第二个

    Func v2 = (Func)vptr_v[2];    //虚函数表的第三个

    Func v3 = (Func)vptr_v[3];    //虚函数表的第三个

        v0();
        v1();
        v2();
    
    MyClass  myclass2 = myclass;
        long* pvptr_v2 = (long*)(&myclass2);
        long* vptr_v2 = (long*)(*pvptr_v2);
        for (int i = 0; i < 4; i++)
        {
            printf("vptr_v2[%d] = 0x: %p\r\n", i, vptr_v2[i]);
        }

    Base  base = myclass;
        long* pvptr_vbase = (long*)(&base);
        long* vptr_vbase = (long*)(*pvptr_vbase);
        for (int i = 0; i < 4; i++)
        {
            printf("vptr_vbase[%d] = 0x: %p\r\n", i, vptr_vbase[i]);
        }


    std::cout << "**************    *************************\n";


}
  • 83行 Base base = myclass; 复制后虚函数的指针变化,①生成一个base的对象 ②使用myclass 初始化base对象,但是并不覆盖base的虚函数指针
    在这里插入图片描述

2.3 多重继承的虚函数表详解

// vfun2.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。

#include <iostream>
#include <stdio.h>
using namespace std;

class Base1
{
public:
    virtual    void f1()
    {
        cout << "Base1::f1()" << endl;
    };


    virtual    void f2()
    {
        cout << "Base1::f2()" << endl;
    };

    //virtual    void f3()
    //{
    //    cout << "Base1::f3()" << endl;
    //};

private:

};

class Base2
{
public:
    virtual    void f4()
    {
        cout << "Base2::f4()" << endl;
    };


    virtual    void f5()
    {
        cout << "Base2::f5()" << endl;
    };

private:

};


class MyClass : public    Base1,public Base2
{
public:
    virtual void f1()
    {
        cout << "MyClass::f1()" << endl;
    }

    virtual void f5()
    {
        cout << "MyClass::f5()" << endl;
    }


    virtual void mf1()
    {
        cout << "MyClass::mf1()" << endl;
    }
    virtual void mf2()
    {
        cout << "MyClass::mf2()" << endl;
    }
    virtual void mf3()
    {
        cout << "MyClass::mf3()" << endl;
    }


private:

};



typedef void(*Func) (void);

int main()
{
    std::cout << "Hello World!\n";

    cout << "BASE1 LEN:" << sizeof(Base1) << endl;
    cout << "BASE2 LEN:" << sizeof(Base2) << endl;

    cout << "MyClass LEN:" << sizeof(MyClass) << endl;

    MyClass  myclass;

    Base1& b1 = myclass;
    Base2& b2 = myclass;
    MyClass& p = myclass;

    long* pvptr_myclass = (long*)(&myclass);    //指向对象p的指针转成1ong*型。大家注意,目前p对象里只有虚函数表指针
    long* vptr_myclass = (long*)(*pvptr_myclass);  //(*pvptr_v)表示pvptr指向的对象,也就是Derive 对象本身这个对象4字节, 这个4字节是虚函数表地址。

    for (int i = 0; i < 7; i++)
    {
        printf("vptr_myclass[%d] = 0x: %p\r\n", i, vptr_myclass[i]);
    }

    Func v0 = (Func)vptr_myclass[0];    //虚函数表的第一个

    Func v1 = (Func)vptr_myclass[1];    //虚函数表的第二个

    Func v2 = (Func)vptr_myclass[2];    //虚函数表的第三个

    Func v3 = (Func)vptr_myclass[3];    //虚函数表的第三个
    Func v4 = (Func)vptr_myclass[4];    //虚函数表的第三个
    Func v5 = (Func)vptr_myclass[5];    //虚函数表的第三个
    Func v6 = (Func)vptr_myclass[6];    //虚函数表的第三个
    Func v7 = (Func)vptr_myclass[7];    //虚函数表的第三个

    v0();
    v1();
    v2();
    v3();
    v4();
    //v5();
    //v6();
    //v7();
    cout << "******************" << endl;

    long* pvptr_2 = pvptr_myclass + 1;   //这里是往后移动是个字节幺
    long* vptr_2 = (long*)(*pvptr_2);

    for (int i = 0; i < 3; i++)
    {
        printf("vptr_2[%d] = 0x: %p\r\n", i, vptr_2[i]);
    }
    Func f0 = (Func)vptr_2[0];    //虚函数表的第一个

    Func f1 = (Func)vptr_2[1];    //虚函数表的第二个

    Func f2 = (Func)vptr_2[2];    //虚函数表的第三个

    f0();
    f1();
    //f2();
    cout << "*********22222*********" << endl;

    b1.f1();
    b1.f2();

    b2.f4();

    p.mf1();
    p.mf2();
    p.mf3();
    cout << "*********22222*********" << endl;
    myclass.f1();
    myclass.f5();
    myclass.mf1();
}

上面的虚函数的内存分布大致如下:

在这里插入图片描述

在这里插入图片描述

  • 一个对象继承多个多个基类,则可能出现多个虚函数表指针 是虚函数表指针,不是虚函数表
  • 在多重继承的情况下,各个基类的vptr是按照继承顺序依次放到空间的, 并且子类的虚函数表和第一个基类共用一个vptr。
  • 子类中的虚函数会覆盖父类中的同名函数
    可以工具查看类的内存分布:
    (1)区分cl和d1,一个是小写字母1,一个是数字1,两者看上去比较像。
    (2)reportSingleClassLayout后面跟的是要查看的类名,
    (3)最后面跟的是要查看的类所在的.cpp源文件名。
    使用VS 的命令行可以查看
cl /d1 reportSingleClassLayout类名 xxxx.cpp

在这里插入图片描述

注意上图中的this adjustor ** 这个是this 指针在使用中的调整

  • 虚函数表指针是什么时候创建的
    虚函数表指针(vptr)是跟着对象走的,对象创建创建出来才有 虚函数表指针(vptr) 对有虚函数的类,在编译的时候编译器就会在构造函数中添加vptr的赋值语句。
  • 虚函数表(vtbl)是什么时候创建的
    编译器在编译的时候就为每个类确定好了对应虚函数表的内容,然后也是在编译期间在相应的类构造函数中添加给vptr赋值的语句,这样程序运行的时
    候,当运行到生成类对象的代码时,会调用类的构造函数,执行到类的构造函数中的给vptr 赋值的语句时,这个类对象的vptr就有值了。

2.4、虚基类探讨

定义:在 C++ 中,虚基类(Virtual Base Class)是通过虚继承(Virtual Inheritance)来定义的。虚继承使用 virtual 关键字来确保在多重继承中只有一个基类的实例。这有助于解决钻石继承问题(Diamond Problem)。

2.4.1定义虚基类

虚基类的定义涉及到以下几个方面:
1.虚继承的语法:在继承基类时使用 virtual 关键字。
2.构造函数的调用顺序:虚基类的构造函数会在最开始被调用。
3.内存布局:虚基类的对象在派生类中只占用一次内存空间。

#include <iostream>

class Base {
public:
    Base(int v) : value(v) {
        std::cout << "Base constructor called with value: " << value << std::endl;
    }
    int value;
};

class Derived1 : virtual public Base {
public:
    Derived1(int v) : Base(v), derived1Value(v * 2) {
        std::cout << "Derived1 constructor called with value: " << derived1Value << std::endl;
    }
    int derived1Value;
};

class Derived2 : virtual public Base {
public:
    Derived2(int v) : Base(v), derived2Value(v * 3) {
        std::cout << "Derived2 constructor called with value: " << derived2Value << std::endl;
    }
    int derived2Value;
};

class FinalDerived : public Derived1, public Derived2 {
public:
    FinalDerived(int v) : Base(v), Derived1(v), Derived2(v), finalDerivedValue(v * 4) {
        std::cout << "FinalDerived constructor called with value: " << finalDerivedValue << std::endl;
    }
    int finalDerivedValue;
};

int main() {
    FinalDerived obj(10);
    return 0;
}

2.4.2虚基表的内存布局
假设 sizeof(int) 为 4 字节,sizeof(void*) 为 4字节,那么 FinalDerived 对象的内存布局可能如下:

深色版本FinalDerived 对象:
+----------------+----------------+----------------+----------------+----------------+----------------+
|  Vbptr         |  derived1Value |  derived2Value |  finalDerivedValue |  Base::value  |
+----------------+----------------+----------------+----------------+----------------+----------------+
 4 bytes          4 bytes          4 bytes          4 bytes            4 bytes
  • Vbptr:指向虚基表的指针。
  • derived1Value:Derived1 的成员变量。
  • derived2Value:Derived2 的成员变量。
  • finalDerivedValue:FinalDerived 的成员变量。
  • Base::value:Base 的成员变量。
    2.4.3虚基表的内容
    假设虚基表的内容如下:
    虚基表通常包含以下信息:
    1.虚基类的偏移量:记录虚基类相对于派生类对象的偏移量。
    2.虚基类的构造函数地址:用于在构造派生类对象时调用虚基类的构造函数。
    3.虚基类的析构函数地址:用于在销毁派生类对象时调用虚基类的析构函数
Vbtable:
+----------------+----------------+----------------+----------------+
|  Offset to Base |  Constructor   |  Destructor    |  Other Entries |
+----------------+----------------+----------------+----------------+
  8 bytes          Function Pointer  Function Pointer  ...
  • Offset to Base:记录 Base 类相对于 FinalDerived 对象的偏移量。
  • Constructor:指向 Base 类的构造函数。
  • Destructor:指向 Base 类的析构函数。
  • Other Entries:可能包含其他辅助信息。
实测代码
// VirtualLayout.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <iostream>

using namespace std;

class Grand
{
public:
    //int m_Grand;
private:

};

class A1: virtual public Grand
{
public:


private:

};

class A2 : virtual public Grand
{
public:


private:

};

class C1 : public A1,public A2
{
public:


private:

};



int main()
{
    std::cout << "Hello World!\n";

    cout << "Grand sizeof :" << sizeof(Grand) << endl;

    cout << "A1 sizeof :" << sizeof(A1) << endl;

    cout << "A2 sizeof :" << sizeof(A2) << endl;
    cout << "C1 sizeof :" << sizeof(C1) << endl;
}

在这里插入图片描述

因为一个空类的大小为1,所有 grand大小为1, 但是后面A1、A2的为什么不是呢?是应为引入了虚基类表指针。
使用类的布局神器看一下 cl /d1 reportSingleClassLayout类名 xxxx.cpp

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

添加一个成员变量更容易看

// VirtualLayout.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <iostream>

using namespace std;

class Grand
{
public:
    int m_Grand;
private:

};

class A1: virtual public Grand
{
public:
    int m_a1;

private:

};

class A2 : virtual public Grand
{
public:
    int m_a2;

private:

};

class C1 : public A1,public A2
{
public:
    int m_C1;

private:

};





int main()
{
    std::cout << "Hello World!\n";

    cout << "Grand sizeof :" << sizeof(Grand) << endl;

    cout << "A1 sizeof :" << sizeof(A1) << endl;

    cout << "A2 sizeof :" << sizeof(A2) << endl;
    cout << "C1 sizeof :" << sizeof(C1) << endl;
}

在这里插入图片描述

Grand的内存布局

在这里插入图片描述

A的内存布局
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

2.4.4 虚基类表内容详解(0~8字节)

// VirtualLayout.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <iostream>

using namespace std;

class Grand
{
public:
    int m_Grand;
private:

};

class A1: virtual public Grand
{
public:
    int m_a1;

private:

};

class A2 : virtual public Grand
{
public:
    int m_a2;

private:

};

class C1 : public A1,public A2
{
public:
    int m_C1;

private:

};





int main()
{
    std::cout << "Hello World!\n";

    cout << "Grand sizeof :" << sizeof(Grand) << endl;

    cout << "A1 sizeof :" << sizeof(A1) << endl;

    cout << "A2 sizeof :" << sizeof(A2) << endl;
    cout << "C1 sizeof :" << sizeof(C1) << endl;

    A1  a;
    a.m_Grand = 10;
    a.m_a1 = 16;
}

在这里插入图片描述

a.m_grand = 10 赋值
在这里插入图片描述
在这里插入图片描述

ECX 的值是8 和 00759D20 内存后面的直接一样 , 并且内存中也是在&a偏移8字节后内容变化。
并且虚基类表的5~ 8字节内容的含义是: 虚基类表指针 这个成员变量(vbptr)的首地址和虚基类子对象首地址之间的偏移量。

2.4、函数的静态绑定和动态绑定

2.4.1 静态绑定

在 C++ 中,静态绑定(Static Binding)和动态绑定(Dynamic Binding)是两种不同的函数调用机制。它们决定了函数调用的解析时间和方式。下面是这两种绑定的详细解释和示例。

  1. 静态绑定(Static Binding)
    静态绑定,也称为早期绑定(Early Binding),是指在编译时确定函数调用的具体实现。编译器在编译阶段就已经知道要调用哪个函数,因此可以在生成的机器码中直接插入函数地址。
    特点
  • 编译时解析:函数调用的解析在编译时完成。
  • 性能较高:由于编译器可以直接生成调用特定函数的代码,因此执行效率较高。
  • 不支持多态:静态绑定不支持运行时多态
  1. 动态绑定(Dynamic Binding)
    动态绑定,也称为晚期绑定(Late Binding),是指在运行时确定函数调用的具体实现。编译器生成的代码在运行时通过虚函数表(Vtable)来查找并调用相应的函数。
    特点
  • 运行时解析:函数调用的解析在运行时完成。
  • 支持多态:动态绑定支持运行时多态,即通过基类指针或引用调用派生类的函数。
  • 性能较低:由于需要在运行时通过虚函数表查找函数地址,因此执行效率略低于静态绑定。
    2.4.2 继承非虚函数注意事项
// classfunBugs.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <iostream>

using namespace std;

class Base1
{
public:
    void f1()
    {
        cout << "Base1::f1()" << endl;
    };

private:

};

class class1: public Base1
{
public:
    void f1()
    {
        cout << "class1::f1()" << endl;
    };

private:

};



int main()
{
    std::cout << "Hello World!\n";
    class1 mclass1;
    class1* p = &mclass1;
    p->f1();

    Base1* pb = &mclass1;
    pb->f1();

}

在这里插入图片描述

注意: 函数是静态绑定 直接执行的是基类的函数

// classfunBugs.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <iostream>

using namespace std;

class Base1
{
public:
    void f1()
    {
        cout << "Base1::f1()" << endl;
    };
    virtual void vf1()
    {
        cout << "Base1::vf1()" << endl;

    }
    /// <summary>
    /// 带有默认参数
    /// </summary>
    virtual void vf2(int value = 1)
    {
        cout << "Base1::vf2(),value:" <<value<< endl;

    }

private:

};

class class1: public Base1
{
public:
    void f1()
    {
        cout << "class1::f1()" << endl;
    };
    virtual void vf1()
    {
        cout << "class1::vf1()" << endl;

    }
    virtual void vf2(int value = 10)
    {
        cout << "class1::vf2(),value:" << value << endl;

    }
private:

};



int main()
{
    std::cout << "Hello World!\n";
    class1 mclass1;
    class1* p = &mclass1;
    p->f1();

    Base1* pb = &mclass1;
    pb->f1();

    cout << "!!!!!!!!!!!\r\n" << endl;
    p->vf1();
    pb->vf1();
    p->vf2();
    pb->vf2();

    cout << "22222222\r\n" << endl;
    Base1 base1;
    pb = &base1;
    pb->vf1();
    pb->vf2();
}

在这里插入图片描述

虚函数时,函数调用时动态绑定,基类指针会调用到子类的函数,但是默认缺省值使用的缺失父类的。 原因是 默认参数值编译器是静态比绑定上去的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值