C++调用非虚成员函数的机制是什么?

What is the mechanism of calling nonvirtual member function in C++?

C++ 对象模型不包含任何 table 非虚拟成员函数。当有这样一个函数的调用时

a.my_function();

经过名称修改,它变成了类似

的东西
my_function__5AclassKd(&a)

该对象仅包含数据成员。非虚函数没有 table。那么在这种情况下调用机制如何找出调用哪个函数呢? 引擎盖下发生了什么?

形式上,标准不要求它们以任何特定方式工作,但通常它们的工作方式与普通函数完全一样,但有一个额外的不可见参数:指向调用它们的对象实例的指针。

当然,编译器也许能够优化它,例如如果成员函数不使用 this 或任何需要 this.

的成员变量或成员函数,则不要传递指针

使用非虚函数,无需在运行时确定调用哪个函数;因此生成的机器代码通常看起来与普通函数调用相同,只是 this 有一个额外的参数,如您的示例所示。 (虽然它并不总是相同的 - 例如,我认为 MSVC 编译 32 位程序,至少在某些版本中,在 ECX 寄存器中传递 this 而不是像通常的函数参数那样在堆栈上传递。 )

因此,调用哪个函数是由编译器在编译时决定的。那时,它具有通过解析 class 声明确定的信息,它可以使用这些信息,例如进行方法重载解析,并从那里计算或查找错位的名称以放入汇编代码中。

编译器的工作是将程序所需的数据和代码布置到内存地址中。每个非虚拟函数——无论是成员函数还是非成员函数——都会获得一个 fixed 虚拟内存地址,可以在该地址调用它。然后调用机器代码硬编码要调用的函数的绝对地址(或使用 位置独立代码 调用地址相对偏移量)地址。

例如,假设您的编译器正在编译一个需要 20 字节机器代码的非虚拟成员函数,并将可执行代码放在从偏移量 0x1000 开始的虚拟地址,并且已经为其他函数生成了 10 字节的可执行代码函数,然后它将在虚拟地址 0x100A 处启动该函数的代码。想要调用该函数的代码在将任何函数调用参数(包括指向要操作的对象的 this 指针)压入堆栈后生成 "call 0x100A" 的机器代码。

你可以很容易地看到这一切的发生:

 ~/dev > cat example.cc         
#include <cstdio>

struct X
{
    int f(int n) { return n + 3; }
};

int main()
{
    X x;
    printf("%d\n", x.f(7));
}

~/dev > g++ example.cc -S; c++filt < example.s
    .file   "example.cc"
    .section    .text._ZN1X1fEi,"axG",@progbits,X::f(int),comdat
    .align 2
    .weak   X::f(int)
    .type   X::f(int), @function
X::f(int):    // code to execute X::f(int) starts at label .LFB0
.LFB0:        // when this assembly is covered to machine code
    .cfi_startproc    // it's given a virtual address
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movq    %rdi, -8(%rbp)
    movl    %esi, -12(%rbp)
    movl    -12(%rbp), %eax
    addl    , %eax
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size   X::f(int), .-X::f(int)
    .section    .rodata
.LC0:
    .string "%d\n"
    .text
    .globl  main
    .type   main, @function
main:
.LFB1:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    subq    , %rsp
    movq    %fs:40, %rax
    movq    %rax, -8(%rbp)
    xorl    %eax, %eax
    leaq    -9(%rbp), %rax
    movl    , %esi
    movq    %rax, %rdi
    call    X::f(int)     // call non-member member function
                          //   machine code will hardcoded address
    movl    %eax, %esi    
    leaq    .LC0(%rip), %rdi
    movl    [=10=], %eax
    call    printf@PLT
    movl    [=10=], %eax
    movq    -8(%rbp), %rdx
    xorq    %fs:40, %rdx
    je  .L5
    call    __stack_chk_fail@PLT
.L5:
    leave
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE1:
    .size   main, .-main
    .ident  "GCC: (Ubuntu 7.2.0-8ubuntu3) 7.2.0"
    .section    .note.GNU-stack,"",@progbits

如果您编译程序然后查看反汇编,它通常也会显示实际的虚拟地址偏移量。