1.多态的原理
1.1 虚函数表
// 这里常考一道笔试题:sizeof(Base)是多少? class Base { public: virtual void Func1() { cout << "Func1()" << endl; } private: int _b = 1; };
通过观察测试我们发现 b 对象是 8bytes,除了_b 成员,还多一个__vfptr 放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v 代表 virtual ,f 代表 function )。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。
那么派生类中这个表放了些什么呢?我们接着往下分析
针对上面的代码我们做出以下改造
1.我们增加一个派生类Derive去继承Base
2.Derive中重写Func1
3.Base再增加一个虚函数Func2和一个普通函数Func3
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: virtual void Func1() { cout << "Derive::Func1()" << endl; } private: int _d = 2; }; int main() { Base b; Derive d; return 0; }
通过观察和测试,我们发现了以下几点问题:
1. 派生类对象 d 中也有一个虚表指针
2. 基类 b 对象虚表和派生类 d 对象虚表是不一样的,这里我们发现 Func1完 成了重写,所以 d 的虚表 中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数 的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
3. 另外 Func2 继承下来后是虚函数,所以也放进了子类虚表,Func3 也继承下来了,但是不是虚函 数,所以不会放进虚表。
4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
5. 总结一下派生类的虚表生成:
a.先将基类中的虚表内容拷贝一份到派生类虚表中
b.再看如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
c.派生类自己 新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
6. 这里还有一个童鞋们很容易混淆的问题:虚函数存在哪的?虚表存在哪的?
答:在 下面的 4.2 节全都梳理清楚了!!!
1.2 虚表、虚表指针、虚函数、普通函数的存储位置 : 详细图文 + 代码演示
首先声明,虚表存储在哪里,C++并没有明确规定,这个就要看编译器
我们这里使用的是 VS2019 ,虚表存储在 常量区
关于 【虚表、虚表指针、虚函数、普通函数的存储位置】的演示都在下面这张图了
一比一对应图片的代码在下面:注释较为详细,自己理解
class Father { public: // 虚函数 1:子类 Son 里面没有和 虚函数Func1 构成重写的函数,因此直接被继承下去(子类 Son的虚表记录的虚函数指针同时指向 Func1) virtual void Func1() { cout << "Father::Func1()" << endl; } // 虚函数 2:和 子类 Son 的 Func2 构成重写 virtual void Func2() { cout << "Father::Func2()" << endl; } // 普通函数:放在公共代码段,需要时取用 void Func() { cout << "Father::Func()" << endl; } private: int _b = 1; }; class Son : public Father { public: // 重写函数 2:Father::Func2 被重写成 Son::Func2 virtual void Func2() { cout << "Son::Func2()" << endl; } // 虚函数 3:子类 Son 自己的 虚函数 virtual void Func3() { cout << "Son::Func3()" << endl; } private: int _d = 2; }; void Test(Father* p) { cout << " 查找虚表中的虚函数 " << '\n'; p->Func1(); // 这个是 父类中的 虚函数 1,在子类中没有构成重写,子类的虚表里也存储着 Func1 的地址 p->Func2(); // 这个是 父类中的 虚函数 2,在子类中构成重写,子类的虚表里存储着 重写后的Func2 的地址 cout << '\n'; } int main() { Father f; Son s; // 演示多态:父类指针指向谁,就到谁的虚表中找虚函数,完成多态的功能 Test(&f); // 父类指针指向 一个 父类:到 父类 的虚表中找 父类 的 虚函数 Test(&s); // 父类指针指向 一个 子类:到 子类 的虚表中找 子类 的 虚函数 cout << '\n'; cout << " 打印:子类中的 普通函数 和 父类中的普通函数" << '\n'; f.Func(); s.Func(); // 打印子类中的 普通函数 Func:先到子类的类域中找,再到父类的类域中找(子类中有一个父类(继承下来的)) cout << '\n'; return 0; }
1.3 再次梳理上面的知识
⭐虚函数的地址会被放入 虚函数表
虚表地址存在 对象中,虚表存在常量区中,虚表中虚函数地址指向的虚函数存在代码段中
虚表地址存在 对象中的 头 4 个字节(至于是头几个字节,取决于地址的大小,32位下 地址大小为 4 位,64位下 地址的大小为 8 位)
⭐父类和子类都有单独的虚函数表
在子类的虚表中:
-
子类继承父类的虚函数:没有重写的,Father::Func1
-
子类继承父类的虚函数:重写的,Son::Func2
-
子类自己的虚函数
⭐派生类的虚表生成:
a.先将基类中的虚表内容拷贝一份到派生类虚表中
b.再看如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
c.派生类自己 新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
多态的本质:
⭐构成多态后,父类指针指向的对象 是谁,就会到谁的 虚表中 找想要的 虚函数 func
-
当 指针指向 父类,则 编译器会到 父类的虚表中 找 虚函数 func
-
当 指针指向 子类,则 编译器会到 子类的虚表中 找 虚函数 func
⭐因此,就拿之前讲过的 不同人买车票的样例 解释
-
父类指针指向 军人,就调用 军人的买票函数:优先买票;
-
父类指针指向学生,就调用学生的 买票函数:7折;
-
父类指针指向普通用户,就调用普通用户的 买票函数:全价
⭐不同对象的虚表是不同的指针数组:
-
若虚函数重写了,则父子的该函数内容不同,且地址不同
-
若虚函数没有重写,则父子的该函数地址内容都相同
1.4 关于 动态绑定 和 静态绑定
若符合多态,则函数地址为动态绑定,若不符合,则为静态绑定
虚拟函数的调用:是一种 运行时绑定/动态绑定 行为,就是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。(虚函数并没有固定,会在运行时,看父类指针指向谁,就到谁的虚表中,调用的谁的虚函数,这个过程是动态变化的)
普通函数的调用:是一种 编译时绑定/静态绑定 行为,即若不构成多态,在程序编译期间通过函数名就确定函数地址了,链接时直接到符号表找对应的函数地址,是一种固定的行为
如果直接指定类域调用虚函数,就不构成多态的调用了,即为静态调用,而不会是 动态调用了,因为指定类域了,就没有指定哪个对象调用哪个对象的作用了
1.5 虚表存储在哪里?
首先说明,虚表存储在哪里,C++并没有明确规定,这个就要看编译器
我们可以写一段程序,看看我们当前编译器中,虚表存储在哪里(这里使用 VS 2019 测试)
因为无法直接判断出 虚表的存储位置,我们可以通过定义各个存储空间的 变量,通过他们的地址,大概判断 虚表的地址接近哪一个 变量的地址,来大致判断虚表处于哪一种存储区
class Father { public: virtual void Func1() { cout << "Father::Func1()" << endl; } }; class Son : public Father { public: virtual void Func2() { cout << "Son::Func2()" << endl; } }; 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); printf("\n"); return 0; }
如何取到 虚表的地址?
前面章节讲解过 虚表地址存在 对象中的 头 4 个字节(至于是头几个字节,取决于地址的大小,32位下 地址大小为 4 位,64位下 地址的大小为 8 位)(我们这里以 32 位的系统测试)
我们是否可以通过将 类类型强转成 int 类型 取出头 4 个字节?
不可以,不能直接将 自定义类型强转成 内置类型
但是 指针之间可以互相转换
则先将 类类型指针强转成 int* ,然后再将 int* 解引用,不就得到 int 了吗?(妙啊!)
因此,要将虚表放到一个 不会轻易被销毁与修改数据的区域,即常量区
Father b; printf("Father 虚表地址:%p\n", *((int*)&b));
则通过这个方法,可以得出 父子类对象中的 虚表地址和对象地址
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); printf("\n"); Father b; Son d; // int/double/char // int和int* 指针之间 printf("Father虚表地址:%p\n", *((int*)&b)); printf("Son 虚表地址:%p\n", *((int*)&d)); printf("Father对象地址:%p\n", &b); printf("Son 对象地址:%p\n", &d); return 0; }
看结果可知:虚表地址接近 常量区的,对象地址接近 栈区
虚表存储在 常量区的好处
一个对象的虚表存储在 常量区
对象销毁了,虚表可不会跟着销毁,因为同类型的对象虚表是一样的,因此无需急着销毁,可以直接给下一次创建该类的对象使用
或者说,同类型的对象的虚表是共享的
就和一个类的成员函数一样,你只需使用函数的逻辑,无需向成员变量那样需要有自己独立的内容