当前位置: 首页 > news >正文

旅游电网站建设目标个人求职网站履历怎么做

旅游电网站建设目标,个人求职网站履历怎么做,全国广告投放平台,工邦邦官网文章目录 前言一、虚函数表二、一道经典的例题三、深度剖析多态的条件之一#xff1a;为什么必须是父类的指针或引用四、深度剖析多态的条件之二#xff1a;为什么是虚函数的重写/覆盖#xff1f;五、虚函数表的一些总结六、关于Func3的验证七、动态绑定与静态绑定八、总结 … 文章目录 前言一、虚函数表二、一道经典的例题三、深度剖析多态的条件之一为什么必须是父类的指针或引用四、深度剖析多态的条件之二为什么是虚函数的重写/覆盖五、虚函数表的一些总结六、关于Func3的验证七、动态绑定与静态绑定八、总结 前言 在前面我们也了解了多态的定义、概念、实现。对于多态的使用有很多需要注意的细节可谓到处都是坑了解了多态的使用那么现在我们来了解一下多态的原理吧。 一、虚函数表 我们先来猜猜下面程序的运行结果是多少 class Base { public:virtual void Func1(){cout Func1() endl;} private:char _c 1; }; int main() {cout sizeof(Base) endl;return 0; }我们可能会以为是1实际上运行结果是8 那么为什么是8呢 我们可以进入调试观察一下我们会发现它里面似乎多了一个指针 这个指针是四字节的话那么内存对齐一下刚好是8个字节。 那么这个指针究竟是何方神圣呢实际上这个指针是虚函数指针(v代表虚拟f代表函数ptr是指针)。从后面的vftable也可以看出来它是一个虚函数表 从这里我们也可以知道我们一般不使用多态的话最好还是不要加上virtual因为是有开销的。 这个虚表里面存储的就是虚函数的地址。而虚函数是存放在代码段的。 如果我们有两个虚函数的话那么这个虚表里面就有两个虚函数的地址 以上是由于虚函数导致的对象中的一些变化虚函数是应用于重写的。那么重写的时候会发生什么呢 我们接下来使用如下代码 class Person { public:virtual void BuyTicket() { cout 买票-全价 endl; }int _a 1; }; class Student : public Person { public:virtual void BuyTicket() { cout 买票-半价 endl; }int _b 1; }; void Func(Person p) {p.BuyTicket(); } int main() {Person p;Func(p);Student s;Func(s);return 0; }我们对其进行分析下面是监视窗口里面的样子 我们不妨将他们用下图代替即用下面的图更能清晰的表达他们的关系 根据上面的图中我们可以注意到所谓的重写其实从原理层的角度来看其实就是将虚表里面的地址给覆盖了。才导致调用不同的函数。所以重写是语法层的概念覆盖是原理层的概念。 对于子类的虚表我们也可以认为是将父类的虚表给拷贝下来了然后在将重写的给覆盖上去。 这个时候我们就知道了如何实现的指向父类调父类指向子类调子类了。 所以现在我们知道了多态是如何实现的。如果是父类的对象进行调用的时候那么自然就是调用它的虚表里面的函数如果是将子类对象使用指针或者引用进行切片的话本质上还是指向子类只不过是指向子类中的父类的那一部分罢了而我们这里的虚表中的地址已经被替换了。所以当然可以实现调用不同的函数了。 其实如果是普通的调用的话那么它在编译的时候地址已经被确定了。 如下就是普通调用的时候在编译的时候地址早已被确定好了所以它恒定的调用一个函数。这里也就解释了为什么必须是基类的指针和引用。如果是对象的切片的话这里的虚表中的内容是不会被切片过去的p调用函数的时候地址在编译时候早已被确定了。 如果符合多态的话运行时到指向对象的虚函数表中找调用对象的地址。不是在编译时候就确定了地址了 我们也不管他是子类还是父类即便是子类经过切片后也是一个父类。我们只需要找到对应的虚表中的地址就可以了。我们可以看到同一个函数多态调用的时候指令都变多了 二、一道经典的例题 我们来看下面这道题猜猜它选什么呢 class A { public:virtual void func(int val 1) { std::cout A- val std::endl; }virtual void test() { func(); } }; class B : public A { public:void func(int val 0) { std::cout B- val std::endl; } }; int main() {B* p new B;p-test();return 0; }这道题的运行结果为 看到这里我们肯定已经蒙了这是为什么呢为什么是这么一个出乎意料的结果呢 我们现在来分析一下代码 首先我们定义了一个派生类的指针指向了B对象。然后我们现在想用这个派生类的指针去调用test函数这里是可以去调用的因为子类继承了父类 在test函数里面只有一个功能就是调用func函数注意这里的func函数是由this指针来进行调用的只不过是this指针隐藏了。 现在我们来思考一下这里调用func是不是多态调用呢 我们知道多态调用有两大条件父类的指针或引用去调用虚函数这个虚函数必须是重写的虚函数。 那么这里的this指针是父类的指针吗答案是这里的this指针确实是父类的指针而不是派生类的指针 为什么是A*父类的指针呢因为这里的func函数是继承下来的。这里的继承并不是单纯的将test函数在派生类生成了一份编译器不会那样做的。 继承的对象模型是这样做的它的对象模型分为两部分一部分是将父类的整体当成一个成员给拿下来这里父类会自己内存对其等操作然后另一部分就是自己的本来的成员经过与父类对象进行内存对齐以后整个进行建模。然后这些成员函数它都是在代码段的它并不会生成多份的。编译的时候是检查语法的先去派生类里面去找找不到再去父类里面去找。 所以test不会有两份所以这里只能是A*指针了。这样就满足了多态的第一个条件了。或者说这里发生一个切片B*指针切片给了A*类型的指针。 第二个条件是虚函数的重写那么这个func满足虚函数的重写吗其实是满足的首先有基类和派生类里面都有func函数这两个函数满足虚函数加三同的条件注意形参的类型相同指的是类型的相同有没有缺省参数缺省参数是多少跟他们没有任何关系即便是形参名字不同也是无所谓的。 所以现在满足了多条的条件已经是多态的调用了。我们知道多态的调用看的是指向的对象是哪里。而这里我们的A*的指针是由B*的指针切片得到的所以这里实际指向的是一个派生类那么自然就调用的是派生类的func了 此时我们以为得到了正确答案B-0实则不然我们又调入了一个大坑里面我们要注意多态改变的是函数的实现虚函数加上三同只是可以告诉我们说这个构成了多态。换言之多态在调用的时候前面的部分即返回值形参函数名这些看的是基类的部分而实现的部分看的是多态的调用即指向的对象的那一部分。而在这里形参使用基类中的1实现打印B。 所以最终结果为B-1 甚至于我们还可以将派生类中的缺省参数给去掉也是没有任何问题的 甚至于我们可以直接换名 但是我们不可以连形参名字都不写了因为我们在里面毕竟用到了val了如果连val名字都不写的话是不行的 所以说虚函数的重写重写的只是实现那一个壳用的还是基类的。这里也印证了为什么派生类可以不加virtual。因为只是重写的实现。 如果我们将上面的题稍作修改如下所示那么结果又会如何变化呢 class A { public:virtual void func(int val 1) { std::cout A- val std::endl; } }; class B : public A { public:void func(int val 0) { std::cout B- val std::endl; }virtual void test() { func(); } }; int main() {B* p new B;p-test();return 0; }这里比较有意思的是我们只是将test这个函数换在了B类里面即派生类里面这样的话我们p调用test的时候this指针和p一样了都是派生类的指针类型这已经不构成多态的条件了所以是一个普通的调用就直接看的是派生类中的这个函数了所以结果为B-0 三、深度剖析多态的条件之一为什么必须是父类的指针或引用 我们在回过头来看一下多态的条件为什么是那两条1.基类的指针或引用去调用虚函数2.被调用的函数必须是重写的虚函数 为什么必须是父类的指针或引用子类的指针或引用为什么不可以呢为什么不能是父类的对象呢 先回答第一个问题因为只有父类的指针才可以指向子类和父类如果是子类的指针的话就只能指向子类了不能指向多种形态了。这个问题还算比较容易理解 再来回答第二个问题我们知道对象的切片和指针与引用的切片是有一些不同的我们先要知道对象切片和指针切片的差异是什么。为了演示这个差异我们使用如下代码 class Person { public:virtual void BuyTicket() { cout 买票-全价 endl; }virtual void Func1() {};virtual void Func2() {}; protected:int _a 0; }; class Student : public Person { public:virtual void BuyTicket() { cout 买票-半价 endl; } protected:int _b 1; }; void Func(Person p) {p.BuyTicket(); } int main() {Person ps;Student st;return 0; }如下是监视窗口中的样子 我们使用如下方式进行展现这样方便我们进行观察 此时我们还是比较容易理解的这里两个对象分别有他们自己的虚函数表。这里的虚函数表严格来说应该是一个指针指向着虚函数表。 我们知道下面的三种方式都是切片那么他们的差异究竟在哪里呢 ps st; Person* ptr st; Person ref st;首先毋庸置疑的是如果是指向父类的指针那么它指向的是一个父类的对象看到的自然就是父类的虚表了。 如果是指向子类的指针或引用的话那么它指向的是一个子类中的父类的那一部分看到的其实是子类中的虚表这个虚表是经过虚函数重写覆盖过的。 所以说指针和引用的切片他们是不存在任何的拷贝的问题的。 而对象的切片就存在拷贝的问题了。 当我们使用对象的切片的时候子类中的父类部分的成员变量肯定是都会被拷贝过去的但是虚表会被拷贝过去吗我们可以测试一下 为了方便我们观察我们可以提前先修改一下派生类中_a的值 然后我们在使用对象的切片如下图所示是未切片的时候 如下所示是切片发生之后 我们已经发现对象的切片并不会改变虚表所以虚表是不会进行拷贝的 那么为什么不拷贝虚表呢拷贝虚表会带来什么问题呢 我们可以这样思考一下假如我们将派生类中的虚表给拷贝过去了那么我们使用ps这个父类对象给取出它的地址以后使用这个指针去调用它里面的函数的话就会反而调用了派生类中的函数。这个明显有点奇怪。因为指向父类的居然调用了子类的函数。这明显不符合多态的要求。毕竟多态是指向什么类就调用什么类的。这样做显然就会乱套的。 所以我们得到一个结论子类赋值给父类对象切片不会拷贝虚表。如果拷贝虚表那么父类对象虚表中的究竟是父类虚函数还是子类虚函数就不知道了因为我们并不知道究竟这个对象有没有被赋值切片过。总之就乱套了 上面这个结论也就回答了我们前面的问题为什么多态的条件不能是父类的对象。 四、深度剖析多态的条件之二为什么是虚函数的重写/覆盖 在前面我们也已经提到过虚函数的重写/覆盖本质就是是什么 在语法层面称之为重写重写的是它的实现。所以有时候我们也会提出一个概念普通的函数的继承称为实现继承而多态虚函数的重写其实就是一个接口继承然后重写它的实现 在原理上就是说将父类的虚函数表给拷贝下来然后将子类中重写的部分给覆盖。 其次因为只有完成了虚函数的重写那派生类的虚表里面才能是派生类的虚函数。这样的话这个基类指针才能做到指向父类调用父类指向子类调用子类。 五、虚函数表的一些总结 派生类对象st中也有一个虚表指针st对象由两部分构成一部分是父类继承下来的成员虚表指针也就是存在这一部分的另一部分是自己的成员。 基类b对象和派生类d对象虚表是不一样的这里我们发现BuyTicker完成了重写所以d的虚表中存的是重写的Student::Buyticker所以虚函数的重写也叫作覆盖覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法覆盖是原理层的叫法 另外Func1和Func2继承下来后是虚函数所以放进了虚表如果Func2不是虚函数那么它也继承下来了但是因为不是虚函数所以不会放进虚表 虚函数表本质是一个存虚函数指针的指针数组一般情况这个数组最后面放了一个nullptr 。注意不是所有的编译器都会给的g编译器就没有给而且有时候vs的编译器有一些问题不会给这个nullptr这时候我们可以自己清理一下解决方案然后重新编译一下就有了这里算是一个编译器的bug 下面的就是给了nullptr的 总结一下派生类的虚表生成 a. 先将基类中的虚表内容拷贝一份到派生类虚表中 b. 如果派生类重写了基类中某个虚函数用派生类自己的虚函数覆盖虚表中基类的虚函数 c. 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后 注意c这个小点中虽然它会将这个添加到派生类虚表的最后但是我们的监视窗口有时候是看不见的如下所示我们并没有看到Func3的虚函数表中的地址。 但是我们是可以从内存窗口看到有一个地址的这个地址就是Func3的虚表中的地址。这里算是一个监视窗口的一个bug 所以说监视窗口和内存窗口有时候还是有一些不一样的。准确的来说这里我们也不能断言说这里一定是Func3函数的地址。因为我们并没有给出证明所以后面我们会给出一个证明。 还有一点是虚表是存储在哪里的呢是栈区or堆区or数据段静态区or代码段常量区这四个中的哪一个呢 首先我们就可以排除的是堆区因为堆区还需要new,delete一下编译器大概率是不会这样做的 然后我们还可以排除的是栈区因为如果是存在栈区的话那如果是两个栈帧的话里面的虚表的地址肯定是不一样的而我们经过下面的测试发现地址是一样的也就是说他们共用虚表所以可以排除栈区。当然其实也不能百分之百排除掉栈区因为万一存储在main函数的栈帧中呢但是大概率还是不会存储在main中的。 同时上面的情形还说明了一件事同类型的对象共用虚表 然后我们就可能会去猜测是静态区中存储着虚表实际上不是的虽然说网上的很多答案都是静态区不过这个答案其实是错误的。 我们可以使用如下代码去验证 class Person { public:virtual void BuyTicket() { cout 买票-全价 endl; }virtual void Func1() {};virtual void Func2() {};int _a 0; }; class Student : public Person { public:virtual void BuyTicket() { cout 买票-半价 endl; }virtual void Func3() {}; protected:int _b 1; };int main() {int a 0;printf(栈区%p\n, a);int* p new int;printf(堆区%p\n, p);static int b 0;printf(静态区数据段 %p\n, b);const char* str hello world;printf(常量区代码段 %p\n, str);Person ps;printf(Person: %p\n, *(int*)ps);Student st;printf(Student: %p\n, *(int*)st);return 0; }我们先来解释一下这段代码前面都很简单最后两个打印的时候由于对象里面是没有虚表的但是有一个虚表指针并且这个指针就是第一个成员变量所以我们ps的地址就是虚表指针的地址然后我们为了可以直接用这个虚表指针的地址去打印出来虚表所在的地址于是我们就对其进行强制类型转换为int*因为我们的指针是四字节的。然后我们直接解引用就可以拿到这个虚表指针所指向的值了。由于这个虚表指针本身就是一个二级指针里面存储的就是一个地址这个地址所指向的就是虚函数所存储的地址了。 或许你已经被绕晕了不要紧我们来画个图来直观的感受一下 而我们上面所进行的操作正好取出来的就是绿色方块里面的值也就是一个地址这个地址就是虚函数表的地址。相信大家这会儿已经听懂了吧 而我们最终的运行结果是这样的 我们对比后发现与常量区即代码段的数值最为接近。所以虚表应该存储在常量区/代码段 那么虚函数存储在哪里呢 如果直接打印地址的话恐怕并不好打印有点繁琐我们不如直接在监视窗口里面观察 可以注意到虚函数显然距离常量区更近一些。所以也是存储在常量区的 六、关于Func3的验证 我们在前面中提到了监视窗口中的虚表少了一个func3的地址但是当我们进入内存查看的时候存在一个指针。那么这个指针究竟是不是func3我们还需要进行验证。 我们想要验证这个东西我们得先将虚表里面的地址看能否给拿出来。只要能拿到虚表里面的函数地址我们就可以去调用这些函数从而判断是不是该函数 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 Student::Func3() endl;}; protected:int _b 1; }; typedef void (*Func_Ptr)(); void PrintVFT(Func_Ptr* table) {for (int i 0; table[i] ! nullptr; i){printf([%d]:%p\n, i, table[i]);}cout endl; } int main() {Person ps;Student st;int vft1 *(int*)ps;PrintVFT((Func_Ptr*)vft1);int vft2 *(int*)st;PrintVFT((Func_Ptr*)vft2);return 0; }如上所示的代码就是可以打印出虚表上面代码的原理是这样的由于虚表是一个函数指针数组每一个函数指针都是void(*)()类型的指针。所以我们直接使用typedef一下方便我们使用这种类型的指针然后我们在想办法取出虚表的地址。这个取法在前文中已经提及了。然后我们就可以直接去打印这个虚表了。注意我们这里使用的vs2022 ,x86环境的我们的指针都是4字节的其次vs在虚表结束的时候是会添加一个nullptr的如果是Linux环境的话首先默认是x64环境的所以指针是八字节的在取地址的时候就要小心了。我们不能用int类型了可以使用long long类型的。其次Linux环境下最后是不会在虚表的结尾补一个nullptr的所以就不能像我们上面那样使用了。必须得写死了才能打印出虚表。 如下就是我们此时打印出来的虚表 现在我们已经有了虚表中的每一个函数的地址了那么有了函数的地址了再去调用这个函数就非常之简单了我们对前面的代码稍作修改得到如下代码可以去正常访问每一个函数 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 Student::Func3() endl;}; protected:int _b 1; };typedef void (*Func_Ptr)(); void PrintVFT(Func_Ptr* table) {for (int i 0; table[i] ! nullptr; i){printf([%d]:%p-, i, table[i]);table[i]();}cout endl; }int main() {Person ps;Student st;int vft1 *(int*)ps;PrintVFT((Func_Ptr*)vft1);int vft2 *(int*)st;PrintVFT((Func_Ptr*)vft2);return 0; }对于上面的代码我们简要的分析一下我们的目的是为了打印虚表中的每一个函数虚表本质是一个函数指针数组注意它与虚基表是不一样的虚基表是菱形虚拟继承中用来存储偏移量的。虚表是虚函数表简称虚表它本质是就是一个函数指针数组。我们可以给每个虚函数都加上打印。因为我们在前面已经取出来了每一个函数的地址这里就有点类似于回调函数中的做法。我们有了函数地址它就可以当作函数名直接调用这个函数然后观察打印结果就可以验证。 注意在这里我们有时候可能会遇到程序崩溃的情况这是因为vs的一个bug本来在虚表后面是要补一个nullptr的但是我们有时候生成完解决方案以后去修改了代码可能就不会添加这个空指针了。从而导致程序调用野指针程序崩溃。这时候我们只需要重新生成一下解决方案即可解决这个问题。 从上面的运行结果来看是由Func3的那么这里就已经验证了Func3的存在是在虚表中的也就说明了那个指针确实是Func3。至于监视窗口没有显示Func3函数的地址可能是由于编译器的bug 这里其实也说明了一件事我们不要太过于相信监视窗口只有内存窗口里面的才是最真实的 不过需要注意的是上面的代码其实是被精心设计过的它并不是正常的访问方式首先我们的虚表中每一个函数我们的类型都设置成了一模一样的否则的话在调用函数的时候必然因为指针的类型不同而出现问题。 其次我们的函数都是没有访问成员变量的一旦函数里面存在访问成员变量的话可能会出现很多问题。毕竟我们的是非正常访问是没有this指针的。这里的非正常访问方式是无视类域的限制的即便是私有的照样可以访问。因为他们都只是语法层面的限制我们这里直接从内存中去找到对应的地址去调用的。 七、动态绑定与静态绑定 我们有时候又将多态分为静态的多态与动态的多态 所谓静态的多态一般是指编译时的多态也就是函数重载 比如下面的例子 int main() {int i 1;double d 1.1;cout i endl;cout d endl;return 0; }即不同的对象调用不同的函数这些是在编译时候就确定好了的。通过函数名修饰规则等来匹配不同的函数。我们也将之称为静态绑定 静态绑定又称为前期绑定(早绑定)在程序编译期间确定了程序的行为也称为静态多态 。它与普通的调用是一样的在编译时就确定了地址 如下所示现在所处的就是一个普通的调用。它在编译时就确定好了地址。 而下面这个则是多态的调用编译器也不知道调用的到底是谁反正就是通过一系列方法将这里面的函数给取出来去调用 这里也就是动态的多态即运行时的多态他是通过继承虚函数重写实现的多态。 八、总结 本次主要讲解了多态的底层原理。深入浅出的讲解了虚函数表深度剖析了多态的条件以及虚表的很多细节。希望能对大家带来帮助
http://www.hkea.cn/news/14335823/

