【问题标题】:Trouble with understanding virtual functions难以理解虚函数
【发布时间】:2013-07-31 03:22:47
【问题描述】:
#include<iostream>

using namespace std;

class X
{
    int a;
    int b;
    public:
    void f(int a)
    {
        cout<<"\nInside X";
    }


    virtual void abc ()
    {
        cout<<"\nHello X";
    }

};


class Y : public X
{
    int a;
    public:

    void f(int a, int b)
    {
        cout<<"\nInside Y";
    }


    void abc()
    {
        cout<<"\nHello Y";
    }

};



int main()
{

    X a;

    cout<<sizeof(X);

    Y b;

    cout<<sizeof(Y);

    X *h = new Y;

    h->abc();
}

我知道类 X 的大小为 12 字节的原因是因为它包含指向虚拟表的 vptr(虚拟指针)。无论如何我可以读取这个虚拟表,如果没有,至少可以访问虚拟指针。我尝试使用联合,但它给了我一些错误。

另外,当我调用 h->abc() 时,它如何知道 h 指向的类对象?我认为这大部分是在编译时完成的。但是当你有一个指向派生类的基类指针时,它怎么知道要执行哪个类函数。 考虑这两种情况

X *h = new X;
h->abc();/* This would call the abc function in X */

X *h = new Y;
h->abc();/* This would call the abc function in Y*/

我读到,h 指针将转到它所指向的对象的 vtable 并因此调用该函数?但这在运行时是如何实现的呢?

【问题讨论】:

  • 我将编辑最后一个问题,因为它不符合讨论的主题。但我相信前两个问题完全在本次讨论的范围内。

标签: c++


【解决方案1】:

好的,你的第一个问题:我给你一个例子,可能会更好理解!

#include<iostream>
using namespace std;
class Base1 {

    public:

        int ibase1;

        Base1():ibase1(10) {}

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

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

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

class Base2 {

    public:

        int ibase2;

        Base2():ibase2(20) {}

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

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

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

};

class Base3 {

    public:

        int ibase3;

        Base3():ibase3(30) {}

        virtual void f() { cout << "Base3::f()" << endl; }

        virtual void g() { cout << "Base3::g()" << endl; }

        virtual void h() { cout << "Base3::h()" << endl; }
};

class Derive : public Base1, public Base2, public Base3 {

    public:

        int iderive;

        Derive():iderive(100) {}

        virtual void f() { cout << "Derive::f()" << endl; }

        virtual void g1() { cout << "Derive::g1()" << endl; }

};

这是实现三个基类 base1、base2、base3 的派生类的内存等级,您在其中:

Base1 *p1 = new Derive();
Base2 *p2 = new Derive();
Base3 *p3 = new Derive();

p1 会指向 vtale1,p2 会指向 vtable2,p3 会指向 vtable3,如果你调用一些虚函数,它会找到非常虚的表并获取地址!

在您的代码中:

X *h = new Y;

h会指向Y的内存起始位置,也就是X的虚拟表,他会找到在Y中实现的abc()的地址!

你的第二个问题:

编译器会将成员函数视为普通函数,所以将成员函数的地址放在code section中,这样就不会占用内存了!!

如果你想读取虚拟表,你可以这样尝试:我在 gcc4.7 的示例中尝试过

typedef void(*Func)(void);
    Derive d;
    int **pd = (int **)(&d);
    int i = 0;
    while(i < 4)
    {
        Func f = (Func)pd[0][i];
        f();
        i++;
    }
    int s = (int)(pd[1]);
    cout << s << endl;
    i = 0;
    cout << "===============================================" << endl;
    while(i < 3)
    {
        Func f = (Func)pd[2][i];
        f();
        i++;
    }
    s = (int)(pd[3]);
    cout << s << endl;
    cout << "===============================================" << endl;
    i = 0;
    while(i < 3)
    {
        Func f = (Func)pd[4][i];
        f();
        i++;
    }
    s = (int)(pd[5]);
    cout << s << endl;
    s = (int)(pd[6]);
cout << s << endl;

你会得到如下结果:

 Derive::f()
Base1::g()
Base1::h()
Derive::g1()
10
===============================================
Derive::f()
Base2::g()
Base2::h()
20
===============================================
Derive::f()
Base3::g()
Base3::h()
30
100

【讨论】:

    【解决方案2】:
    1. 除非您确定自己在做什么,否则不应尝试访问 vtable 指针。就我们通常用来定义程序含义的语言而言,vtable 甚至不存在。这是一个实现细节,它属于实现(也就是编译器和运行时环境)。

      如果实现受到可移植 ABI(应用程序二进制接口)的限制,那么 ABI 将说明在哪里可以找到 vtable 指针以及 vtable 中的内容。 reinterpret_cast&lt; vtable const * const &amp; &gt;( my_obj ) 应该能够从任何“合理”ABI 上的对象中获取指针。

      这样的程序将受限于一个平台和一个 ABI。 (C++ ABI 接口的变化往往比 C 多,但比其他语言少。)依赖 ABI 是一个糟糕的设计选择,除非你只是想证明你疯了。

    2. vtable 标识派生类——这就是它的用途。它包含指向由覆盖基类的派生类实现的函数的指针。它还包含一个带有派生类名称的结构并链接到它的基类,以便动态确定它是从什么派生的。

      dynamic_cast 用于确定派生和查找派生对象的算法实际上可能非常慢——而不是 O(1)。它通常必须搜索基类的链接结构。

    【讨论】:

      【解决方案3】:

      反正我可以读这个虚拟表

      不是真的,不是不知道指针相对于对象指针值的位置,这是编译器特定的。

      如果没有,至少可以访问虚拟指针。

      为什么?你可以通过h-&gt;abc获取函数的地址,是你想要的吗?

      另外,当我调用 h->abc() 时,它如何知道 h 指向的类的对象?

      它不是真的,它只是知道那个类的 vtable 在哪里。如果您使用 RTTI,则 vtable 中有信息告诉它是什么类,但调用虚函数不是必需的。从 X 派生的每个类都有自己的 vtable,其中包含自己的指针,用于自己的虚函数。 (当然总是假设基于 vtable 的实现。)

      我读到,h 指针会指向它所指向的对象的 vtable,因此会调用该函数?但这在运行时是如何实现的呢?

      你自己已经描述过了。稍微详细说明一下,指针h-&gt;abc 解析为h-&gt;_vtable[x] 的某个常量x,表示abc 的虚函数指针在vtable 中的偏移量。所以调用解析为*(h-&gt;_vtable[abc])(...)

      另一个问题,与我需要澄清的虚函数无关。如果函数像任何其他变量一样具有地址,为什么它们不占用对象中的空间?

      他们为什么要这样做?这意味着每个对象中每个函数的副本。有什么意义?

      【讨论】:

        猜你喜欢
        • 2014-05-16
        • 1970-01-01
        • 2016-01-05
        • 1970-01-01
        • 1970-01-01
        • 2021-01-17
        • 2016-02-22
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多