面试题答案
一键面试虚基类对象的内存布局
- 单一虚基类情况:
- 派生类对象的内存布局中,虚基类子对象通常被放置在派生类对象内存布局的末尾。
- 派生类对象会包含一个指向虚基类子对象的虚基表指针(vbtptr)。这个指针指向一个虚基表(vbtable),虚基表中存储了虚基类子对象相对于派生类对象起始地址的偏移量。这样,通过这个偏移量,就可以找到虚基类子对象在内存中的位置。
- 多重虚基类情况:
- 派生类对象会有多个虚基表指针,每个虚基表指针对应一个虚基类。同样,每个虚基表指针指向的虚基表中存储着相应虚基类子对象相对于派生类对象起始地址的偏移量。
- 这种布局方式避免了在多重继承时虚基类子对象的重复出现,实现了虚基类子对象的共享。
虚函数的内存布局及调用规则
- 内存布局:
- 每个包含虚函数的类都有一个虚函数表(vtable)。在类的对象中,有一个虚表指针(vptr),这个指针指向该类的虚函数表。
- 虚函数表是一个函数指针数组,数组中的每个元素是该类虚函数的地址。如果派生类重写了基类的虚函数,那么虚函数表中相应位置的函数指针会被替换为派生类中重写的虚函数的地址。
- 不同继承层次下虚函数调用规则:
- 单继承:当通过基类指针或引用调用虚函数时,运行时系统会根据对象的实际类型(即对象在内存中的vptr所指向的虚函数表)来确定调用哪个函数。例如,如果基类指针指向派生类对象,那么会调用派生类中重写的虚函数,因为派生类对象的vptr指向派生类的虚函数表,该表中相应虚函数位置存储的是派生类重写虚函数的地址。
- 多重继承:情况相对复杂,但基本原理相同。当通过基类指针或引用调用虚函数时,运行时系统同样根据对象实际类型的虚函数表来确定调用的函数。不过,在多重继承中,一个对象可能有多个虚函数表(如果有多个基类包含虚函数),此时需要根据指针或引用所属的基类类型,找到对应的虚函数表来调用虚函数。
- 对象类型转换情况下虚函数调用规则:
- 向上转型:当派生类对象转换为基类对象(通过指针或引用)时,虚函数调用仍然根据对象的实际类型(即派生类对象的实际内存布局中的vptr)来调用虚函数。所以,即使使用基类指针或引用,只要对象实际是派生类对象,就会调用派生类中重写的虚函数。这是因为虚函数机制是基于对象实际类型的运行时多态。
- 向下转型:如果使用
dynamic_cast
等进行向下转型,只有当转型成功(即对象实际类型确实是目标派生类类型)时,通过转型后的指针或引用调用虚函数,才会调用派生类中重写的虚函数。如果转型失败(返回空指针等情况),调用虚函数可能会导致未定义行为。这是因为虚函数调用依赖于对象的实际类型,向下转型失败意味着对象实际类型并非目标派生类类型,此时调用派生类虚函数可能访问到错误的内存位置。
原因
虚函数和虚基类的这种内存布局和调用规则设计,是为了实现C++的运行时多态和避免多重继承中基类子对象的重复。虚函数通过虚函数表和虚表指针,使得在运行时能够根据对象的实际类型确定调用哪个函数,实现多态性。虚基类通过虚基表指针和虚基表,保证在多重继承时虚基类子对象只存在一份,避免数据冗余和不一致问题。