深度探索C++对象模型(19)——函数语义学(3)——单、多继承下的虚函数

本文深入探讨C++中单继承和多继承下虚函数的实现原理,通过反汇编和内存布局分析虚函数表的构造和调用过程。在单继承中,子类的虚函数表保持与父类一致并添加自身虚函数。多继承时,涉及到虚析构函数的使用,确保正确释放内存,解释了thunk在多继承中的作用。建议在涉及继承时使用虚析构函数。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.单继承下的虚函数

代码1:

#include <iostream>
#include <stdio.h>

using namespace std;

class Base
{
public:
	virtual void f() { cout << "Base::f()" << endl; }
	virtual void g() { cout << "Base::g()" << endl; }
	virtual void h() { cout << "Base::h()" << endl; }
};

class Derive :public Base {
public:
	virtual void i() { cout << "Derive::i()" << endl; }
	virtual void g() { cout << "Derive::g()" << endl; }
	void myselffunc() {} //只属于Derive的函数
};
int main()
{
	Derive myderive;
	Derive *pmyderive = &myderive;
	pmyderive->f();
	pmyderive->g();
	pmyderive->h();
	pmyderive->i();

}

问题:

在子类中在g()的前面定义了i(),那子类的虚函数表应该是怎样的顺序呢?

反汇编:

可以从上述汇编中发现子类虚函数表中虚函数顺序为:

f(),     g(),    h(),    i() 

虚函数表简图:

子类中虚函数表中顺序与父类中排列顺序一致,在后面增加自己的虚函数地址(虚函数表表项),在虚函数表中顺序地记录着每个函数的首地址

代码2:

#include <iostream>
#include <stdio.h>

using namespace std;

class Base
{
public:
	virtual void f() { cout << "Base::f()" << endl; }
	virtual void g() { cout << "Base::g()" << endl; }
	virtual void h() { cout << "Base::h()" << endl; }
};

class Derive :public Base {
public:
	virtual void i() { cout << "Derive::i()" << endl; }
	virtual void g() { cout << "Derive::g()" << endl; }
	void myselffunc() {} //只属于Derive的函数
};
int main()
{


	Base *pb = new Derive();  //基类指针指向一个子类对象
	pb->g();
	//编译器视角
	//(*pb->vptr[1])(pb);    使用虚函数表指针调用虚函数表项,传入this指针进行调用虚函数
							//在编译期间就确定了该虚函数在虚函数表中第几项,也就是虚函数表在编译期间就已经生成

	Derive myderive;
	Base &yb = myderive; //基类引用 引用 一个子类对象
	yb.g();
}

结论:

虚函数表在编译期间生成,而我们唯一需要在执行期间知道的东西就是通过哪个虚函数表来调用虚函数(父类的还是子类的)

代码3:

虚函数地址:编译期间知道,写在了可执行文件中,虚函数表在编译期间已经构建出来。

vptr编译期间产生:编译器在构造函数中插入了给vptr赋值的代码;当创建对象时,因为要执行对象的构造函数,此时vptr就被赋值。

#include <iostream>
#include <stdio.h>

using namespace std;

class Base
{
public:
	virtual void f() { cout << "Base::f()" << endl; }
	virtual void g() { cout << "Base::g()" << endl; }
	virtual void h() { cout << "Base::h()" << endl; }
};

class Derive :public Base {
public:

};
int main()
{
	Derive a1;
	Derive a2;
	Derive *pa3 = new Derive();

	Base b1;
}

内存详情查看:

对象a1:

对象a2:

对象指针pa3所指的内存:

对象b1:

结论:

(1)可以发现虚函数表跟着类走,同一个类实例化的对象虚函数指针指向同一虚函数表

(2)即便子类没有覆盖父类的虚函数且没有自己的虚函数,子类和父类也都有各自的虚函数表,只是两个表的表项都相同,指向相同的虚函数地址,即子类不会重用父类的虚函数表。

代码4:

#include <iostream>
#include <stdio.h>

using namespace std;

class Base
{
public:
	virtual void pvfunc() = 0;
};

int main()
{
	cout << sizeof(Base) << endl;
}

运行结果:

结论:

即便纯虚函数在父类中没有被定义,但是它在父类的虚函数表中还是要占一个表项的。

2.多继承下的虚函数

代码:

#include <iostream>
#include <stdio.h>

using namespace std;

class Base
{
public:
	virtual void f() { cout << "Base::f()" << endl; }
	virtual void g() { cout << "Base::g()" << endl; }
	virtual void h() { cout << "Base::h()" << endl; }

};

