先来看一道经典的面试题:

以下程序输出结果是什么()
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++里,我们知道派生类初始化时,其对象首先会“安插”一个基类对象(安插:首先调用基类构造函数,完成基类对象初始化);然后接着完成本类新增成员变量的初始化,于是一个派生类对象长这样:

图1 一个派生类对象占的内存里有些什么

可以在这里看到,并没有“基类的成员函数”啊?那为什么还说派生类还继承了基类的成员函数,这“继承”怎么个继承法?

因此,对「普通成员函数」(非 virtual、非 static)来说,「继承」并不是在派生类里再写一份代码,而是让编译器在派生类的作用域里自动拥有了「对基类那些函数的调用权」

具体理解:

图2:具体理解结合代码

上文提到:派生类继承了基类的成员函数,实际上是获得了其对基类那些函数的调用权。所以此处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*:

图3 this已经是基类指针

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

图4 调用到了b重写的虚函数func()

那答案就是:“B->0”了?我们暂先按下不表。先来总结“继承和多态下,调用成员函数分析步骤

***继承和多态下,调用成员函数分析步骤***

1)如果第一句是Derive对象(指针)调用成员函数(忽略成员函数具体逻辑):

  • 如果成员函数在本类中并无定义,则顺着继承数往上去到基类作用域寻找——找到后进入下一步(此时完成this指针的向上转型)。
  • 如果成员函数在本类有直接定义,找到后进入下一步。

2)如果是基类指针或引用调用成员函数(忽略成员函数具体逻辑)

  • 成员函数是虚函数,则回到调用函数的指针真正指向的对象的虚表里取虚函数地址,找到真正执行的函数。找到了进入下一步。
  • 如果不是虚函数,那么就直接去基类作用域寻找函数定义——此时就不能往下(往派生类作用域寻找)

小总结:看是否满足多态,不满足就正常逻辑分析。


虚函数的重写:实际上是重写函数体(函数行为),而缺省参数不会被重写。

所以可以理解为本类的虚函数的定义为:基类的声明 + 重写的函数体

所以此处的val还是为1 ,最终答案为B. B->1.

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