前言
今天在跑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;
}
上面的代码在运行过后的截图如下
从运行结果可以看出,指向同个子类对象的指针t1和t2,在调用print方法时分别调用了父类方法和子类方法。看到这里我不禁思考
对于一个子类对象,它是如何调用到父类的成员函数的?
成员函数的调用
经过查阅相关资料发现,类的成员函数是隐式的inline函数。这也就意味着对于成员函数的调用不是运行时确定的,而是编译时期确定的!那么指针t1访问到T1的函数则是因为,编译器在编译到t1->print时,检测到是在调用成员函数,因此需要使用某个成员函数体对该行进行替换。那么成员函数体从哪里来呢?编译器一看,*t1的类型是T1,因此就将类T1的print函数拉过来替换,同时将t1作为print的隐藏this参数。
好了,现在总结一下就是:对于任何非虚的成员函数的调用都是在编译时期确定的。
嗯,现在可以理解为什么会调用到父类的成员函数了,但是还有一个问题
T1的print函数和T2的print函数接收的this的地址是一样的,只是传入时类型不同(分别是T1*和T2*),但是访问到的a却分别是父类的和子类的,C++是怎么实现作用域限定的?
作用域
原来在程序运行时,子类对象的前半部分放置的是继承自父类的所有成员变量,像下面这样
而且对于对象成员变量的访问,编译器会在编译过程中将其转化为对相对地址的访问,比如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;
}
真是有趣啊!