相关文章:

  • 公司网站建设案例如何修改wordpress主题模板
  • 帮人做网站在徐州被敲诈五万看空间网站
  • 购物网站导航素材代码网站建设要注意
  • 珠海网站建设黄荣mysql 网站空间
  • 有经验的邯郸网站建设企业自己可以做视频网站吗
  • 如何开发手机网站2345官网下载
  • 怎么学网站建设网络营销常用的方法
  • 网站建设需求分析班级山西网站建设排名
  • python 做网站优势拨打12355可以找团员密码吗
  • 广东建设信息公开网站常用的软件开发文档
  • 做购物网站建设的公司php做的网站处理速度怎么样
  • 住房和城乡建设部网站买卖合同西安学校部门定制网站建设公司
  • wordpress中文目录下北京网站优化诊断
  • 台州建设信息网站如何才能做好网络营销
  • dw做网站时怎么改为绝对路径新沂网站建设
  • 宠物网站建设报告wordpress网站打开速度
  • jsp做的网站答辩问题软文发稿网站
  • 昌邑网站建设网站的流量是怎么算的
  • 假链接制作网站做软件用什么编程语言
  • 桂林哪里做网站北京昌平网站设计
  • 济源网站制作月子会所网站建设方案
  • 嘉兴网站备案去哪里有网站模板如何预览
  • html免费网页模板连云港网站seo
  • 股票网站模板 dedecmsphp网站怎么做集群
  • 郑州网站建设网络公司管理咨询公司一般是做什么的
  • 有哪些关于校园内网站建设的法律防做网站
  • 北京自助企业建站模板wordpress文件类型不受支持
  • 把插钉机子拍下怎么做网站大连中小网站建设公司
  • 昆明pc网站建设池州网站seo
  • 网站怎么做才有收录福州网站建设设计公司