繁体   English   中英

指向成员函数的指针如何工作?

[英]How do pointers to member functions work?

我知道普通函数指针包含要指向的函数的起始地址,因此当使用普通函数指针时,我们只需跳转到存储的地址即可。 但是指向对象成员函数的指针包含什么?

考虑:

class A
{
public:
    int func1(int v) {
        std::cout << "fun1";
        return v;
    }
    virtual int func2(int v) {
        std::cout << "fun2";
        return v;
    }
};

int main(int argc, char** argv)
{
    A a;
    int (A::*pf)(int a) = argc > 2 ? &A::func1 : &A::func2;
    static_assert(sizeof(pf) == (sizeof(void*), "Unexpected function size");
    return (a.*pf)(argc);
}

在上述程序中,函数指针可以从一个虚函数(即需要通过虚表被访问)或一个普通的类构件采取其值(即实现为具有一个隐含一个正常功能this作为第一个参数。)

那么,在指向成员函数的指针中存储的值是多少?编译器如何使事情按预期工作?

当然,这取决于编译器和目标体系结构,并且有多种方法可以实现。 但是,我将描述它如何在我最常使用的系统上(在Linux x86_64上使用g ++)工作。

g ++遵循Itanium C ++ ABI ,它描述了许多架构可以在幕后实现各种C ++功能(包括虚拟功能)的一种方式的许多细节。

ABI在第2.3节中说明了有关成员函数的指针:

指向成员函数的指针是一对,如下所示:

ptr

对于非虚拟函数,此字段是简单函数指针。 ...对于虚拟函数,它是1加上函数的虚拟表偏移量(以字节为单位),表示为ptrdiff_t 零值表示NULL指针,与下面的调整字段值无关。

调整

this所需的调整,表示为ptrdiff_t

它具有大小,数据大小以及包含该两个成员的类的顺序(该顺序)。

虚拟函数的+1到ptr有助于检测该函数是否是虚拟的,因为在大多数平台上,所有函数指针值和vtable偏移量都是偶数。 它还可以确保空成员函数指针具有与任何有效成员函数指针不同的值。

您的A类的vtable / vptr设置将类似于以下C代码:

struct A__virt_funcs {
    int (*func2)(A*, int);
};

struct A__vtable {
    ptrdiff_t offset_to_top;
    const std__typeinfo* typeinfo;
    struct A__virt_funcs funcs;
};

struct A {
    const struct A__virt_funcs* vptr;
};

int A__func1(struct A*, int v) {
    std__operator__ltlt(&std__cout, "fun1");
    return v;
}

int A__func2(struct A*, int v) {
    std__operator__ltlt(&std__cout, "fun2");
    return v;
}

extern const std__typeinfo A__typeinfo;

const struct A__vtable vt_for_A = { 0, &A__typeinfo, { &A__func2 } };

void A__initialize(A* a) {
    a->vptr = &vt_for_A.funcs;
}

(是的,实名处理方案需要对函数参数类型进行某些操作以允许重载,并且还要做更多的事情,因为涉及到的operator<<实际上是函数模板的专门化。但这不重要。)

现在,让我们看看为您的main()获得的程序集(带有-O0 -fno-stack-protector选项)。 我的评论已添加。

Dump of assembler code for function main:
     // Standard stack adjustment for function setup.
   0x00000000004007e6 <+0>: push   %rbp
   0x00000000004007e7 <+1>: mov    %rsp,%rbp
   0x00000000004007ea <+4>: push   %rbx
   0x00000000004007eb <+5>: sub    $0x38,%rsp
     // Put argc in the stack at %rbp-0x34.
   0x00000000004007ef <+9>: mov    %edi,-0x34(%rbp)
     // Put argv in the stack at %rbp-0x40.
   0x00000000004007f2 <+12>:    mov    %rsi,-0x40(%rbp)
     // Construct "a" on the stack at %rbp-0x20.
     // 0x4009c0 is &vt_for_A.funcs.
   0x00000000004007f6 <+16>:    mov    $0x4009c0,%esi
   0x00000000004007fb <+21>:    mov    %rsi,-0x20(%rbp)
     // Check if argc is more than 2.
     // In both cases, "pf" will be on the stack at %rbp-0x30.
   0x00000000004007ff <+25>:    cmpl   $0x2,-0x34(%rbp)
   0x0000000000400803 <+29>:    jle    0x400819 <main+51>
     // if (argc <= 2) {
     //   Initialize pf to { &A__func2, 0 }.
   0x0000000000400805 <+31>:    mov    $0x4008ce,%ecx
   0x000000000040080a <+36>:    mov    $0x0,%ebx
   0x000000000040080f <+41>:    mov    %rcx,-0x30(%rbp)
   0x0000000000400813 <+45>:    mov    %rbx,-0x28(%rbp)
   0x0000000000400817 <+49>:    jmp    0x40082b <main+69>
     // } else { [argc > 2]
     //   Initialize pf to { 1, 0 }.
   0x0000000000400819 <+51>:    mov    $0x1,%eax
   0x000000000040081e <+56>:    mov    $0x0,%edx
   0x0000000000400823 <+61>:    mov    %rax,-0x30(%rbp)
   0x0000000000400827 <+65>:    mov    %rdx,-0x28(%rbp)
     // }
     // Test whether pf.ptr is even or odd:
   0x000000000040082b <+69>:    mov    -0x30(%rbp),%rax
   0x000000000040082f <+73>:    and    $0x1,%eax
   0x0000000000400832 <+76>:    test   %rax,%rax
   0x0000000000400835 <+79>:    jne    0x40083d <main+87>
     // int (*funcaddr)(A*, int); [will be in %rax]
     // if (is_even(pf.ptr)) {
     //   Just do:
     //   funcaddr = pf.ptr;
   0x0000000000400837 <+81>:    mov    -0x30(%rbp),%rax
   0x000000000040083b <+85>:    jmp    0x40085c <main+118>
     // } else { [is_odd(pf.ptr)]
     //   Compute A* a2 = (A*)((char*)&a + pf.adj); [in %rax]
   0x000000000040083d <+87>:    mov    -0x28(%rbp),%rax
   0x0000000000400841 <+91>:    mov    %rax,%rdx
   0x0000000000400844 <+94>:    lea    -0x20(%rbp),%rax
   0x0000000000400848 <+98>:    add    %rdx,%rax
     //   Compute funcaddr =
     //     (int(*)(A*,int)) (((char*)(a2->vptr))[pf.ptr-1]);
   0x000000000040084b <+101>:   mov    (%rax),%rax
   0x000000000040084e <+104>:   mov    -0x30(%rbp),%rdx
   0x0000000000400852 <+108>:   sub    $0x1,%rdx
   0x0000000000400856 <+112>:   add    %rdx,%rax
   0x0000000000400859 <+115>:   mov    (%rax),%rax
     // }
     // Compute A* a3 = (A*)((char*)&a + pf.adj); [in %rcx]
   0x000000000040085c <+118>:   mov    -0x28(%rbp),%rdx
   0x0000000000400860 <+122>:   mov    %rdx,%rcx
   0x0000000000400863 <+125>:   lea    -0x20(%rbp),%rdx
   0x0000000000400867 <+129>:   add    %rdx,%rcx
     // Call int r = (*funcaddr)(a3, argc);
   0x000000000040086a <+132>:   mov    -0x34(%rbp),%edx
   0x000000000040086d <+135>:   mov    %edx,%esi
   0x000000000040086f <+137>:   mov    %rcx,%rdi
   0x0000000000400872 <+140>:   callq  *%rax
     // Standard stack cleanup for function exit.
   0x0000000000400874 <+142>:   add    $0x38,%rsp
   0x0000000000400878 <+146>:   pop    %rbx
   0x0000000000400879 <+147>:   pop    %rbp
     // Return r.
   0x000000000040087a <+148>:   retq   
End of assembler dump.

但是,成员函数指针的adj值如何处理? 该组件添加到它的地址, a做虚函数表查找之前,也调用该函数,该函数是否是虚拟的或不前。 但是main情况下,这两种情况都将其设置为零,因此我们还没有真正看到它在起作用。

当我们有多个继承时, adj值就会出现。 现在假设我们有:

class B
{
public:
    virtual void func3() {}
    int n;
};

class C : public B, public A
{
public:
    int func4(int v) { return v; }
    int func2(int v) override { return v; }
};

类型为C的对象的布局包含一个B子对象(包含另一个vptr和一个int ),然后是一个A子对象。 因此,包含在C中的A的地址与C本身的地址不同。

您可能已经知道,任何时间代码都会将(非空) C*指针隐式或显式转换为A*指针,因此C ++编译器会通过在地址值中添加正确的偏移量来解决这一差异。 C ++也允许从一个指针转换成的成员函数A的指针的成员函数C (因为任何成员A也是一个构件C ),并且当发生这种情况(为一个非空成员函数的指针),一个需要进行类似的偏移量调整。 因此,如果我们有:

int (A::*pf1)(int) = &A::func1;
int (C::*pf2)(int) = pf1;

pf1 = { &A__func1, 0 };成员函数指针中的值将为pf1 = { &A__func1, 0 }; pf2 = { &A__func1, offset_A_in_C };

然后,如果我们有

C c;
int n = (c.*pf2)(3);

编译器将通过向地址&c添加偏移量pf2.adj来找到对成员函数指针的调用,以找到隐式的“ this”参数,这是很好的,因为这将是A__func1期望的有效A*值。

对于虚拟函数调用,同样的事情,除了反汇编转储所示,需要偏移量以查找隐式的“ this”参数和包含实际功能代码地址的vptr。 虚拟情况增加了一个变化,但普通的虚拟调用和使用指向成员函数的指针的调用都需要这种情况:虚拟函数func2将使用A* “ this”参数进行调用,因为这是原始覆盖的地方声明,并且编译器通常将无法知道“ this”参数是否实际上是任何其他类型。 但是重写C::func2的定义需要一个C* “ this”参数。 因此,当最派生的类型是CA子对象中的vptr将指向一个vtable,该表的入口不指向C::func2本身的代码,而是指向一个微小的“ thunk”函数,该函数除了减去外不执行任何操作从“ this”参数offset_A_in_C ,然后将控制权传递给实际的C::func2

GCC 的文档是保偏光纤实现为那些知道如何计算的价值结构this并进行任何虚函数表查找。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM