面试题答案
一键面试虚函数表的实现原理
- 概念:
- 当一个类包含虚函数时,编译器会为这个类生成一个虚函数表(vtable)。虚函数表是一个指针数组,数组中的每个元素是一个指向虚函数的指针。
- 每个包含虚函数的类对象都有一个隐含的指针,称为虚指针(vptr),它指向该类的虚函数表。
- 生成过程:
- 编译器在编译阶段,为每个包含虚函数的类构建虚函数表。在虚函数表中,按照虚函数在类中声明的顺序,依次填入虚函数的地址。
- 对于派生类,如果重写了基类的虚函数,那么在派生类的虚函数表中,相应位置会填入派生类重写的虚函数地址;如果没有重写,就填入基类虚函数的地址。同时,派生类可能会在虚函数表末尾添加自己新定义的虚函数。
- 示例代码说明:
class Base {
public:
virtual void func1() {}
virtual void func2() {}
};
class Derived : public Base {
public:
void func1() override {}
virtual void func3() {}
};
在上述代码中,Base
类有func1
和func2
两个虚函数,编译器会为Base
类生成一个虚函数表,表中两个元素分别指向Base::func1
和Base::func2
的地址。Derived
类重写了func1
,其虚函数表中func1
位置指向Derived::func1
的地址,func2
位置仍指向Base::func2
的地址,并且在虚函数表末尾添加了指向Derived::func3
的地址。
对象调用虚函数过程中的性能开销
- 编译期开销:
- 生成虚函数表:编译器需要额外的工作来为每个包含虚函数的类生成虚函数表,这增加了编译时间和生成的目标代码大小。
- 生成虚指针:编译器要为每个包含虚函数的类对象添加一个虚指针,这会增加对象的大小。例如,在32位系统中,一个虚指针占4字节,64位系统中占8字节。
- 运行期开销:
- 虚指针寻址:每次通过对象调用虚函数时,首先要通过对象的虚指针找到虚函数表,这需要一次额外的内存间接寻址操作。例如,假设对象地址为
obj_addr
,虚指针偏移量为offset
,要获取虚函数表地址vtable_addr
,需要执行vtable_addr = *(void**)(obj_addr + offset)
,这种间接寻址会增加指令执行时间。 - 虚函数表索引:找到虚函数表后,还要根据虚函数在表中的索引找到具体的虚函数地址,然后才能调用该函数,这也增加了一定的开销。
- 虚指针寻址:每次通过对象调用虚函数时,首先要通过对象的虚指针找到虚函数表,这需要一次额外的内存间接寻址操作。例如,假设对象地址为
在复杂继承体系中优化虚函数调用性能的方法
- 使用非虚接口(NVI)惯用法:
- 原理:在基类中提供一个非虚的公共接口函数,该函数内部调用一个虚函数来实现具体的功能。这样,外部调用者调用的是非虚函数,避免了虚函数调用的间接开销,同时又能利用虚函数的多态性。
- 示例代码:
class Shape {
public:
void draw() {
doDraw();
}
private:
virtual void doDraw() = 0;
};
class Circle : public Shape {
private:
void doDraw() override {
// 具体的画圆逻辑
}
};
在上述代码中,Shape::draw
是非虚函数,外部通过Shape
对象或Circle
对象调用draw
时,直接执行该函数,然后在内部调用虚函数doDraw
实现多态。
2. 使用静态多态(模板):
- 原理:模板是在编译期实现多态,通过模板参数的不同实例化不同的代码,避免了运行期的虚函数调用开销。
- 示例代码:
template <typename T>
void draw(T& obj) {
obj.draw();
}
class Rectangle {
public:
void draw() {
// 具体的画矩形逻辑
}
};
Rectangle rect;
draw(rect);
这里通过模板函数draw
,在编译期根据传入对象的类型确定具体调用的draw
函数,没有运行期虚函数调用的开销。
3. 减少不必要的虚函数:
- 原理:如果某些函数在整个继承体系中不会被重写,那么将其定义为非虚函数,避免虚函数调用的开销。
- 示例:假设
Shape
类中有一个getArea
函数,在所有派生类中实现方式相同,就可以将其定义为非虚函数。
class Shape {
public:
double getArea() {
// 通用的计算面积逻辑
return 0;
}
};
- 使用缓存:
- 原理:在某些情况下,可以缓存虚函数的调用结果。例如,如果虚函数的返回值在一定条件下不会改变,可以将第一次调用的结果缓存起来,后续直接返回缓存值,减少虚函数调用次数。
- 示例代码:
class ComplexCalculation {
public:
virtual double calculate() {
if (cachedResult.has_value()) {
return cachedResult.value();
}
double result = /* 复杂计算 */;
cachedResult = result;
return result;
}
private:
std::optional<double> cachedResult;
};
在上述代码中,calculate
虚函数在第一次调用后缓存结果,后续调用直接返回缓存值,减少了虚函数调用带来的开销。