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