文章目录
- 单继承中的虚函数表
- 多继承中的虚函数表
- 动态绑定与静态绑定
- 问题探究
- 第一步:观察普通调用的汇编代码
- 第二步:观察ptr1的汇编代码:
- 第三步:观察ptr2的汇编代码:
- 总结:
- 继承和多态常见的面试问题
单继承中的虚函数表
我们来通过一段单继承的程序来观察一下:
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
void Func3()
{
cout << "Base::Func3()" << endl;
}
private:
int _b = 1;
};
class Derive : public Base
{
public:
void Func1()
{
cout << "Derive::Func1()" << endl;
}
virtual void Func5()
{
cout << "Derive::Func5()" << endl;
}
private:
int _d = 2;
};
void func(Base& p)
{
p.Func1();
}
通过调试,我们发现在父类对象b的虚表中有两个虚函数指针,这个我们可以理解,但是子类虚表也只有两个,分别是重写的fun1,还有继承下来的fun2,那么子类的虚函数fun5去哪里了呢,先俩通过内存观察一下:
我们通过观察虚函数表,发现除了在调试阶段看到的两个 函数以外,还有一个函数指针存储到虚函数表里边。
到这里,还有人可能说内存中看到并不代表就是我们子类中的虚函数,当然也可以通过打印虚函数表来实现:
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
void Func3()
{
cout << "Base::Func3()" << endl;
}
private:
int _b = 1;
};
class Derive : public Base
{
public:
void Func1()
{
cout << "Derive::Func1()" << endl;
}
virtual void Func5()
{
cout << "Derive::Func5()" << endl;
}
private:
int _d = 2;
};
void func(Base& p)
{
p.Func1();
}
typedef void(*VFPTR)();
void PrintVFTable(VFPTR* table,size_t n)
{
for (size_t i = 0; i < n; ++i)
{
printf("vft[%d]:%p->", i, table[i]);
//table[i]();
VFPTR pf = table[i];
pf();
}
cout << endl;
}
int main()
{
Base b;
Derive d;
PrintVFTable((VFPTR*)*(int*)&d,3);
PrintVFTable((VFPTR*)*(int*)&b,2);
return 0;
}
打印了虚函数表之后,结果就很明了了,但是这里在打印虚函数表传参时,可能还有点问题,这里要传入的参数为(VFPTR*)*(int*)&d
,这是因为对d取地址之后,代表整个类的地址,我们强转为int*类型,只看前四个字节的地址,就是虚函数表的地址,将地址解引用,得到一种整形数字,将整形数字强转为函数指针类型,就能得到函数表中的函数指针。
多继承中的虚函数表
在前边继承的章节,我们学习到了多继承,多继承就是一个子类拥有两个或者两个以上的直接父类。
class Base1 {
public:
virtual void func1() { cout << "Base1::func1" << endl; }
virtual void func2() { cout << "Base1::func2" << endl; }
private:
int b1;
};
class Base2 {
public:
virtual void func1() { cout << "Base2::func1" << endl; }
virtual void func2() { cout << "Base2::func2" << endl; }
private:
int b2;
};
class Derive : public Base1, public Base2 {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
private:
int d1;
};
在多继承中,当父类1和父类2 都有虚表时,子类会将父类的虚表都继承下来,所以说子类中有两个虚表,通过调试来观察一下:
此时,又出现了和单继承同样的情况,子类中的虚函数到底有没有存在虚表中,存在哪个虚表中呢?
先来看Base2的虚函数表:
再来看Base1的虚函数表:
其实子类中的虚函数是会存在第一个虚函数表中。
动态绑定与静态绑定
- 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,
比如:函数重载。 - 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体
行为,调用具体的函数,也称为动态多态。
问题探究
在在观察调试时,发现了一个问题,在两个虚函数表中,子类重写的func1函数,指向同一个函数,那么为什么他们的地址不同呢?
int main()
{
Derive d;
d.b1 = 1;
d.b2 = 2;
d.d1 = 3;
Base1* ptr1 = new Derive;
Base2* ptr2= new Derive;
ptr1->func1();
ptr2->func1();
}
为了搞清楚这个问题,我们来观察汇编来解决这个问题:
第一步:观察普通调用的汇编代码
由于ptr3是普通调用,所以是直接跳转的:
第二步:观察ptr1的汇编代码:
第三步:观察ptr2的汇编代码:
总结:
其实我们发现,在虚函数表中存的并不是函数真正的地址,只是一条跳转语句,只不过他们最后跳转的地方都是相同的函数,当然地址也是相同的。
继承和多态常见的面试问题
- 什么是多态?
多态就是同一操作作用于不同的对象,会产生不同的结果。
- 什么是重载、重写(覆盖)、重定义(隐藏)?
重载
:就是函数重载,一个函数名相同,但是参数的类型不同,参数的顺序不同,参数的个数不同,这是因为函数名在修饰后是不同的。
重写
:重写就是多态的一个条件,首先父类必须是虚函数,而且子类中的函数必须和父类的函数名,参数,返回值都必须相同。但是有两个特殊情况,在前边的文章中有详细介绍。
重定义
:重定义就是父类和子类中的函数名相同,子类中的函数会对父类中的函数会进行隐藏,所以要访问父类中的函数,必须突破父类的类域。
- 多态的实现原理?
当子类的函数对父类函数完成重写时,将父类中的虚函数复制,并且修改重写的函数,当我们使用父类的指针和引用访问函数时,在运行时决议,指针指向父类调父类函数,指针指向子类调子类函数,所以就实现了多态。
- inline函数可以是虚函数吗?答:可以,不过编译器就忽略inline属性,这个函数就不再是
inline,因为虚函数要放到虚表中去。 - 静态成员可以是虚函数吗?
答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表,没有this指针,但是要访问虚表必须使用this指针,所以他们是矛盾的。
- 构造函数可以是虚函数吗?
答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。
- 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
答:可以,并且最好把基类的析构函数定义成虚函数。在上篇文章中,有一种特殊情况,如果不将析构函数实现多态,会少析构子类中除去子类中的一部分。
- 对象访问普通函数快还是虚函数更快?
如果没有实现多态,虚函数和普通函数都是一样快的,如果实现了多态,那么虚函数更慢,因为多态调用是运行时决议的,必须在虚函数表中去寻找函数。
- 虚函数表是在什么阶段生成的,存在哪的?
答:虚函数表是在编译阶段就生成的,虚函数表是只读的,一般情况下存在代码段(常量区)的。但是切记虚函数表指针是存在对象中的。
- C++菱形继承的问题?虚继承的原理?答:参考继承课件。注意这里不要把虚函数表和虚基
表搞混了。 - 什么是抽象类?抽象类的作用?答:抽象类强制重写了虚函数,另外抽象类体现出了接口继承关系。