C++_继承与多态双双环绕,正确理解“派生类怎么‘继承’基类的成员函数”(1/2)
先来看一道经典的面试题:
以下程序输出结果是什么()
A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确
class A
{
public:virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }virtual void test() { func(); }
};
class B : public A
{
public:void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};
int main(int argc, char* argv[])
{B* p = new B;p->test();return 0;
}
你的心里是否有答案,以及每一步调用如何如何
铺垫知识
C++里,我们知道派生类初始化时,其对象首先会“安插”一个基类对象(安插:首先调用基类构造函数,完成基类对象初始化);然后接着完成本类新增成员变量的初始化,于是一个派生类对象长这样:

可以在这里看到,并没有“基类的成员函数”啊?那为什么还说派生类还继承了基类的成员函数,这“继承”怎么个继承法?
因此,对「普通成员函数」(非 virtual、非 static)来说,「继承」并不是在派生类里再写一份代码,而是让编译器在派生类的作用域里自动拥有了「对基类那些函数的调用权」。
具体理解:

上文提到:派生类继承了基类的成员函数,实际上是获得了其对基类那些函数的调用权。所以此处p虽然指向b,但是它依然可以去调用test() ——尽管B类没有test()定义。
接下来是关键,怎么找到 test()的?
b在B::里没找到test()定义,于是自然顺着继承树里查到A::了。进入A::test()后,this 的静态类型自然是A*,但这步转换是完全合法且安全的(向上转型—— B* —> A*)。
如果要理解:那就是进入了基类的地界,就自然而然向上转型。
// p->test()就等价于
A::test(p); // 把 p 转成 A* 作为 this 传入(此处把隐含的this指针挑明了,为了好理解)
所以:派生类对象虽然获得了基类成员函数的调用权,但这真正调用到基类的成员函数时,隐含的this*指针已经安全向上转型了
如今加入了“多态”这一语境:
多态构成条件
1. 基类指针或者引用调用2. 调用虚函数
再看
p->test();// p为B* ,并非基类指针调用;两个条件不足其一,即不构成多态
而当进入了A::test()时,p向上转型成了A*:

此时满足了基类指针调用(多态条件一),亦满足了调用了虚函数func()(多态条件二)—— 编译器此时不会像刚刚一样“ 直接生成一条普通call指令”;而是去根据p真正指向的对象b里,存着的虚表_vfptr中取func这个虚函数的地址,生成调用指令。

那答案就是:“B->0”了?我们暂先按下不表。先来总结“继承和多态下,调用成员函数分析步骤”
***继承和多态下,调用成员函数分析步骤***
1)如果第一句是Derive对象(指针)调用成员函数(忽略成员函数具体逻辑):
- 如果成员函数在本类中并无定义,则顺着继承数往上去到基类作用域寻找——找到后进入下一步(此时完成this指针的向上转型)。
- 如果成员函数在本类有直接定义,找到后进入下一步。
2)如果是基类指针或引用调用成员函数(忽略成员函数具体逻辑)
- 成员函数是虚函数,则回到调用函数的指针真正指向的对象的虚表里取虚函数地址,找到真正执行的函数。找到了进入下一步。
- 如果不是虚函数,那么就直接去基类作用域寻找函数定义——此时就不能往下(往派生类作用域寻找)
小总结:看是否满足多态,不满足就正常逻辑分析。
虚函数的重写:实际上是重写函数体(函数行为),而缺省参数不会被重写。
所以可以理解为本类的虚函数的定义为:基类的声明 + 重写的函数体
所以此处的val还是为1 ,最终答案为B. B->1.