C++反汇编第四讲,认识多重继承,菱形继承的内存结构,以及反汇编中
目录: 1.多重继承在内存中的表现形式 多重继承在汇编中的表现形式 2.菱形继承 普通的菱形继承 虚继承 汇编中的表现形式 一丶多重继承在内存中的表现形式 高级代码: class Father1 { public: Father1(){}//空构造 virtual ~Father1(){} //空析构 virtual void Player(){} //玩耍的函数 int m_price;//金钱 }; class Father2 { public: Father2(){} virtual ~Father2(){} virtual void SetMoney(){}//设置金钱 int m_Money; }; class Child : public Father1,public Father2 //继承两个父类 { public: Child(){} virtual ~Child(){} virtual void Eat(){}//吃的函数 }; int main(int argc,char* argv[]) { Child MyChild; //只需要注意这里,以及继承两个父类的地方 return 0; } 通过main函数我们得知,我们生成了一个孩子类的对象.此时按照C/C++的规范,应该先从左往右依次构造父类1,父类2 此时的情况和我们昨天所讲的单继承里面,包含一个成员是一样的.但是有不同之处 1.在子类自身构造中会复写两次虚表. 2.在父类2指向子类的时候,会产生三木目运算的表达式. 2.观看反汇编中的表现形式. 1.main函数下,构造位置处 2.自身构造函数内部 ? ? ? ?可以看出,自身构造中会产生如上代码,首先 1.先构造父类各自的虚表,相当于一开始父类1有父类1的虚表,父类2有父类2的虚表 2.然后自身构造的时候,分别对其自己的父类的虚表进行复写行为.通过这一点,可以简单的断定子类有两个父类. Release下的反汇编 在Release下,因为我们的父类都是空的,所以直接优化了. 但是我们会发现有一个三木运算符的反汇编代码 neg eax sbb eax,eax lea ecx,[this] //获得this指针,简写了 and eax,ecx 这个是一个无分支三目运算的反汇编代码,在讲解C语言的反汇编的时候已经讲解过了,但为什么会出现这个. 首先 如果我们代码写成? father2? *p2 = &ch; 是没有问题的是把,? 父类指针,指向了子类,因为father2在内存中需要加便宜,所以直接加便偏移即可. 但是此时问题来了.如果我们写成 father2 *p2 = NULL; p2 = &ch;? 怎么办,此时还要不要加偏移了? 所以产生了一个三木运算的表达式 p2 ? NULL? : NULL : p2 + offset;? 在经过编译器的一优化,所以变成了上面的代码. 如果不懂father *p2 = &ch,那要从内存角度看了.首先看Debug下的反汇编. ? 我们的father2构造的位置是在内存+8的位置进行构造的,所以此时你如果father2的指针指向子类,那么需要+8对不对,所以如果先给NULL,就不用+8了.所以产生了三木运算符. ? 内存结构图 之后的内存结构图,最后子类自己的构造中需要复写两个父类自己的虚表.而且自己扩展的虚函数会放在父类1的虚表中. ? 总结: 1.识别双父类的时候,看自己的构造中是否进行了两次虚表复写行为,(多个父类则多次构造父类,多次复写父类虚表行为) 2.识别指针的三目运算,父类指针指向子类对象的时候,会产生三目运算表达式? ??例如:? p2 = &child;? p2有可能会有==NULL的情况下,如果不等于NULL,那么自己需要+offset进行指向, 所以产生了三目表达式,? p2 ? NULL ; NULL,p2 + offset; ? ?3.析构中也会进行两个父类析构,然后重新填写虚表的行为.(和构造相反) ? 二丶菱形继承 1.普通的菱形继承讲解 普通的菱形继承,为什么说普通的.请看高级代码 高级代码: class CGrandFather //新添加的爷爷类 { public: CGrandFather(){} virtual ~CGrandFather(){} virtual void Dance(){}//随便写的虚函数 int m_int }; class Father1 : public CGrandFather { public: Father1(){}//空构造 virtual ~Father1(){} //空析构 virtual void Player(){} //玩耍的函数 int m_price;//金钱 }; class Father2: public CGrandFather { public: Father2(){} virtual ~Father2(){} virtual void SetMoney(){}//设置金钱 int m_Money; }; class Child : public Father1,char* argv[]) { Child MyChild; return 0; } 通过上面我们可以得出一个逻辑图: 单其实反汇编那种不是这样的. 从反汇编和内存中可以看出,每一个父类都有一个自己的爷爷类.而且每个父类构造爷爷类的时候,都会填写爷爷类的虚表,并且在自己的构造中对其复写(重写) 所以形成了下面这样的图 ? 所以我们修改我们的高级代码. int main(int argc,char* argv[]) { Child MyChild; //MyChild.m_int = 1; //重点是这句 MyChild.Father1::m_int = 1; return 0; } 我们调用爷爷类中的m_int的时候会出现错误,因为不明确,因为通过上图我们得出,每个父类都有自己的爷爷类,这时候你访问m_int,需要指明那个父类的, 而且你修改父类1的m_int不会影响父类2的m_int.造成了数据冗余的设计. 2.从反汇编的角度下看 1.main函数下构造的位置 2.自身构造内部 ? 构造内部同样进行两次父类先构造的情况,最后复写两个父类的虚表 3.父类1的构造内部 ? 父类1的构造内部又构造爷爷类,爷爷类在自身位置填写虚表,完了之后父类1又复写了. 父类2同上. 得出结论: 1.菱形继承的时候,会有三次改虚表的动作 构造爷爷类的时候修改 构造完爷爷类之后父类修改 构造完父类之后孩子类修改. 2.每个父类都会构造自己的父类. Release下的反汇编表现形式 还是一样,Release下会有优化.指针+不加偏移产生的无分支三目运算. ? ? 三丶虚继承. 通过第普通的菱形继承,我们得出了每一个父类都会有一个父类(爷爷类)然后产生了相同的数据,且数据不明确必须指明调用,所以C++为了解决这种问题,出了一个虚继承. 直接贴上来内存结构: ? 有人说,为什么爷爷类会放在下面.而不是上面,视编译器而定,也可以放在上面.为什么放在上面说来话长,不符合此博客的篇幅. 提示一句:把自己当做编译器. 根据构造的时候先父类构造我们得出了. 首先爷爷类会先构造,但是此时有一个问题,我们要怎么知道爷爷类在哪里.所以这个时候就需要进行记录了. 然后我们上面的内存结构变为了下面这样. ? 每个父类记录一下爷爷类的偏移即可.这个偏移是编译器填写的. 新的问题: 我们怎么知道爷爷类是在下面还是在上面 所以这个偏移是一个结构体的地址,指向了一个2个成员的结构体,结构体+0的偏移是向上的偏移.+4的位置是向下的偏移,我们的父类+上这个偏移就能找到爷爷类了. 看其反汇编代码: 1.main函数下的构造 ? 看出一个特征,push 1了,为什么? 因为要判断是否构造爷爷类,填写爷爷类的虚表,所以push 1,而当父类构造的时候,爷爷类就不要构造了. 2.构造内部. ? 1.首先和参数进行比较,判断是否为1,相等就跳转,不相等就指向, 2.因为条件没有跳转,所以编译器首先给父类填写偏移. 3.如果跳转了,可以看到push 0.然后调用父类构造,其内部一样的判断是否构造爷爷类 4.最后构造爷爷类. ? 识别这个很简单了. 1.看是否构造 2.找偏移,也就是编译器填写偏移的位置,通过偏移的位置加上父类当前位置看一下是不是爷爷类的位置 3.会有两次写虚表的行为,一个是自身改,一个是基类改 4.总共会修改三处虚表,两个父类,一个祖先类的虚表. ?表格 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |