源代码:
#include <iostream>
struct Base
{ virtual void f(); };
struct Derived:Base
{ void f() override; };
int main()
{ Base *p = new Derived; return 0; }
编译 / 链接 / 运行:vtable 与构造函数到底发生了什么
链接过程不包含构造对象,不会执行任何一行代码。
用 main.cpp 编译出的目标文件,把「编译 / 链接 / 运行」三个阶段各自做了什么实际看一遍。
链接是纯静态的过程,不执行任何代码,也没有任何对象被构造。有的说法是「构造函数的代码里引用了 vtable」和「构造函数运行时写 vptr」,不对。
下面按三个阶段拆开,每一步都用 main.cpp 的实际产物来看。
第一阶段:编译
g++ 先调用 cc1plus 把源码变成汇编,再汇编成 main.o。
编译器看到 new Derived,就隐式生成 Derived::Derived() 和 Base::Base() 的机器码。
Base::Base() 里有这样两条指令:
c: lea 0x0(%rip),%rdx # 取 vtable 地址
f: R_X86_64_PC32 vtable for Base+0xc
17: mov %rdx,(%rax) # 把它写进对象头部, 也就是写 vptr
编译器不知道 vtable 在哪,所以 lea 的操作数先填 0 占位,同时在重定位表里记一笔:偏移 0xf 处要填 _ZTV4Base 的地址。
符号表里 _ZTV4Base 标为 UND,意思是「我用到了,但定义不在我这」:
000000000000000f R_X86_64_PC32 _ZTV4Base + c ← .rela.text._ZN4BaseC2Ev
18: 0000000000000000 NOTYPE GLOBAL UND _ZTV4Base
第二阶段:链接
collect2 调用 ld,它做的是查表和填数:
把所有 .o 和库的 section 合并,然后逐条处理重定位项,每一条都去所有输入里找符号的定义,找到就把真实地址算出来填进占位处。
_ZTV4Base 在任何输入里都没有定义,查表失败,于是报错。
报错信息里出现 in function Base::Base(),只是因为这条没法填的重定位项位于 Base::Base() 的代码段里,
链接器在告诉你「引用发生在哪个函数的代码中」,而不是它运行了这个函数。
对比补了定义的 main_fixed.o,_ZTV4Base 是有定义的,落在一个自己的 section 里,长度 24 字节:
20: 0000000000000000 24 OBJECT WEAK DEFAULT 19 _ZTV4Base
[19] .data.rel.ro.local._ZTV4Base PROGBITS … 000018
链接后的 a.out 里,同一条 lea 的占位 0 已经被换成真实偏移,指向 vtable for Base+0x10,也就是跳过 offset-to-top 和 typeinfo 指针之后的第一个虚函数槽:
1256: lea 0x2b13(%rip),%rdx # 3d70 <vtable for Base+0x10>
1261: mov %rdx,(%rax)
重定位项里写的是 +0xc 而这里显示 +0x10,差的 4 字节是因为 PC32 相对寻址以指令结束处为基准,属于编码细节,不影响理解。
第三阶段:运行
只有 ./a.out 真正跑起来,
main 调用 new Derived,
Derived::Derived() 再调用 Base::Base(),
上面那条 mov %rdx,(%rax) 才被执行,vptr 才被写进堆上的对象。
这才是「构造对象」。
你的原始 main.cpp 在第二阶段就停了,根本没走到这一步。
更准确的说法
编译器在构造函数的机器码里放了一条「把 vtable 地址写进对象」的指令,
并给这条指令留了一个待填的地址。
链接器负责填这个地址时,在所有输入里都找不到 _ZTV4Base 的定义,
于是报错。
构造函数在这里只是「引用了 vtable 的那段代码」,并没有被执行。




