面试题答案
一键面试一、C++继承机制对内存布局的影响
- 对象模型角度的内存布局
- 无虚函数的简单继承:在C++中,当一个派生类继承自基类且没有虚函数时,派生类对象的内存布局是基类对象成员在前,派生类新增成员在后。例如:
class Base {
public:
int base_member;
};
class Derived : public Base {
public:
int derived_member;
};
在这种情况下,Derived
对象的内存布局是先存放base_member
,接着存放derived_member
。这样的布局使得访问基类和派生类成员时,内存访问是连续的,有利于提高缓存命中率,从而提高内存访问效率。
- 有虚函数的继承:当基类或派生类中有虚函数时,情况会有所不同。每个包含虚函数的类(或从包含虚函数的类派生而来的类)的对象都会包含一个虚函数表指针(vptr)。这个指针通常位于对象内存布局的起始位置。例如:
class Base {
public:
virtual void virtual_function() {}
int base_member;
};
class Derived : public Base {
public:
void virtual_function() override {}
int derived_member;
};
Base
对象的内存布局是vptr
在前,然后是base_member
。Derived
对象的内存布局是vptr
(指向Derived
类的虚函数表),接着是base_member
,最后是derived_member
。由于虚函数表指针的存在,访问对象成员时可能需要额外的间接寻址,这在一定程度上会影响内存访问效率。
- 多重继承:当一个类从多个基类继承时,内存布局会更加复杂。派生类对象中会依次存放各个基类子对象,并且如果基类有虚函数,每个基类子对象可能都有自己的虚函数表指针。例如:
class Base1 {
public:
virtual void virtual_function1() {}
int base1_member;
};
class Base2 {
public:
virtual void virtual_function2() {}
int base2_member;
};
class Derived : public Base1, public Base2 {
public:
int derived_member;
};
Derived
对象的内存布局可能是Base1
子对象(包含vptr
和base1_member
)在前,接着是Base2
子对象(包含vptr
和base2_member
),最后是derived_member
。这种布局可能导致对象内存布局不紧凑,增加内存占用,并且在访问不同基类成员时可能需要更复杂的地址计算,影响内存访问效率。
二、C++继承机制对程序运行时性能的影响
- 虚函数表等机制在多态调用时的性能开销
- 间接寻址开销:在多态调用时,通过基类指针或引用调用虚函数,程序需要通过虚函数表指针找到对应的虚函数表,再从虚函数表中找到实际要调用的函数地址,这涉及到两次间接寻址。例如:
Base* ptr = new Derived();
ptr->virtual_function();
这里ptr
是Base
类型的指针,调用virtual_function
时,首先要通过ptr
指向的对象中的vptr
找到虚函数表,然后从虚函数表中找到Derived
类中virtual_function
的地址,最后才调用该函数。这种间接寻址会增加额外的时间开销。
- 虚函数表的内存开销:每个包含虚函数的类都有一个虚函数表,这会增加程序的内存占用。特别是在大型程序中,如果有大量包含虚函数的类,虚函数表占用的内存可能会相当可观。
三、在设计继承体系时减少不利影响的方法
- 合理使用虚函数:只在确实需要多态行为的地方使用虚函数。如果一个函数在整个继承体系中行为基本一致,不需要动态绑定,就不要将其声明为虚函数。例如,一些工具类的辅助函数可能不需要多态,就可以设计为普通成员函数。
- 优化继承层次:尽量避免过深的继承层次和复杂的多重继承。过深的继承层次可能导致对象内存布局复杂,增加虚函数调用的开销。多重继承要谨慎使用,因为它可能导致对象内存布局不紧凑和菱形继承等问题。可以考虑使用组合(Composition)代替多重继承,组合可以在一定程度上实现类似的功能,同时避免多重继承带来的复杂性。
- 使用final关键字:在C++11及以后的标准中,可以使用
final
关键字修饰虚函数或类。如果一个虚函数被声明为final
,则表示该函数不能在派生类中被重写,这样编译器可以对虚函数调用进行优化,减少间接寻址的开销。如果一个类被声明为final
,则表示该类不能被继承,同样有助于编译器进行优化。例如:
class Base {
public:
virtual void virtual_function() final {}
};
class Derived : public Base {
// 这里试图重写virtual_function会导致编译错误
// void virtual_function() override {}
};
- 内联虚函数:对于一些短小的虚函数,可以使用
inline
关键字(C++17起虚函数默认可以内联),让编译器在调用处直接展开函数代码,减少虚函数调用的间接寻址开销。不过,内联虚函数要谨慎使用,因为如果函数体较大,内联可能会增加代码体积,降低缓存命中率。