c++继承体系中的成员函数调用问题探究

c++中带有继承关系的类在进行成员函数调用时,需要考虑很多问题,是否是虚函数?动态静态类型是否一致?调用形式?本文主要就是对这些问题进行一一剖析。

总结:在给一个基类的指针赋值一个子类的地址时,调用函数时会出现以下几种情况:

  1. 调用一个虚函数,调用的是子类对象的虚函数;
  2. 调用一个函数(不是虚函数,但是子类基类中都有定义),调用的是基类对象的函数版本;

动态类型与静态类型

当我们在继承体系中进行成员函数调用时,一个变量(或表达式)的静态类型与其动态类型与其息息相关。

  • 静态类型:一个变量(或表达式)的静态类型在编译时总是已知的,它是变量声明时的类型(或表达式生成的类型);
  • 动态类型:是一个变量或表达式表示的内存中的对象的类型,往往直到运行时才可知。

通常如果一个表达式(变量名也是表达式)不是引用也不是指针,则它的动态类型与静态类型永远一致基类的指针或引用的静态类型可能与其动态类型不一致。我们都知道一个派生类对象的存储中基类部分与派生类部分是分开存储的。并且在我们使用一个派生类对象对一个基类对象进行赋值时,调用的是等式左边的基类对象的拷贝赋值运算符,所以参数传入时将一个基类引用绑定到右边的派生类对象上。在拷贝赋值操作中其实是将右边派生类对象的基类部分拷贝给左边的基类对象。本质也就是将派生类部分“切掉”再赋值给基类对象的,也就是说得到的基类对象就是一个实打实的基类对象。除了数据成员值相同,与原本的派生类对象不再有任何关系

1
2
3
4
5
class base{};
class derived: public base{};
derived d1;
base b1 = d1; //静态类型与动态类型都是base
base *b2 = &d1; //静态类型base,动态类型derived

对象、指针和引用的静态类型决定了我们能够使用哪些成员。就算是虚函数,也是因为这个虚函数本身在静态类型的基类中也存在。

继承体系中的虚函数与非虚函数

我们都知道面向对象的编程中,每个类定义了自己的作用域。当存在继承关系时,派生类的作用域嵌套在其基类的作用域之内。如果一个名字在派生类的作用域中无法正确解释,编译器会继续在外层的基类作用域中寻找该名字的定义。因此,派生类往往可以像使用自己的成员一样使用基类的成员。

和其他作用域一样,派生类也能重定义在其直接或间接基类中的名字,此时定义在内层作用域的名字将隐藏定义在外层作用域的名字。如果派生类中定义了与基类中一样名字的函数,则其会直接隐藏基类中的函数,无论它们参数形式是否一致(因为是隐藏不会重载)。

可以发现对于一个不是虚函数的普通函数,调用时对其的查找是从当前静态类型作用域开始逐步向上查找的(向基类)。如果想要使用一个被隐藏的基类成员,可以使用作用域运算符::来指定使用。

作用域问题讲完,言归正传,在继承体系中调用普通成员函数与虚函数,区别如下。

普通成员函数(不是虚函数,但是子类基类都有定义)

在调用一个普通成员函数时,会执行调用对象的静态类型对应的版本。

虚函数

在调用一个虚函数时,会执行调用对象的动态类型对应的版本。这也叫做动态绑定

继承体系中函数调用的解析过程

此处来分析一下继承体系中函数调用的解析过程,如p->memobj.mem()的解析过程: 1. 首先确定p(或obj)的静态类型。因为我们调用的是一个成员,所以该类型必然是类类型

  1. 在p(或obj)的静态类型对应的类中查找mem(如果是虚函数,要么覆盖要么继承此处就可以找到)。如果找不到,则依次在直接基类中不断查找直到到达继承链的顶端。如果找遍了该类及其基类依然找不到,则编译器将报错

  2. 一旦找到了mem,就进行常规的类型检查以确认调用合法。

  3. 若调用合法,则编译器将根据是否是虚函数而产生不同的代码:

    • 如果mem是虚函数且我们是通过引用或指针进行的调用,则编译器产生的代码将在运行时确定运行指针(或引用)的动态类型的虚函数版本
    • 反之,如果mem不是虚函数或者我们是通过对象(非引用、指针)进行的调用,则编译器将产生一个常规函数调用

参考资料

  • [1] C++ Primer(第5版)

c++继承体系中的成员函数调用问题探究
http://line.com/2018/10/28/2018-10-28-cpp-inheritance-virtual-func/
作者
Line
发布于
2018年10月28日
许可协议