C ++中的虚函数困境

时间:2011-10-04 15:17:57

标签: c++

我有两个问题要问......

a)

Class A{
int a;
public:
virtual void f(){}
};

Class B {
int b;
public:
virtual void f1(){}
};

Class C: public A, public B {

int c;
public:
virtual void f(){} // Virtual is optional here
virtual void f1(){} // Virtual is optional here

virtual void f2(){}

};

Class D: public C {
int d;
public:
void f2(){}

};

现在C ++说在C的实例中不会有3个虚拟指针但只有2个。然后,如何调用说,

C* c = new D();

c->f2(); //由于没有与f2()中定义的虚函数对应的虚拟指针。后期绑定是如何完成的?...

我读到说,这个函数的虚拟指针被添加到第一个超类C的虚拟指针中。为什么会这样?...为什么没有虚拟表?...

的sizeof(* C); //这将是24而不是28 ..为什么?...

另外说,考虑到上面的代码,我这样做,

void (C::*a)() = &C::f;
void (C::*b)() = &C::f1;

printf("%u", a); 
printf("%u",b);

// Both the above printf() statements print the same address. Why is that so ?...
// Now consider this,

C* c1 = new C();

c1->(*a)();

c1->(*b)();

//尽管a和b具有相同的地址,但调用的函数是不同的。这个函数的定义如何在这里界定?...

希望我能尽快得到回复。

4 个答案:

答案 0 :(得分:2)

C ++标准没有提及虚拟表,因此编译器可以以任何方式自由地优化它。在这种情况下,似乎已将C的vtable与其中一个vtable合并,但这当然不是必需的。 需要的是,如果你这样做:

C* c = new D();
c->f2();

它调用D::f2,因为它在C中是虚拟的。

不允许将成员函数指针转换为void*,更不用说unsigned所以毫不奇怪它们可能无法在printf中以预期的方式打印(只读取要打印的原始字节)。原因是使用%u,你对printf撒谎,告诉它在你实际传递一个完全不是int的参数时打印一个int。换句话说,ab成员函数指针实际上是不同的,尽管printf似乎告诉你。由于它们真的不同,因此它们正常工作并不奇怪。

如果你想尝试打印编译器给你的真实函数指针,那么“最便携”的方法是memcpy将它变成unsigned char的向量然后打印出来。冗长的例子:

#include <iostream>
#include <vector>

class Foo
{
public:
    virtual void f1() { }
    virtual void f2() { }
    void f3() { }
};

int main()
{
    void (Foo::*a)() = &Foo::f1;
    void (Foo::*b)() = &Foo::f2;
    void (Foo::*c)() = &Foo::f3;

    std::cout << a <<std::endl;
    std::cout << sizeof(a) << std::endl;

    std::cout << b <<std::endl;
    std::cout << sizeof(b) << std::endl;

    std::cout << c <<std::endl;
    std::cout << sizeof(c) << std::endl;

    std::vector<unsigned char> a_vec(sizeof(a));
    memcpy(&a_vec[0], &a, sizeof(a));
    for(size_t i = 0; i < sizeof(a); ++i)
    {
        std::cout << std::hex << static_cast<unsigned>(a_vec[i]) << " ";
    }
    std::cout << std::endl;

    std::vector<unsigned char> b_vec(sizeof(b));
    memcpy(&b_vec[0], &b, sizeof(b));
    for(size_t i = 0; i < sizeof(b); ++i)
    {
        std::cout << std::hex << static_cast<unsigned>(b_vec[i]) << " ";
    }
    std::cout << std::endl;

    std::vector<unsigned char> c_vec(sizeof(c));
    memcpy(&c_vec[0], &c, sizeof(c));
    for(size_t i = 0; i < sizeof(c); ++i)
    {
        std::cout << std::hex << static_cast<unsigned>(c_vec[i]) << " ";
    }
    std::cout << std::endl;

    return 0;
}

在g ++ 4.2上,这会产生:

1
8
1
8
1
8
1 0 0 0 0 0 0 0
5 0 0 0 0 0 0 0
c6 1d 5 8 0 0 0 0

你可以清楚地看到,所有三个成员函数指针都是不同的。

答案 1 :(得分:1)

C的vtable通常与其超类之一(AB)的vtable合并作为优化。但你不应该依赖于此。

答案 2 :(得分:1)

如果您想了解幕后发生的事情,那么这是一个很好的阅读:Inside the C++ Object Model, de Stanley Lippman。内容开始显示其年龄,但它提供了一些全面的演示,这些技术曾经(有时仍然)用于实现C ++特性,如继承,多态,模板等。

