揭秘C++多态背后的虚函数表机制
一、纯虚函数和抽象类那何为纯虚函数,何为抽象类呢?
1.1 纯虚函数在虚函数的后面写上=0,则这个函数为纯虚函数,纯虚函数不需要定义实现(实现没啥意义因为要被派生类重写,但是语法上可以实现),纯虚函数只要声明即可。
纯虚函数也可以实现定义部分:
1.2 抽象类我们先来看看什么是抽象?
抽象派画作:
就是某种程度说这个东西想表达的意思就是跟现实世界不对应,他不在现实世界中对应某一实体,不对应某个实体就不能实例化出对象
包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,如果派生类继承后不重写纯虚函数,那么派生类也是抽象类,派生类也不能实例化出对象
这是为什么?
如果派生类继承后不重写纯虚函数,派生类也是抽象类,纯虚函数某种意义上强制派生类重写虚函数,因为不重写就实例化不出对象。
抽象类不能实例化出对象:
派生类继承后不重写纯虚函数,派生类也是抽象类
派生类继承后重写纯虚函数,派生类就不是抽象类,就可以实例化出对象
那既然抽象类不能实例化出对象,为什么还要搞这个呢?
抽象类不能实例化出对象,但是可以定义指针,抽象类的指针可以指向派生类对象
总结:纯虚函数适用于:
要强制派生类中重写这个虚函数,实现多态基类中确实就是有些不想被实例化出对象的一些抽象的类别ok,接下来,我们来看一道题:
下面编译为32位程序的运行结果是什么(D)
A. 编译报错 B. 运行报错 C. 8 D. 12
也许会有很多小伙伴会有疑惑?为什么是12呢?不应该是8吗?
ok,这就需要我们对多态的底层有一定的了解——
二、多态的原理2.1 虚函数表指针当我们运行上面题目中的代码时,会发现b的成员好像不止两个——
除了_b和_ch成员外,还有一个_vfptr,_vfptr是什么🤨?
ok,其实_vfptr是一个指针,对象中的这个指针我们叫做虚函数表指针(简称虚表指针),本质上是一个指针数组(虚函数指针数组)
当一个类里面有虚函数的时候,那这个对象中就会多一个指针,这个指针(_vfptr 虚函数表指针)会指向一张表 --> 指向一个数组,数组中存放的是虚函数的地址!!!
虚函数和普通函数的真正区别是——
普通函数就像之前那样调用;而虚函数要实现多态调用,所以虚函数要实现多态调用,它就要把它的地址放进虚函数表中,多态的核心就要靠着这个虚函数表来实现的
所以上面那道题选择D——
2.2 虚函数表机制:多态背后的"引擎"ok,说完了这一部分之后,我们再来看看——
代码语言:javascript复制class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
private:
string _name;
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-打折" << endl; }
private:
string _id;
};
void Func(Person* ptr)
{
// 这⾥可以看到虽然都是Person指针Ptr在调⽤BuyTicket
// 但是跟ptr没关系,⽽是由ptr指向的对象决定的。
ptr->BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(&ps);
Func(&st);
return 0;
}当Student类中重写了虚函数以后,他的内部的情况变成了什么情况?
⼀个含有虚函数的类中都至少都有⼀个虚函数表指针,因为⼀个类所有虚函数的地址要被放到这个类对象的虚函数表中
我们先来看看Person类中的情况:
我们再来看看Student类中的情况:
通过上面的学习我们知道,多态的核心就要靠着这个虚函数表来实现的
总结:(很重要)普通调用时编译时确定地址
多态调用时运行时到指向的对象中找到这个虚函数表指针,然后通过虚函数表指针找到虚函数表,在虚函数表中找到相应的虚函数,然后去调用这个虚函数。
或者是多态调用时运行是到指向的对象里面找地址,所以指向谁就去谁的里面找这个虚函数表指针,然后通过虚函数表指针找到虚函数表,在虚函数表中找到相应的虚函数,然后去调用这个虚函数,指向基类调用基类中的虚函数,指向派生类调用派生类中的虚函数
为什么这样设计?为什么要求是基类的指针或者引用调用函数并且这个被调用的函数是虚函数并完成重写/覆盖?
这种"跟对象走"的设计实现了真正的多态:
统一接口:所有派生类对象都可以通过基类指针来操作
个性实现:每个对象执行自己特有的版本
运行时决定:具体调用哪个函数在运行时根据实际对象类型确定
面试题——
什么是多态?
多态分为静态多态和动态多态
静态多态是编译式确定的多态(编译时确定地址),它就是一个同名函数因为参数的不同而表现出多种形态动态多态是指用一个基类的指针或者引用去调用函数,并且这个被调用的函数是虚函数并完成重写/覆盖,这个指针或者引用指向谁就去谁的里面找到这个虚函数指针,通过这个虚函数指针找到这个虚函数表,底层原理是依靠对象当中存放的一张虚函数表,不同的虚函数表中存放的是不同的虚函数,基类中存放的是基类的虚函数,派生类中存放的是派生类的虚函数,然后再到这个虚函数表中找到相应的虚函数,再去调用这个虚函数2.3 动态绑定与静态绑定对不满足多态条件(指针或者引用+调用虚函数)的函数调用是在编译时绑定,也就是编译时确定调用函数的地址,叫做静态绑定满足多态条件的函数调用是在运行时绑定,也就是在运行时到指定对象的虚函数表中找到调用函数的地址,也就叫做动态绑定代码语言:javascript复制class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
private:
string _name;
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-打折" << endl; }
private:
string _id;
};
void Func(Person* ptr)
{
// 这⾥可以看到虽然都是Person指针Ptr在调⽤BuyTicket
// 但是跟ptr没关系,⽽是由ptr指向的对象决定的。
ptr->BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(&ps);
Func(&st);
return 0;
}2.4 虚函数表基类对象的虚函数表中存放基类所有虚函数的地址。同类型的对象共用同一张虚标,不同类型的对象各自有独立的虚表,所以基类和派生类有各自独立的虚表。派生类由两部分组成,继承下来的基类和自己的成员,一般情况下,继承下来的基类中有虚函数表指针,派生类自己就不会再生成虚函数表指针。但是要注意的是这里继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同一个,就像基类对象的成员和派生类对象中的基类对象成员也是独立的。
派生类中重写的基类的虚函数,派生类的虚函数表中对应的虚函数就会被覆盖成派生类重写的虚函数地址这就可以来解释一下,为什么虚函数重写也叫做覆盖?
student类继承person类,若派生类中无重写的虚函数,其实student派生类中也有虚函数,这个虚函数是基类的虚函数,派生类的虚表中放的是基类的虚函数;若重写了,放的是派生类重写的虚函数的地址,这个重写的虚函数的地址就会覆盖原来的地址
也就是说派生类中没有重写虚函数,放的是基类的虚函数;如果重写了放的是重写的虚函数
派生类中没有重写虚函数派生类中重写虚函数派生类的虚函数表中包含三个部分:
基类的虚函数地址派生类重写的虚函数地址完成覆盖派生类中自己的虚函数地址也许或有小伙伴会问:怎么没有看到func3的地址?这是因为监视窗口不让我们看,内存窗口勉强可以看到——
虚函数表本质是一个存放虚函数指针的指针数组,虚函数表中存放的是虚函数的地址,本质虚函数还是放在代码段的,将虚函数的地址放在虚表中只是为了方便实现多态
虚函数存在哪里? 虚函数和普通函数一样的,编译好后是一段指令,都是存在代码段的,只是虚函数的地址又存到了虚表中
虚函数表存在哪里?这个问题严格说并没有标准答案,C++标准并没有规定,但是我们写下面的代码可以对比验证一下。
代码语言:javascript复制class Base {
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
void func5() { cout << "Base::func5" << endl; }
protected:
int a = 1;
};
class Derive : public Base
{
public :
// 重写基类的func1
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func1" << endl; }
void func4() { cout << "Derive::func4" << endl; }
protected:
int b = 2;
};
int main()
{
int i = 0;
static int j = 1;
int* p1 = new int;
const char* p2 = "xxxxxxxx";
printf("栈:%p\n", &i);
printf("静态区:%p\n", &j);
printf("堆:%p\n", p1);
printf("常量区:%p\n", p2);
Base b;
Derive d;
Base* p3 = &b;
Derive* p4 = &d;
printf("Person虚表地址:%p\n", *(int*)p3);
printf("Student虚表地址:%p\n", *(int*)p4);
printf("虚函数地址:%p\n", &Base::func1);
printf("普通函数地址:%p\n", &Base::func5);
return 0;
}运行一下:
VS下虚函数表是存在代码段的(常量区)!!!
结尾写到这里多态这一章节就完美散花啦,那请大佬不要忘记给博主来个赞哦!
૮₍ ˶ ˊ ᴥ ˋ˶₎ა