zoukankan      html  css  js  c++  java
  • 探秘C++机制的实现

        我曾经自学过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

    每个类有特定的虚表地址,每个对象会保存这个虚表地址,应该想到了吧,偷懒,不写了。

    综上。可以看到,面向对象机制在底层并不特别,机制的实现主要靠的是编译器。

  • 相关阅读:
    为图片指定区域添加链接
    数值取值范围问题
    【leetcode】柱状图中最大的矩形(第二遍)
    【leetcode 33】搜索旋转排序数组(第二遍)
    【Educational Codeforces Round 81 (Rated for Div. 2) C】Obtain The String
    【Educational Codeforces Round 81 (Rated for Div. 2) B】Infinite Prefixes
    【Educational Codeforces Round 81 (Rated for Div. 2) A】Display The Number
    【Codeforces 716B】Complete the Word
    一个简陋的留言板
    HTML,CSS,JavaScript,AJAX,JSP,Servlet,JDBC,Structs,Spring,Hibernate,Xml等概念
  • 原文地址:https://www.cnblogs.com/nanshu/p/2891101.html
Copyright © 2011-2022 走看看