回答什么会发生或者为什么运行那段代码时会发生什么,我通过
g++ -ggdb main.cc
进行了编译,并使用gdb逐步调试。
main.cc:
class A {
public:
A() {
fn();
}
virtual void fn() { _n=1; }
int getn() { return _n; }
protected:
int _n;
};
class B: public A {
public:
B() {
}
void fn() override {
_n = 2;
}
};
int main() {
B b;
}
在 main
中设置断点,然后进入 B() 函数,在打印出 this
指针后,进入 A()(基类构造函数):
(gdb) step
B::B (this=0x7fffffffde80) at main2.cc:16
16 B() {
(gdb) p this
$27 = (B * const) 0x7fffffffde80
(gdb) p *this
$28 = {<A> = {_vptr.A = 0x7fffffffdf80, _n = 0}, <No data fields>}
(gdb) s
A::A (this=0x7fffffffde80) at main2.cc:3
3 A() {
(gdb) p this
$29 = (A * const) 0x7fffffffde80
显示this
最初指向派生的B对象b
,该对象在栈上构造于0x7fffffffde80处。下一步进入基类A()的ctor,this
变成了A * const
并指向相同的地址,这是有道理的,因为基类A位于B对象的开头,但它仍未被构造:
(gdb) p *this
$30 = {_vptr.A = 0x7fffffffdf80, _n = 0}
还有一步:
(gdb) s
4 fn()
(gdb) p *this
$31 = {_vptr.A = 0x402038 <vtable for A+16>, _n = 0}
_n已被初始化,它的虚函数表指针包含virtual void A::fn()
的地址:
(gdb) p fn
$32 = {void (A * const)} 0x40114a <A::fn()>
(gdb) x/1a 0x402038
0x402038 <_ZTV1A+16>: 0x40114a <_ZN1A2fnEv>
因此,通过活动的this
和_vptr.A
执行A :: fn()使得下一步非常合理。再走一步,我们回到B()ctor:
(gdb) s
B::B (this=0x7fffffffde80) at main2.cc:18
18 }
(gdb) p this
$34 = (B * const) 0x7fffffffde80
(gdb) p *this
$35 = {<A> = {_vptr.A = 0x402020 <vtable for B+16>, _n = 1}, <No data fields>}
已经构建了基类A。请注意,存储在虚函数表指针中的地址已更改为派生类B的vtable。因此,对fn()的调用将通过this->fn()选择派生类覆盖B::fn(),给定活动的this和_vptr.A(取消注释B()中的对B::fn()的调用以查看此内容)。再次检查存储在_vptr.A中的1个地址,现在它指向派生类的覆盖:
(gdb) p fn
$36 = {void (B * const)} 0x401188 <B::fn()>
(gdb) x/1a 0x402020
0x402020 <_ZTV1B+16>: 0x401188 <_ZN1B2fnEv>
通过查看这个例子,以及一个具有3级继承的例子,似乎编译器在下降构造基本子对象时,
this*
的类型和
_vptr.A
中相应的地址会发生变化,以反映当前正在构造的子对象 - 因此它被留在指向最派生类型的位置。因此,我们期望从构造函数内部调用的虚函数选择该级别的函数,即与非虚拟函数相同的结果。同样适用于析构函数但是反过来。而且,在构造成员时,
this
变成了成员指针,因此它们也可以正确地调用为它们定义的任何虚函数。