MST

星途 面试题库

面试题:C++继承对运行时性能和内存布局的影响分析

深入探讨C++继承机制对程序运行时性能以及内存布局的影响。例如,从对象模型的角度分析,继承体系下对象的内存布局是怎样的,这种布局如何影响内存访问效率;在多态调用时,虚函数表等机制对运行时性能的开销体现在哪些方面,如何在设计继承体系时尽量减少这些不利影响。
35.3万 热度难度
编程语言C++

知识考点

AI 面试

面试题答案

一键面试

一、C++继承机制对内存布局的影响

  1. 对象模型角度的内存布局
    • 无虚函数的简单继承:在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_memberDerived对象的内存布局是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子对象(包含vptrbase1_member)在前,接着是Base2子对象(包含vptrbase2_member),最后是derived_member。这种布局可能导致对象内存布局不紧凑,增加内存占用,并且在访问不同基类成员时可能需要更复杂的地址计算,影响内存访问效率。

二、C++继承机制对程序运行时性能的影响

  1. 虚函数表等机制在多态调用时的性能开销
    • 间接寻址开销:在多态调用时,通过基类指针或引用调用虚函数,程序需要通过虚函数表指针找到对应的虚函数表,再从虚函数表中找到实际要调用的函数地址,这涉及到两次间接寻址。例如:
Base* ptr = new Derived();
ptr->virtual_function();

这里ptrBase类型的指针,调用virtual_function时,首先要通过ptr指向的对象中的vptr找到虚函数表,然后从虚函数表中找到Derived类中virtual_function的地址,最后才调用该函数。这种间接寻址会增加额外的时间开销。

  • 虚函数表的内存开销:每个包含虚函数的类都有一个虚函数表,这会增加程序的内存占用。特别是在大型程序中,如果有大量包含虚函数的类,虚函数表占用的内存可能会相当可观。

三、在设计继承体系时减少不利影响的方法

  1. 合理使用虚函数:只在确实需要多态行为的地方使用虚函数。如果一个函数在整个继承体系中行为基本一致,不需要动态绑定,就不要将其声明为虚函数。例如,一些工具类的辅助函数可能不需要多态,就可以设计为普通成员函数。
  2. 优化继承层次:尽量避免过深的继承层次和复杂的多重继承。过深的继承层次可能导致对象内存布局复杂,增加虚函数调用的开销。多重继承要谨慎使用,因为它可能导致对象内存布局不紧凑和菱形继承等问题。可以考虑使用组合(Composition)代替多重继承,组合可以在一定程度上实现类似的功能,同时避免多重继承带来的复杂性。
  3. 使用final关键字:在C++11及以后的标准中,可以使用final关键字修饰虚函数或类。如果一个虚函数被声明为final,则表示该函数不能在派生类中被重写,这样编译器可以对虚函数调用进行优化,减少间接寻址的开销。如果一个类被声明为final,则表示该类不能被继承,同样有助于编译器进行优化。例如:
class Base {
public:
    virtual void virtual_function() final {}
};

class Derived : public Base {
    // 这里试图重写virtual_function会导致编译错误
    // void virtual_function() override {} 
};
  1. 内联虚函数:对于一些短小的虚函数,可以使用inline关键字(C++17起虚函数默认可以内联),让编译器在调用处直接展开函数代码,减少虚函数调用的间接寻址开销。不过,内联虚函数要谨慎使用,因为如果函数体较大,内联可能会增加代码体积,降低缓存命中率。