The compiler's job is to lay out the data and code the program needs into memory addresses. Each non-virtual function - whether member or non-member - gets a fixed virtual memory address at which it can be called. Calling machine code then hardcodes an absolute (or with position independent code a calling-address-relative offset) address of the function to call.
For example, say your compiler is compiling a non-virtual member function that takes 20 bytes of machine code, and it's putting the executable code at virtual addresses from offset 0x1000 and has already generated 10 bytes of executable code for other functions, then it will start the code of this function at virtual address 0x100A. Code that wants to call the function then generates machine code for "call 0x100A" after pushing any function call arguments (including a this
pointer to the object to be operated upon) onto the stack.
You can easily see all this happening:
~/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 $3, %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 $16, %rsp
movq %fs:40, %rax
movq %rax, -8(%rbp)
xorl %eax, %eax
leaq -9(%rbp), %rax
movl $7, %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 $0, %eax
call printf@PLT
movl $0, %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
If you compile a program then look at the disassembly it'll usually show the actual virtual address offsets too.