现在,回答您的问题:首先,您应该知道供应商必须实现给定功能的方式通常不是由C ++标准指定的。这就是这种情况:根本不需要实现虚拟方法表(即使它们经常使用)。

话虽这么说,我们仍然可以尝试猜测这里发生了什么。首先,让我们看看如果我们创建了一个A实例,内存会是什么样的:

A someA;
    ________________               ----------------                  
    | @A_vtable    | vptr -------->|     @A::f    |                   
    ________________               ----------------                  
    | [some value] | a             A_vtable
    ________________
    someA

除了成员变量之外,您还可以看到A的实例包含虚拟表指针(vptr)。此vptr指向A的虚拟表,其中包含A f实现的地址。

B的实例应该非常相似,所以我不打算画一个。现在让我们看看C实例的样子:

C someC;
    ________________         ------->----------------                  
    | @C_A_vtable  | A_vptr /        |     @C::f    |                   
    ________________                 ----------------                  
    | [some value] | a               |     @C::f2   |
    ----------------                 ---------------- 
    | @C_B_vtable  | B_vptr \         C_A_vtable
    ________________         \         
    | [some value] | b        \
    ________________           \      
    someC                       ---->----------------
                                     |     @C::f1   |
                                     ----------------
                                     C_B_vtable

您可以看到someC包含A部分和B部分,两者都包含vptr。这样,我们就可以通过在类中使用偏移量将C转换为AB。现在,关于C添加的方法,您会注意到我将其地址放在vtable的现有A的末尾:而不是创建一个全新的表,这将需要一个额外的vptr,我只是扩展了现有的一个。对f2的调用只会获取A_vptr指向的表中的好地址,并以与其他虚拟方法完全相似的方式调用它。

D的实例只需将两个vptr设置为指向正确的表(一个包含C::f的地址(因为f未被覆盖)和D::f2,另一个包含C::f1的地址。

答案 3 :(得分:0)

以下是我的Visual C ++ 2010如何在内存中列出这些类的对象:

object_a    {a=-858993460 } A
    __vfptr 0x009d5740 const A::`vftable'   *
        [0] 0x009d11f9 A::f(void)   *
    a   -858993460  int

object_b    {b=-858993460 } B
    __vfptr 0x009d574c const B::`vftable'   *
        [0] 0x009d1203 B::f1(void)  *
    b   -858993460  int

object_c    {c=-858993460 } C
    A   {a=-858993460 } A
        __vfptr 0x009d5764 const C::`vftable'{for `A'}  *
            [0] 0x009d108c C::f(void)   *
        a   -858993460  int
    B   {b=-858993460 } B
        __vfptr 0x009d5758 const C::`vftable'{for `B'}  *
            [0] 0x009d10a5 C::f1(void)  *
        b   -858993460  int
    c   -858993460  int

object_d    {d=-858993460 } D
    C   {c=-858993460 } C
        A   {a=-858993460 } A
            __vfptr 0x009d5780 const D::`vftable'{for `A'}  *
                [0] 0x009d108c C::f(void)   *
        a   -858993460  int
        B   {b=-858993460 } B
            __vfptr 0x009d5774 const D::`vftable'{for `B'}  *
                [0] 0x009d10a5 C::f1(void)  *
            b   -858993460  int
        c   -858993460  int
    d   -858993460  int

如您所见,多重继承为每个类型生成多个虚拟表,并且每个对象生成多个虚拟表指针。

基于此,您的问题的答案如下:


c->f2(); // Since there is no virtual pointer corresponding to the virtual function defined in f2(). How is the late binding done ?.

编译器知道C的布局,因此它知道使用第二个__vfptr以及C::f1在该表中的偏移量。


sizeof(*c); // It would be 24 and not 28.. Why ?...

在我的系统上(32位版本中):

sizeof(C)
    == sizeof(__vfptr) + sizeof(a) + sizeof(__vfptr) + sizeof(b) + sizeof(c)
    == 4 + 4 + 4 + 4 + 4
    == 20

显然,你的编译器做了不同的事情。


void (C::*a)() = &C::f;
void (C::*b)() = &C::f1;

printf("%u", a); 
printf("%u", b);

// Both the above printf() statements print the same address. Why is that so ?...

因为它们是成员函数指针,而不是普通的函数指针。实施细节各不相同,但这些可能是小结构甚至是thunk。显然,在这种情况下,两个函数调用都被相同的结构或thunk“覆盖”,但是成员指针的单独“部分”通过printf不可见,并且在a之间不同和b

请记住,所有这些都是一个实现细节,您永远不应该编写依赖它的代码。