复盘 之 虚函数,多态
Posted qq_53398102
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了复盘 之 虚函数,多态相关的知识,希望对你有一定的参考价值。
麻了,昨天面试完才发现自己对虚函数,多态的知识理解的实在是太浅了…被问烂了…
建议大家看看这几篇文章:
虚函数及虚函数表
为什么基类指针(或引用)可以调用派生类的private虚函数
下面是一些个人理解总结:
1. 首先我们搞清楚一个点,对于(基类指针指向派生类对象)到底有什么用?
Base *p = new Pai();
不是为了p能去使用派生类中的所有东西,p只能在实现多态时去调用派生类重写的虚函数,仅此而已!因为在编译的时候,编译器理解的就是p始终就是指向一个基类对象的,所以当我们在通过p去调用派生类中非动态的成员时,编译器报的错误是说:在基类中找不到成员。只有在运行时,这个基类指针才会动态的绑定到它被赋值的派生类上(比如在代码中是绑定到派生类),因此在通过基类指针调用虚函数时,使用的虚表指针是派生类对象的虚表指针(而不是基类对象的),从而去派生类的虚表里面找对于的虚函数,而在派生类的虚表中以及重写了(继承之后的地址偏移是一致的,但是重写后同样偏移地址上的函数被派生类的覆盖了),所以调用的就是派生类的!
关于虚函数,多态的细节真的非常多…
2.关于虚表的结构?以及虚表指针是如何寻址的?
虚表的结构就是一个类似数组的结构,但是没有那么简单,可以暂时这么理解;
虚表里面存放的就是虚函数们的入口地址(并不是函数),虚表就相当于是一个指针数组;
虚表指针就是指向虚表的指针,虚表指针和虚表的关系可以理解为:
int *vptr = new int[maxn];
vptr就相当于是虚表指针,[]就相当于是虚表;
那么这些虚表的内容都是在编译期间就确定的了,也就是我们相当于是知道了每个虚函数入口地址在虚表中的偏移量,所以我们在通过虚表指针访问虚函数的时候,不是遍历的找,而是类似于随机访问p->fun() <=> (*p->vptr[n]),n就是虚函数fun在虚表中的偏移量,是在编译阶段就确定了的。而动态是指,在运行的时候,绑定的是那个派生类,调用的是那个派生类的虚表指针。
3.虚表放的啥东西?
注意一个点,虚函数表中放的是函数的入口地址,所有函数只会保留一份;
别被网上说的误导了…在继承的时候,虚函数不会被继承多份,会被继承多份的是虚函数的入口地址!
看下面这张图:
下面是我自己画的:
图中1的地方是当派生类没有虚函数时的情况,其虚表指针直接指向基类的虚表就行;
4.一个注意点。
如果要实现基类指针指向派生类对象,那么派生类一定要public继承基类,在私有继承和保护继承时基类指针(引用)无法指向派生类!
5.为什么在多态的时候,在类外,基类指针指向派生类时,基类指针能够调用到派生类重写的私有虚函数呢?
9-3:C++多态之多态的实现原理之虚函数表,虚函数表指针静态绑定和动态绑定
(1)虚函数表
如下代码,计算此带有虚函数的类的大小
#include <iostream>
using namespace std;
class Test
{
public:
virtual void fun1()
{
cout << "fun(1)" << endl;
}
virtual void fun2()
{
cout << "fun(2)" << endl;
}
void fun3()
{
cout << "fun(3)" << endl;
}
private:
int _num = 1;
};
int main()
{
Test t;
cout << sizeof(t) << endl;
}
其结果并不是4,而是8
启动调试后发现,除了一个成员外,似乎还多了一个_vfptr
,很明显它是一个指针,我们称这个指针为虚函数表指针,它指向了一个虚函数表,这个虚函数表就是一个数组,这个数组中的每个元素都是一个虚函数指针
关于虚函数表及虚函数表指针以下几点需要注意
- 这里的虚函数表和前面的虚基表不要搞混,前面的虚基表存储的是偏移量,这里的虚函数表存储的是虚函数指针
- 不同对象的虚函数表是一样的
- 虚函数表指针在代码段
如下的继承体系中,子类重写了虚函数fun1
#include <iostream>
#include <stdio.h>
using namespace std;
class Test
{
public:
virtual void fun1()
{
cout << "fun(1)" << endl;
}
virtual void fun2()
{
cout << "fun(2)" << endl;
}
void fun3()
{
cout << "fun(3)" << endl;
}
private:
int _num = 1;
};
class Test2 : public Test
{
public:
virtual void fun1()
{
cout << "Test2:fun1" << endl;
}
private:
int _num2 = 2;
};
int main()
{
Test t;
Test2 t2;
}
透过监视窗口可以看出,由于子类重写了fun1,因此子类的虚函数表中的第一个虚函数指针发生了变化,而fun2没有被重写,因此还是使用的是相同的
(2)多态原理
如下继承体系中,fun1和fun2重写,使用FUN函数,以父类指针做形参,分别调用fun1和fun2,因此构成多态
#include <iostream>
#include <stdio.h>
using namespace std;
class Test
{
public:
virtual void fun1()
{
cout << "fun(1)" << endl;
}
virtual void fun2()
{
cout << "fun(2)" << endl;
}
void fun3()
{
cout << "fun(3)" << endl;
}
private:
int _num = 1;
};
class Test2 : public Test
{
public:
virtual void fun1()
{
cout << "Test2:fun1" << endl;
}
virtual void fun2()
{
cout << "Test2:fun2" << endl;
}
private:
int _num2 = 2;
};
void FUN(Test* p)
{
p->fun1();
p->fun2();
}
int main()
{
Test t;
Test2 t2;
FUN(&t);
FUN(&t2);
}
通过,汇编代码可以发现多态的实现原理。
首先是父类指针指向父类对象,分别调用fun1和fun2,原理如图
前面说过,如果将形参换成父类的对象,而不采用指针或引用,那么就不会构成多态,因此在这样的情况下,调用谁的函数已经早就在编译的时候就确定好了
(3)静态多态与动态多态
A:静态多态
静态多态其实我们已经用过很多次了,甚至在第一次学习C++时,就遇到过了
#include <iostream>
using namespace std;
int main()
{
int i=0;
double d=3.14;
cout<<i;
cout<<d;
}
为什么同一个cout可以输出不同类型的变量——函数重载。所以函数重载就属于一种静态的多态,静态多态又称为静态绑定,之所以是静态的是因为在程序编译时就已经确定了行为。比如前面的父类对象作为形参那个例子,编译器已经知道了一定会调用父类的函数
B:动态多态
动态多态又称为动态绑定,就像前面的例子中:在程序运行时,会根据具体拿到的类型执行相应的动作。
以上是关于复盘 之 虚函数,多态的主要内容,如果未能解决你的问题,请参考以下文章