class Base2
{
public:
	virtual void hBase2() {

		cout << "Base2::hBase2()" << endl;
	}
};

class Derive :public Base, public Base2 {
public:
	virtual void i() { cout << "Derive::i()" << endl; }
	virtual void g() { cout << "Derive::g()" << endl; }
	void myselffunc() {} //只属于Derive的函数
};

int main()
{
	Base2 *pb2 = new Derive();

        //编译器视角
	//Derive *temp = new Derive();
	//Base2 *pb2 = temp + sizeof(Base); // 错误表达,实际这里加的是sizeof(Base) * sizeoeof(Derive);
                                         //给指针+1就跟数组类似,实际加的是1*指针所指的类型的大小
	//Base2 *pb2 = (Base2 *)((char *)temp + sizeof(Base));  //正确表达,这才是加sizeof(Base)字节

    delete pb2;   //会报异常
}

调试delete pb2;的结果:

报告异常的原因是我们只删除了new出来的一部分内存

内存布局:

提出问题:

如何成功删除用第二基类指针new出来的继承类对象?

(1)我们要删除的实际是整个Derive()对象

(2)根据C++的设计,我们delete整个Derive()对象就要能够保证Derive()对象的析构函数被正常调用

(3)上述代码中编译器遇到delete会调用Base2的析构函数,还是调用Derive的析构函数呢?在执行delete pb2;这条语句时,编译器究竟执行了什么操作?

a)如果Base2里没有析构函数,编译器会直接删除以pb2开头的这段内存,一定报异常,因为这段内存压根就不是new起始的内存;

b)如果Base2里有一个析构函数,但整个析构函数是个普通析构函数(非虚析构函数),那么当delte pb2,这个析构函数就会被系统调用,但是delete的仍旧是pb2开头这段内存,所以一定报异常。因为这段内存压根就不是new起始的内存;析构函数如果不是虚函数,编译器会实施静态绑定,静态绑定意味着你delete Base2指针时,删除的内存开始地址就是pb2的当前位置;所以肯定是错误的

c)如果Base2里是一个虚析构函数,由于多态性,动态绑定,编译器会先去执行Derive虚析构函数,编译器会往Derive虚析构函数中插入调用父类虚析构函数的代码

如下代码:

#include <iostream>
#include <stdio.h>

using namespace std;

class Base
{
public:
	virtual void f() { cout << "Base::f()" << endl; }
	virtual void g() { cout << "Base::g()" << endl; }
	virtual void h() { cout << "Base::h()" << endl; }

};

class Base2
{
public:
	virtual void hBase2() {

		cout << "Base2::hBase2()" << endl;
	}

	virtual ~Base2() {
		int abc;
		abc = 1;
	}
};

class Derive :public Base, public Base2 {
public:
	virtual void i() { cout << "Derive::i()" << endl; }
	virtual void g() { cout << "Derive::g()" << endl; }
	void myselffunc() {} //只属于Derive的函数


};

int main()
{
	Base2 *pb2 = new Derive();
	delete pb2;

    
    //为了测试虚函数表
	Base *pbm = new Base();
	Base2 *pb222 = new Base2();
	Derive *p11212 = new Derive();

	p11212->g();
	p11212->i(); //走虚函数表
}

编译成功

编译器向子类Derive虚析构函数中插入调用Base和Base2的虚析构函数的代码,这里内存就被完整地释放,detele语句顺利执行

疑问:子类Derive没有虚析构函数,为何能调试成功?

因为父类Base2有虚析构函数,子类继承后,编译器会向子类中添加虚析构函数为了来调用父类的虚析构函数,所以这里是编译器为子类合成了虚析构函数

或者如果子类中有非虚析构函数,而父类中是虚析构函数,编译器也会把子类的非虚析构函数变为虚析构函数

结论:

凡是涉及到继承的,所有类都最好虚析构函数,即便函数体内没有内容;

虚函数表简图:

我们在Derive 类的第二个虚函数表中发现了thunk字样:

一般thunk出现在多重继承中(从第二个虚函数表开始就会有),它其实是一段汇编代码,这段代码干两个事情:

1)调整this指针,比如在delete pb2时,就将this指针调整到Derive对象起始地址,来对Derive进行操作

2)调用Derive析构函数

上述代码调试delete pb2反汇编:

可以发现在调用子类的虚析构函数的时候在内部调用了Base2的虚析构函数,没有调用Base的虚析构函数,因为Base中没有定义析构函数

整个delete pb2的流程图:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值