面试题答案
一键面试1. 内存布局体现
- 普通成员函数:普通成员函数并不占用对象的内存空间。类的对象在内存中主要存储其成员变量。多个对象的普通成员函数代码是共享的,存储在代码段。例如:
class A {
public:
int data;
void func() {
// 函数体
}
};
在这种情况下,A
类对象只包含data
成员变量的内存空间,func
函数的代码存储在代码段,所有A
类对象共用这段代码。
- 虚函数:当类中包含虚函数时,对象的内存布局会增加一个虚指针(vptr)。虚指针指向虚函数表(vtable)。虚函数表是一个存储虚函数地址的数组。例如:
class B {
public:
int data;
virtual void vfunc() {
// 函数体
}
};
B
类对象在内存中除了data
成员变量外,还会有一个虚指针。虚指针指向的虚函数表中存储了vfunc
函数的地址。如果B
类有多个虚函数,虚函数表会按顺序存储这些虚函数的地址。
2. 调用机制
- 普通成员函数:普通成员函数的调用是在编译期确定的。编译器根据对象的类型直接生成调用函数的机器指令。例如:
A a;
a.func();
编译器在编译时就知道a
是A
类对象,直接生成调用A::func
的指令。这种调用方式效率较高,因为不需要额外的查找操作。
- 虚函数:虚函数的调用是在运行期确定的。当通过对象指针或引用调用虚函数时,程序首先会根据对象中的虚指针找到虚函数表,然后在虚函数表中查找对应虚函数的地址,最后调用该地址处的函数。例如:
B b;
B* ptr = &b;
ptr->vfunc();
在运行时,程序根据ptr
指向的对象中的虚指针找到虚函数表,再从虚函数表中找到vfunc
的地址并调用。
3. 虚函数表(vtable)在虚函数调用过程中的作用
以以下代码结合汇编分析:
class Base {
public:
virtual void vfunc() {
// 函数体
}
};
class Derived : public Base {
public:
void vfunc() override {
// 函数体
}
};
int main() {
Base* basePtr = new Derived();
basePtr->vfunc();
delete basePtr;
return 0;
}
在汇编层面,当执行basePtr->vfunc();
时:
- 首先从
basePtr
指向的对象内存起始位置获取虚指针(假设对象内存起始地址为ebp - 8
,虚指针通常位于对象内存起始位置)。 - 通过虚指针找到虚函数表的地址。
- 在虚函数表中,根据虚函数的索引(虚函数在虚函数表中的位置)找到具体虚函数的地址。在这个例子中,
Derived
类重写了vfunc
,所以虚函数表中对应vfunc
的地址是Derived::vfunc
的地址。 - 调用找到的虚函数地址处的函数。
虚函数表的作用就是在运行时为虚函数调用提供一个间接寻址的机制,使得程序能够根据对象的实际类型(而不是指针或引用的静态类型)来调用正确的虚函数,从而实现多态性。