C++分析研究  
C++
日历
<2013年6月>
2627282930311
2345678
9101112131415
16171819202122
23242526272829
30123456
统计
  • 随笔 - 92
  • 文章 - 4
  • 评论 - 4
  • 引用 - 0

导航

常用链接

留言簿

随笔档案

文章档案

搜索

  •  

最新评论

阅读排行榜

评论排行榜

 
  我曾经自学过C++,现在回想起来,当时是什么都不懂。说不上能使用C++,倒是被C++牵着鼻子走了。高中搞NOIP并不允许使用STL库,比赛中C++面向对象的机制基本没有什么用武之地,所以高中搞NOIP名为用C++,其实就是c加上了cout和cin。
 
   前几天看韩老师的《老码识途》,里面记录了一些C++面向对象机制的探索,又勾起了我的兴趣。而这个学期自学了汇编,又给了我自己动手探索提供了能力基础,自己上手以后,从一个更加底层的视角看C++机制的实现,让我在黑暗中摸到了驯服C++的缰绳。
 
   引用:
 
   本质上是指针,这一点即使大家没有看反汇编应该也是猜到了。
 
   对象在内存上的布局:
 
   1: class Father
 
   2: {
 
   3: int iA_;
 
   4: int iB_;
 
   5:
 
   6: void FuncA();
 
   7: void FuncB();
 
   8: };
 
   9:
 
   10: class Child : Father
 
   11: {
 
   12: int iC_;
 
   13: void FuncC();
 
   14: };
 
   一个Father对象里只包含 (低地址 –> 高地址) : iA_,iB_。也就是一个Father对象的大小是8个字节,函数并不会占用内存空间托福答案
 
   为什么不会?
 
   其实类的成员函数可以看做本质上与普通函数相同。
 
   编译器在编译的时候就知道函数的位置,所以调用普通函数的时候会直接 call 函数地址(偏移)。也就是被硬编码了,函数的地址是固定的( 不考虑重定位之类的情况 )。
 
   而成员函数的调用也是如此,只是编译器还多做了一件事情,就是判断这个对象有没有调用这个函数的“权限”(函数不是你声明的,当然无权调用),“权限”不够就会报错,告诉那个对象类型没有这个方法。
 
   所以,类对象的大小与这个类的方法数多少是没关系的。成员函数和普通函数本质上一样,实现这个机制,要靠编译器来做工作雅思答案
 
   this指针:
 
   成员函数与普通函数不同之处之一就是访问对象的数据。
 
   要访问一个对象的元素,说白了就是要找到这个元素所在的内存位置,也就是要有指针。
 
   我们没有看到传递this指针,因为这件事又是编译器帮我们做了。
 
   反汇编会看到对象调用一个方法的时候,会将这个对象的首部地址赋值给ecx寄存器,通过寄存器来传递this指针。
 
   我们在成员函数里可以不需明写this指针地调用对象元素,还是因为编译器帮我们多做了一步“翻译”。
 
   私有化:
 
   不多说,就是编译器在编译阶段通过源码来判断某个元素是不是能够被访问,某个方法是不是能够被调用,运行的时候并不会有访问限制。看代码:
 
   1: #include <stdio.h>
 
   2:
 
   3: class Exp
 
   4: {
 
   5: int iA_;
 
   6: int iB_;
 
   7:
 
   8: public:
 
   9: Exp()
 
   10: {
 
   11: iA_ = iB_ = 0;
 
   12: }
 
   13: void Out()
 
   14: {
 
   15: printf("%d \t %d \n",iA_,iB_);
 
   16: }
 
   17: };
 
   18:
 
   19: int main()
 
   20: {
 
   21: Exp oA;
 
   22: void *pC = &oA;
 
   23:
 
   24: oA.Out();
 
   25: *(int*)pC = 1;
 
   26: *(int*)((int)pC+4) = 2;
 
   27: oA.Out();
 
   28:
 
   29: return 0;
 
   30: }
 
   结果是: 0 0
 
   1 2
 
   虽然 iA_,iB_是私有的,但是还是被外界修改了。因为编译器无法知道我干了这事(显式的 oA.iA_ = 1 就被发现了哈)
 
   构造与析构:
 
   说道底还是编译器帮我们在多做了一些工作,生成了一些额外代码。
 
   需要注意的是:
 
   1: void Test( Father oP )
 
   2: {
 
   3: }
 
   4:
 
   5: int main()
 
   6: {
 
   7: Father oA;
 
   8: Test(oA);
 
   9: return 0;
 
   10: }
 
   会调用拷贝构造函数。
 
   重载:
 
   一样还是编译器的功劳,C++最后生成的函数名是与参数有关的,所以又不同参数的函数最后生成的函数名不同,看似同名,实则不同。在函数调用的时候,编译器会判断参数的类型,相应的可以生成一个函数名进行“匹配”。( 当然不止这么简单,还会考虑发生类型转换的情况 )
 
   继承:
 
   从内存布局的角度上看
 
   1: struct Child : Father
 
   和
 
   1: struct Child
 
   2: {
 
   3: Father o;
 
   4: //other
 
   5: };
 
   相同(虚函数情况后面讨论)。子类的前面部分和父类是一样的。
 
   所以一个接受 Father * 参数的函数可以接受 Child *参数,而且转换是安全的北美托福答案
 
   有 Father & 类型参数的函数可以接受 Child &,但是继承方式要public。But , why ?
 
   protected和private继承模式,子类继承的父类的接口对外都是隐藏的,所以以一个Father &传入的参数所有的方法元素原则上是不可用的,用了肯定是违反规则的,编译器判定这一点,所以报错。
 
   虚函数:
 
   比较特别的是这个。
 
   Question:为什么需要虚函数?
 
   网上看到的答案:基类可以通过虚函数对子类的相识功能进行管理。(我的C++primer被借走以后就此失踪,所以只能网上找了)。
 
   虚函数具体怎么回事就不细说了,讨论一下背后的机制。
 
   为了能够实现虚函数,每个有虚函数的类有一张对应的虚表。这个虚表储存在只读内存区,记录了对应函数的地址。(PS:一个类就只有一个虚表)
 
   每个类对象都要保存一个虚表指针,保存本类的虚表地址。所以你使用 Father *指针指向一个Child对象,调用的虚函数是Child的。
 
   虚表指针保存在每个对象的首部。
 
   1: class Child : Father
 
   2: {
 
   3: int iC_;
 
   4: void FuncC();
 
   5: virtual void VF();
 
   6: };
 
   现在这个Child对象较前面的多了四个字节。内存布局(从低地址到高地址)是:虚表指针__vfptr,iA_,iB_,iC_。
 
   好。问题来了,Child继承了Father,但是Father的函数并没有为Child再量身定做一次,也就是说无论是Father对象还是Child对象,他们调用FuncA()都是同一个函数。但是Father并没有__vfptr,Child对象在头部多了这个,FuncA()中用this指针定位iA_和iB_不是都不正确吗?
 
   现象告诉我们FuncA()是可以正确访问iA_和iB_,所以推测Child对象在调用FuncA的时候,传的不是真正的首部地址,而是往后偏移了四个字节托福改分
 
   反汇编,确实如此。这么说Father类里不能调用虚函数了?当然,Father都还不知道虚函数这回事,怎么在FuncA中调用。
 
   还有一个有趣的现象:
 
   1: #include <stdio.h>
 
   2:
 
   3: class Base
 
   4: {
 
   5: public:
 
   6: virtual void ShowID()
 
   7: {
 
   8: printf("Base\n");
 
   9: }
 
   10: };
 
   11:
 
   12: class CB : public Base
 
   13: {
 
   14: public:
 
   15: virtual void ShowID()
 
   16: {
 
   17: printf("CB\n");
 
   18: }
 
   19: };
 
   20:
 
   21: class CC : public Base
 
   22: {
 
   23: public:
 
   24: virtual void ShowID()
 
   25: {
 
   26: printf("CC\n");
 
   27: }
 
   28: };
 
   29:
 
   30: void Test( CB& oB )
 
   31: {
 
   32: oB.ShowID();
 
   33: }
 
   34:
 
   35: int main()
 
   36: {
 
   37: Base oBase;
 
   38: CB oB;
 
   39: CC oC;
 
   40:
 
   41: CB* pCB = &oB;
 
   42:
 
   43: *(int*)(&oB) = *(int*)(&oC); //修改虚表指针
 
   44: oB.ShowID();
 
   45: ((CB*)(&oB))->ShowID();
 
   46: pCB->ShowID();
 
   47: Test(oB);
 
   48:
 
   49: return 0;
 
   50: }
 
   猜猜结果啊,买定离手。
 
   结果是:CB CB CC CC
 
   在43行的地方,修改了oB的虚表指针,让其指向CC类的虚表。
 
   但是oB.ShowID()没理会我们的修改,还是调用CB类的ShowID。反汇编,发现他没走“获取虚表指针,在虚表中得到相应的函数地址”这一套,直接调用了。因为一般人不会闲着蛋疼去改对象的虚表指针的,对象的类型是明确的,编译器可以通过这些信息确定调用的函数地址,所以没必要走他一套,这样效率还更高托福答案
 
   而pCB->ShowID()就不同了,他很乖地地走了流程,因为一个父类指针可以指向一个子类对象,编译器无法找信息,所以走流程托福改分
 
   那现在纠结了,为神马 ((CB*)(&oB))->ShowID() 输出CB。
 
   反汇编看,发现编译器又擅自做主,没有走指针的流程。
 
   那你猜猜((Base*)(&oB))->ShowID();输出的是什么?CC。
 
   比较二者的差异,可以大概发现一些端倪,什么时候走流程,什么时候不走。
 
   最后是Test(oB)了,前面说过引用的本质是指针,所以这个结果很好理解。
 
   还有,想过
 
   1: void Test2( Base oP )
 
   2: {
 
   3: oP.ShowID();
 
   4: }
 
   拷贝的时候有没有拷贝虚表指针吗?试试就知道,厄…发现没有。
 
   前面说过这样会调用拷贝构造函数,但是你在这个函数你没有写虚表指针的赋值。但是邪恶的编译器已经帮你悄悄加上去了哈哈哈哈~。(唉?节操呢)
 
   RTTI
 
   每个类有特定的虚表地址,每个对象会保存这个虚表地址,应该想到了吧,偷懒,不写了。
 
   综上。可以看到,面向对象机制在底层并不特别,机制的实现主要靠的是编译器。
 
posted on 2013-04-16 11:42 HAOSOLA 阅读(304) 评论(0)  编辑 收藏 引用

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理


 
Copyright © HAOSOLA Powered by: 博客园 模板提供:沪江博客
PK10开奖 PK10开奖