既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
在监视窗口的观察下我们可以看出来,虚函数表指针中存放有一个虚函数,如果在代码中有多个虚函数的话,虚函数表中就有多个虚函数。
在看完了Mike之后接下来我们就来观察一下John对象。
从图中我们可以看出在John中存放着自己的_b。同时在John里面还我们包含了一个父类,父类里面有一个虚表,而我们就可以用一个指针指向虚表。
如果被重写在这个虚表里面存放的是重写的虚函数。
那么这样我们就可以分析出为什么多态可以做到指向父类调用父类,指向子类调用子类的操作。
这里代码中是父类的调用或者指针,如果我们指向父类,那么我们看到的就是父类。
如果我们指向子类,那么这个地方就先进行切片,将父类部分切出来给给代码,实际看到的依旧是一个父类。
无论在这里传的是父类还是子类,对于编译器而言它看到的都是父类。
那么我们是怎么实现多态的呢?
这就是多态实现的原理,如果不符合多态,那么在编译的时候就确定地址。如果符合多态,那么它到运行的时候到指向对象的虚函数表中找调用函数的地址。
在调用的时候,如果传的是父类,那么我们就去父类中找到它的虚表;如果传的是子类,那么先切片,我们看到的依旧是父类对象,但是这个父类是子类里面的父类,它的虚表已经被覆盖,找到的是子类的地址。
多态存在的问题:
在了解多态的过程中,因为多态实现条件的严苛,我们可以延伸出来很多问题。
为什么不能是父类的对象:
那么就从第一个条件开始讲起。
我们说过多态的实现必须要是父类的指针或者引用,这里就延伸出了一个问题,如果是父类的对象还能实现多态吗?
答案是不行:这是因为对象的切片和指针或者引用的切片有些许的不同。
这里先将代码写出来,接下来就要去调试里面找到窗口。
找到父类的对象和父类的指针或者引用有哪些不一样的地方。
在派生类对象中,它是由3个部分构成,派生类虚表和父类虚表并不相同,它的构成一部分是父类一部分是派生类自己。
这里派生类有自己的虚表是因为它完成了虚函数的重写。
像左边3种写法都是切片的方法。
首先如果是指针和引用,这里的指针可以指向父类对象,它在父类对象中看到的是父类的虚表。它指向子类的话,看到的是子类中父类的那一部分。
那么对于这个指针而已,我们指向父类对象看到的是父类对象,指向子类对象看到的也是父类对象。
子类父类对象和子类当中的父类对象的虚表是不一样的。
而这里不一样的虚表是第一个虚表。
借助这张图,我们这里就可以来详细讲解的重写或者覆盖的操作了。
这里我们的做法是先把父类的虚表给拷贝下来,拷贝完了之后重写后用重写的地址对其进行覆盖操作。
因此指针或者引用并不会发生什么拷贝问题。
但是对象的切片需要拷贝。
在对象切片的拷贝中,我们的_a(上图见)会拷贝过去,但是虚表并不会拷贝过去。 这里不拷贝虚表的原因是,如果拷贝虚表,我们再用父类的指针去指向父类对象反而会去调用子类。
因此我们得出一个结论:子类赋值给父类对象切片,不会拷贝虚表。如果拷贝虚表,那么父类对象虚表中是父类虚函数函数子类虚函数我们无法确定。
虚表存放的位置:
接下来我们要问的问题就是虚表存放的位置。
对于存放数据的位置,不外乎就是栈,堆,数据段(静态区) ,代码段(常量区)。那么这个地方虚表究竟存放在哪里呢?
这里我们虚表存放的位置是常量区,因为虚表是不能被修改的。
而这里我们是通过取地址的方法,取出栈,静态区,常量区和堆处的地址,接下来让它们的地址与虚表的地址相比较。
在这里可以看见栈和堆距离两个虚表相差盛远,静态区和常量区的地址和虚表的地址比较接近,最后再比较静态区和常量区。
比较的结果是常量区相比较静态区,到两个虚表的byte较少,因此这个地方我们就推测虚表存放的位置为常量区。
派生类中自己的对象存放的位置:
在多态中我们还有一个问题,那就是派生类中自己的对象的位置在哪里?
简单口头讲解可能不太清楚,我们来看一下图。
这里要简单的补充一点知识,那就是在这里虚表在内存空间结束的位置通常会加一个0,但是并不是每个编译器都会这样做,有些编译器就没有这样做。
然后就是这里派生类Func3处的问题,在监视窗口里面我们并没有看到派生类的Func3。但是在内存窗口处有一个地址,它可能是Func3。
那么我们要怎么验证Func3的存在?
这里的一个方法就是将其强制打印。
在这里我们给每一个Func后面都增加一句打印。如果在调用的时候打印了Func3的数据,那就可以证明那个地址就是Func3的地址。
那么这个地方我们打印虚表要怎么打?
这里要先明白虚表,它的全名叫做虚函数表,简称虚表,它的本质可以认为是函数指针数组。此处就需要我们定义一个函数指针变量。
接下来我们就要将函数指针数组的代码书写出来。
书写出来后我们依次去访问里面的内容。
下一个步骤就是要打印虚表,打印虚表的话就需要我们将虚表的地址,而虚表的地址就在头四个字节上。
这里我们通过代码的实现可以明显的看出来在虚表中不止有两个地址,在这个地方还存在着第三个地址。
而这个代码在监视窗口下只显示两个地址,我们可以将其看成监视窗口的一个小bug。
但是即使到了这一步我们也仅仅是确定了监视窗口出现了一个bug,而不能说明这个地址放的就是Func3。
这里为了证明这个地址存放的是Func3,我们还需要做最后一个步骤。
在这个地方我们增加了一个步骤,那就是在打印虚表的同时去进行调用函数的操作。
而在上边我们又做了一个伏笔,每个函数都有对应的打印标记。
借用这个函数标记我们就可以访问对应的函数。
那么如果在这个地方借助第三个不确定的地址打印出来了Func3函数里面对应的数据,这个地方我们才能正式的确定,Func3是存放在虚表里面的。
而事实确实是如此,在这里我们出现了Func3对应的函数标记,因为Func3里面的数据被打印出来了,所以这里就能确定这个地址就是Func3的地址。
同时这里也要注意一个点,那就是监视窗口有时候是不可靠的,在使用监视窗口的时候要留个心眼,相对比内存的可信度要更加的高。
静态多态与动态多态:
编译器中我们还将多态分为静态的多态和动态的多态。
那么这里的静态多态和动态多态有什么区别吗?
静态多态:
首先这里我们要谈的是静态的多态。
静态(编译时)的多态,这里指的就是函数重载。
例如上图,就是一个简单的函数重载。
这个地方我们传不同的参数,编译器在编译的时候通过函数名修饰规则,它会去匹配不同的函数。
这个就是静态的多态,也被叫做静态绑定,它们在编译的时候就确定好了地址。
动态多态:
接下来就是动态的多态。
这个地方动态的多态指的是继承,虚函数重写,实现的多态。
这里和静态的多态不一样的地方是,静态的多态是在编译时确认的地址,而动态的多态则是在运行时去虚表中找到它的地址,然后再去调用。
多继承和单继承的关系的虚表:
再下来就是讲一下多继承关系的虚表。这个地方不讲解单继承的虚表是因为在上面虚表的一系列实现中我们都是使用的单继承的方式。
这里我们就不再去讲解一遍单继承。
这个地方我们就要先写一个多继承的代码出来。
同时上边打印虚表的代码也要使用到。
接下来就对我们的多继承先进行一下分析,在Base1中有一个func1和一个func2,同样的在Base2中也有一个func1和一个func2。
接下来在派生类中我们重写了func1,并且还增添了一个func3,与此同时同时它们各种都有一个整形。
这里的父类就是一个普通的继承,并没有什么问题,这里主要会出现的问题还是在派生类那里。
派生类函数的大小:
那么派生类中Dervide的大小是多少。
这里我们就用Derive去定义一个d。
接下来通过d去测量Derive的大小。
最后测得它的大小是20个字节。
在这里的Base1中的大小是8个字节,有一个虚表的指针,外加一个整形。同样的Base2里面也是一个虚表指针和一个整形,二者相加的大小为16个字节,最后再加上自己的一个成员,大小正好为20个字节,它有两个虚表。
但是这个地方就出现了一个问题那就是Derive里面的func3在哪里?
按照我们上边的表来看,20字节的空间,一个是Derive自己整形,另外就是两个虚表指针,这么看来func3没有存储的空间。
那么,这里就出现三种情况,一种是放在Base1中,另一种是放在Base2中,最后一种是两边都放。而为了验证func3属于以上的哪种情况,这里就需要打印它的虚表。
这里通过打印出来的虚表我们可以观察到,func3是放置在Base1里面的。但是这里的结果中出现了一个,那就是Derive的func1地址不一样了。
但是这里地址虽然不一样,但是最后它们都去调用到了func1。
那么这就成为了我们下一个问题。
地址不相同问题:
作为上边问题的总结。
在这个地方我们func1和func2都重写了func1,但是之后Base1和Base2的虚表中func1的地址不一样。
这个地方要解决或者了解这个问题,就需要汇编/反汇编的帮助。
像这里,在调试的情况下我们去使用反汇编,同时也用监视窗口去查看反汇编里面的eax与两个虚表。
那么虚表在这里要解决的问题代码也将其写出来。
这里重写的func1在Base1和Base2里面的地址不同。
这就是我们要问题所在,地址不同但是却又调用同一个函数,这里就要借助汇编来探查。
那么在汇编中我们会看到什么呢?
在这里我们可以看到,在编译器的汇编中,当箭头走到call eax处。这个地方去使用监视窗口,通过监视窗口可以看见eax的地址和Base1中func1的地址是一样的。
然后在下来我们就要F11进行call里面查看。
这个地方的jmp指令才是真正存放数据的地址。
也就是说,在汇编中我们call的地址一般都是这个jmp指令的地址。
然后在这里最后结尾处还有一个地址0C32840。
这里的这段汇编代码再F11一次后,这个地方它会跳到此处来,这个地方也就是派生类中的重写的func1里面。
这里的Base1就讲解好了,接下来就是Base2里面数据的讲解。
这个地方我们的第一步都是一样的,在call中eax和Base2中的func1地址是一致的。
但是当我们进入这个代码观察jmp的时候,这个地方会有不同之处。
这个地方的jmp的地址和我们一开始Base1中jmp的地址不同。
与此同时,在jmp后面附带的地址也和Base1的地址不同。
这个地方完了更深一步的探索,我们要再次F11进入jmp里面,查看它会跳转到哪个地方。
这个地方jmp以后会回到我们上图的第一行里面,再下来继续执行jmp指令,这个地方的jmp指令的后边有一个地址00C328C3。
而这个地方又刚刚好是我们Base1中真正存放数据的指针。
最后实现了和Base1中相同的操作。
也就是说在这里最后都调用到了func1,只不过Base2做执行了几行代码。
而这里多执行几行代码的核心就是这个。
这里的ecx就类似我们的this指针,而汇编中这段的意思是这样的:这里执行时,this指针进行-8的操作。
那么为什么这里要this指针-8呢?
我们要想知道为什么this指针-8就需要这张图,此前有了解过Base1和Base2之间相差了8个字节的大小。
这里的ptr1指向Base1开始的位置,ptr2指向Base2开始的位置。在上边的解析中,ptr2减去8则来到了ptr1的位置,那么为什么ptr2要减回到ptr1这个位置呢?
这里我们首先要明白,这个地方我们要调用的是派生类中的func1,这个地方的this指针应该指向的是Derive对象。
这里Base1的开始位置恰恰好于Derive的开始位置重合,所以ptr1不需要去移动。这里ptr2需要移动的原因是,它要去调用Derive函数。this指针要保证是正确的,这个地方ptr1因为和Derive位置重合,所以我们可以对其进行调用。
这个地方我们不能调用ptr2的原因是因为它的指针指向是不正确的,这里的this指针传的是ptr2,因此在实际调用之前进行一定程度的封装与修正。
所以汇编中这段代码的作用实际上就是修正this指针。
也可以理解为Base1这里的地址是真地址,Base2处的地址是“假”地址,它被接下来封装在这个过程中进行了修正。
抽象类:
接下来我们来介绍一下抽象类,而在抽象类中有一个概念,它在这里叫做纯虚函数。
那么这里纯虚函数的代码该怎么写?
像这里在虚函数后面加上赋值0之后的函数被我们称为纯虚函数。
而这里的抽象类就是包含着纯虚函数的类,它想表达的意思就是,这个类在现实世界中没有对应的实体。
因此抽象类这里就有一个特点,那就是它不能实例化对象。
而不能实例化对象也就意味着正常情况下我们并不能去使用它。
而且解决抽象类使它可以被正常使用的方法也是很简单。
在这里我们只要对其进行重写就可以去使用它了,这里要注意的是必须要经历重写操作才能使用,如果只是继承没有重写的话,代码函数会报错。
这里还要注意一个点,因为父类的纯虚函数没有对象,所有它里面没有虚表的寻找。但是派生类对其进行了重写操作,这样会是派生类中有对象,派生类对比父类是存在虚表的。
而纯虚函数在多态中有一个作用,那就是它间接强制派生类重写虚函数。(override是进行检查二者不同)
代码:
#include <iostream>
using namespace std;
//class Person
//{
//public:
// virtual Person* BuyTicket() const
// {
// cout << "全价票" << endl;
// return 0;
// }
//};
//
//class Student :public Person
//{
//public:
// virtual Student* BuyTicket() const
// {
// cout << "半价票" << endl;
// return 0;
// }
//};
//
//void func(const Person& p)
//{
// p.BuyTicket();
//}
//
//int main()
//{
// func(Person());
// func(Student());
//
// return 0;
//}
//class Person
//{
//public:
// virtual ~Person()
// {
// cout << "~Person()" << endl;
// }
//};
//
//class Student :public Person
//{
//public:
// virtual ~Student()
// {
// cout << "~Student()" << endl;
// }
//};
//
//int main()
//{
//
// return;
//}
//class Car
//{
//public:
// virtual void Drive()
// {
//
// }
//};
//
//class Benz :public Car
//{
//public:
// virtual void Drive() override
// {
// cout << "Benz-舒适" << endl;
// }
//};
//
//int main()
//{
//
// return 0;
//}
//class Car
//{
//public:
// virtual void Drive() = 0;
//};
//
//class Benz :public Car
//{
//public:
// virtual void Drive()
// {
// cout << "Benz-舒适" << endl;
// }
//};
//class Base
//{
//public:
// virtual void Func1()
// {
// cout << "Func1()" << endl;
// }
//
// virtual void Func2()
// {
// cout << "Func2()" << endl;
// }
//
// void Func3()
// {
// cout << "Func3()" << endl;
// }
//
//private:
// char _b = 1;
//};
//
//int main()
//{
// cout << sizeof(Base) << endl;
//
// Base b1;
//
// return 0;
//}
// class Person
//{
//public:
// virtual void BuyTicket()
// {
// cout << "全价票" << endl;
// }
//
// virtual void Func1()
// {
// cout << "Person::Func1()" << endl;
// }
//
// virtual void Func2()
// {
// cout << "Person::Func2()" << endl;
// }
//
// int _a = 0;
//};
//
//class Student :public Person
//{
//public:
// virtual void BuyTicket()
// {
// cout << "半价票" << endl;
// }
//
// virtual void Func3()
// {
// cout << "Person::Func3()" << endl;
// }
//
// int _b = 1;
//};
//
//void Func(Person& p)
//{
// p.BuyTicket();
//}
//typedef void(*FUNC_PTR)();
//
打印函数指针数组
//void PrintVFT(FUNC_PTR* table)
//{
// for (size_t i = 0; table[i] != nullptr; i++)
// {
// printf("[%d]:%p->", i, table[i]);
//
// FUNC_PTR f = table[i];
// f();
// }
// printf("\n");
//}
int main()
{
Person ps;
Student st;
int vft1 = *((int*)&ps);
PrintVFT((FUNC_PTR*)vft1);
int vft2 = *((int*)&st);
PrintVFT((FUNC_PTR*)vft2);
return 0;
}
//
//int main()
//{
// int i = 1;
// int d = 1.1;
// cout << i << endl;
// cout << d << endl;
//
// Person ps;
// Person* ptr = &ps;
//
// ps.BuyTicket();
![img](https://img-blog.csdnimg.cn/img_convert/473b9b43b51106f8551818d64c61417a.png)
![img](https://img-blog.csdnimg.cn/img_convert/5b1c89c61e753c2c6a77106b7dbc5311.png)
**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!**
**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**
**[如果你需要这些资料,可以戳这里获取](https://bbs.csdn.net/topics/618668825)**
!= nullptr; i++)
// {
// printf("[%d]:%p->", i, table[i]);
//
// FUNC_PTR f = table[i];
// f();
// }
// printf("\n");
//}
int main()
{
Person ps;
Student st;
int vft1 = *((int*)&ps);
PrintVFT((FUNC_PTR*)vft1);
int vft2 = *((int*)&st);
PrintVFT((FUNC_PTR*)vft2);
return 0;
}
//
//int main()
//{
// int i = 1;
// int d = 1.1;
// cout << i << endl;
// cout << d << endl;
//
// Person ps;
// Person* ptr = &ps;
//
// ps.BuyTicket();
[外链图片转存中...(img-LLkn36aF-1715811763189)]
[外链图片转存中...(img-ecgtHWCH-1715811763190)]
**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!**
**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**
**[如果你需要这些资料,可以戳这里获取](https://bbs.csdn.net/topics/618668825)**