C++之指向子类的父类指针

父类指针的作用域如何被限制在父类中及成员函数如何调用

Posted by UNMOVE on May 14, 2020

前言

今天在跑C++代码时忽然对一个非常常见的现象产生了疑惑,就是将一个父类指针和一个子类指针指向相同的子类对象时,C++底层是怎么做到父类指针访问父类的作用域,而子类指针访问子类的作用域的,下面就进入正题。

问题起源

整个问题起源于一个关于类的继承的实验,最开始我以为的是,既然父类指针和子类指针指向的是同一个子类对象,那么通过指针调用的成员函数一定就是子类的成员函数了,然而噩梦来了。

注意:本篇文章所指的成员函数均为实函数

  class T1 {
  public:
      int a;
      void print () {
          cout << this << endl;
          cout << "T1 this->a: " << this->a << endl;
          cout << a << endl;
      }
  };
  class T2 : public T1{
  public:
      int a;
      T2 (int a) : a(a) {
          T1::a = a + 1;
      }
      void print () {
          cout << this << endl;
          cout << "T2 this->a: " << this->a << endl;
          cout << a << endl;
      }
  };

  int main () {
      T2 *t2 = new T2(1);
      T1 *t1 = t2;
      t2 -> print();
      t1 -> print();

      return 0;
  }

上面的代码在运行过后的截图如下

2020-05-04-CodingResult.png

从运行结果可以看出,指向同个子类对象的指针t1和t2,在调用print方法时分别调用了父类方法和子类方法。看到这里我不禁思考

对于一个子类对象,它是如何调用到父类的成员函数的?

成员函数的调用

经过查阅相关资料发现,类的成员函数是隐式的inline函数。这也就意味着对于成员函数的调用不是运行时确定的,而是编译时期确定的!那么指针t1访问到T1的函数则是因为,编译器在编译到t1->print时,检测到是在调用成员函数,因此需要使用某个成员函数体对该行进行替换。那么成员函数体从哪里来呢?编译器一看,*t1的类型是T1,因此就将类T1的print函数拉过来替换,同时将t1作为print的隐藏this参数。

好了,现在总结一下就是:对于任何非虚的成员函数的调用都是在编译时期确定的。

嗯,现在可以理解为什么会调用到父类的成员函数了,但是还有一个问题

T1的print函数和T2的print函数接收的this的地址是一样的,只是传入时类型不同(分别是T1*和T2*),但是访问到的a却分别是父类的和子类的,C++是怎么实现作用域限定的?

作用域

原来在程序运行时,子类对象的前半部分放置的是继承自父类的所有成员变量,像下面这样

2020-05-14-ObjectMemory.png

而且对于对象成员变量的访问,编译器会在编译过程中将其转化为对相对地址的访问,比如T1的print中this->a会被编译为*((int *)(this)),而T2的print中this->a会被编译为*((int *)((int *)this + 1)),如此一来,调用T1的print就会输出*(int *)this,此时this指向的位置恰好是T2对象中父类T1的成员变量所在位置,此时就体现出了作用域的特性。

为了便于理解问题,我还写了新的代码,如下

  int main () {
    T2 t2(1);
    cout << &t2 << " " << &(((T2 *)(&t2)) -> a) << " " << (((T2 *)(&t2)) -> a) << endl;
    cout << &t2 << " " << &(((T1 *)(&t2)) -> a) << " " << (((T1 *)(&t2)) -> a) << endl;

    return 0;
  }

2020-05-14-CodingResult2.png

真是有趣啊!