C++中的虚拟函数困境

Virtual Functions Dilemma in C++

本文关键字:函数 困境 虚拟 C++      更新时间:2023-10-16

我有两个问题要问。。。

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具有相同的地址,但调用的函数是不同的。函数的定义如何在这里有界?。。。

希望我能尽快得到回复。

C++标准没有提到虚拟表,因此编译器可以自由地以任何方式对其进行优化。在这种情况下,它似乎已经将C的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

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

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

如果你想了解幕后发生的事情,这是一本很好的读物:在C++对象模型内部,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添加的方法,您会注意到,我将其地址放在A的现有vtable的末尾:我没有创建一个需要额外vptr的全新表,而是简单地扩展了现有表。对f2的调用将简单地获取A_vptr指向的表中的好地址,并以与其他虚拟方法完全类似的方式调用它。

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

以下是我的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中不可见,并且在ab之间有所不同。

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