C ++基类指针调用子虚函数,为什么基类指针可以看到子类成员

时间:2018-05-26 06:25:15

标签: c++ pointers polymorphism virtual-functions vtable

我想我可能会让自己感到困惑。我知道C ++中具有虚函数的类有一个vtable(每个类类型一个vtable),因此Base类的vtable将有一个元素&Base::print(),而Child类的vtable将有一个元素&Child::print()

当我声明我的两个类对象basechild时,base的vtable_ptr将指向Base类的vtable,而child为vtable_ptr将指向Child类的vtable。在我将base和child的地址分配给Base类型指针的数组之后。我致电base_array[0]->print()base_array[1]->print()。我的问题是,base_array[0]base_array[1]都是Base*类型,在运行时,虽然v表查找将提供正确的函数指针,但{{1}怎么可能} type看到Base*类中的元素? (基本上值2?)。当我致电Child时,base_array[1]->print()的类型为base_array[1],但在运行时,它会发现它会使用Base*Child。但是,我很困惑为什么在这段时间内可以访问print(),因为我正在使用类型value2 .....我想我必须错过某个地方。

Base*

3 个答案:

答案 0 :(得分:2)

通过指针调用print执行vtable查找以确定要调用的实际函数。

该函数知道'this'参数的实际类型。

编译器还会插入代码来调整参数的实际类型(比如你有class child:

public base1, public base2 { void print(); };

其中print是从base2继承的虚拟成员。在这种情况下,相关的vtable将不会在子级中偏移0,因此需要进行调整以将存储的指针值转换为正确的对象位置)。

该修复所需的数据通常存储为隐藏的运行时类型信息(RTTI)块的一部分。

答案 1 :(得分:0)

  

我想我必须错过某个地方

是的,你把大部分东西都拿到了最后。

这里提醒一下C / C ++中真正基本的东西(C和C ++:相同的概念遗产,所以很多基本概念都是共享的,即使精细细节在某些方面有很大差异)。 (这可能是非常明显和简单的,但是值得大声说出它。)

表达式是编译程序的一部分,它们存在于编译时;对象存在于运行时。对象(the thing)由表达式(单词)指定;它们在概念上是不同的。

在传统的C / C ++中,左值(左值的缩写)是一个表达式,其运行时评估指定一个对象;取消引用指针给出左值(f.ex。*this)。它被称为"左值"因为左侧的赋值运算符需要分配对象。 (但并非所有左值都可以在赋值运算符的左边:指定const对象的表达式是左值,通常不能赋值。)Lvalues总是有一个定义良好的标识,并且大多数都有一个地址(只有struct的成员声明为位字段不能获取其地址,但底层存储对象仍然具有地址)。

(在现代C ++中,左值概念被重命名为glvalue,并且发明了一个新的左值概念(而不是为新概念创建一个新术语,并保持对象概念的旧术语具有可能或可能的身份不可修改。这在我不那么谦虚的意见中是一个严重的错误。)

多态对象(具有至少一个虚函数的类类型的对象)的行为取决于其动态类型,即其开始构造的类型(构造函数的名称)开始构造数据成员或进入构造函数体的对象)。在执行Child构造函数的主体期间,由*this设计的对象的动态类型是Child(在执行基类构造函数的主体期间,动态类型是运行的基类构造函数。

动态多态意味着你可以使用带有左值的多态对象,其声明的类型(在编译时从语言规则推导出的类型)不是完全相同的类型,而是相关的类型(通过继承相关)。这就是C ++中虚拟关键字的重点,没有它就完全没用了!

如果base_array[i]包含对象的地址(因此其值已明确定义,而不是null),则可以取消引用它。根据定义,这会给你一个左值,其声明的类型总是Base *:那是声明的类型,base_array的声明是:

Base (*(base_array[2])); // extra, redundant parentheses 

当然可以写成

Base* base_array[2];

如果你想用那种方式写,但是解析树,编译器分解声明的方式不是

{ Base* } { base_array[2] }

(使用粗体花括号来象征性地表示解析)

但是

基础 {* {{ base_array } [2] }}

我希望你明白这里的花括号是我选择的元语言而不是语言语法中用来定义类和函数的花括号(我不知道如何在这里画文本框)。< / p>

作为一名初学者,您必须&#34;编程&#34;你的直觉正确,总是像编译器那样读取声明;如果您在同一声明中声明了两个标识符,则差异很重要int * a, b;表示int (*a), b;而不是int (*a), (*b);

(注意:即使你可能对OP很清楚,因为这显然是C ++初学者感兴趣的问题,提醒C / C ++声明语法可能会被其他人使用。)

因此,回到多态的问题:派生类型的对象(最近输入的构造函数的名称)可以由声明类型的基类的左值指定。与非虚函数调用的行为不同,虚函数调用的行为由表达式指定的对象的动态类型(也称为实类型)确定;这是C ++标准定义的语义。

编译器获取语言标准定义的语义的方式是它自己的问题,并没有在语言标准中描述,但是当只有一个有效的简单方法时,所有编译器都以相同的方式完成它(很好)详细信息是特定于编译器的

  • 每个多态类的一个虚函数表(&#34; vtable &#34;)
  • 指向每个多态对象的vtable(&#34; vptr &#34;)的一个指针

(vtable和vptr显然都是实现概念而不是语言概念,但它们非常常见,以至于每个C ++程序员都知道它们。)

vtable是对类的多态方面的描述:对给定声明类型的表达式的运行时操作,其行为取决于动态类型。每个运行时操作都有一个条目。 vtable就像一个struct(记录),每个操作有一个成员(条目)(所有条目通常都是相同大小的指针,因此很多人将vtable描述为一个指针数组,但我不知道,我描述了它作为结构)。

vptr是一个隐藏的数据成员(没有名称的数据成员,C ++代码无法访问),它在一个对象中的位置与任何其他数据成员一样是固定的,当一个左值时,运算符代码可以读取它多态类类型(称为 D 用于&#34;声明类型&#34;)被评估。在 D 中取消引用vptr会为您提供描述 D值'的vtable,其中包含 D 左值的每个运行时方面的条目。根据定义,vptr的位置和vtable的解释(其条目的布局和使用)完全由声明的类型 D 确定。 (显然,使用和解释vptr所需的信息不是对象的运行时类型的函数:当该类型未知时使用vptr。)

vptr的语义是vptr上有保证的有效运行时操作的集合:如何取消引用vptr(现有对象的vptr始终指向有效的vtable)。它是表单属性的集合:通过将偏移量关闭添加到vptr值,您可以得到一个可以用于&#34;这样的方式&#34;。 这些保证形成了运行时合同。

多态对象最明显的运行时方面是调用虚函数,因此每个虚函数的 D 左值的vtable中都有一个条目,可以在类型为 D ,这是在该类或基类中声明的每个虚函数的条目(不计算重写,因为它们是相同的)。所有非静态成员函数都有一个&#34; hidden&#34;或者&#34;隐含&#34;参数,this参数;编译时会变成普通指针。

D 派生的任何 X 类将具有 D 左值的vtable。为了普通(非虚拟)单继承的简单情况下的效率,基类的vptr(我们称之为主基类)的语义将使用新属性进行扩充,因此的vtable X 将被增强: D 的vtable的布局和语义将被增强: D 的vtable的任何属性也是 X 的vtable的属性,语义将继承&#34;:&#34;继承&#34; vtable与类中的继承并行。

从逻辑上讲,增加了保证:派生类对象的vptr的保证强于基类对象的vptr的保证。因为它是一个更强的契约,所以为基础左值生成的所有代码仍然有效。

[在更复杂的继承中,可以是虚拟继承,也可以是非虚拟继承 辅助继承(在多重继承中,从辅助基础继承,即任何未定义为&#34的基础;主要基础&#34;),基类的vtable语义的增强并不那么简单。 ]

[解释C ++类实现的一种方法是作为C的翻译(实际上第一个C ++编译器正在编译为C,而不是编译)。 C ++成员函数的转换只是一个C函数,其中隐式this参数是显式的,一个普通的指针参数。]

D 左值的虚函数的vtable条目只是一个指向函数的指针,该函数带有as参数的现在显式this参数:该参数是指向 D的指针,它实际指向源自 D 的类对象的 D 基础子对象,或实际动态类型对象 D

如果 D X 的主要基础,那就是从与派生类相同的地址开始,并且vtable从同一地址开始的地方,所以vptr值是相同的,vptr在主基和派生类之间共享。这意味着虚拟调用(通过vtable调用左值)到 X 中虚拟函数的相同替换(使用相同的返回类型覆盖)只遵循相同的协议。

(虚拟覆盖器可以有一个不同的协变返回类型,在这种情况下可能会使用不同的调用约定。)

还有其他特殊的vtable条目:

  • 如果覆盖者具有需要和调整的协变返回类型(不是主要基础),则给定虚拟函数签名的多个虚拟调用条目。
  • 对于特殊的虚函数:当在具有虚拟析构函数的多态基础上使用的delete operator时,它通过删除虚拟析构函数来完成,以调用正确的operator delete(如果有的话,则替换为删除)。
  • 还有一个非删除虚拟析构函数,用于显式析构函数调用:l.~D();
  • vtable存储每个虚拟基础子对象的偏移量,用于隐式转换为虚拟基础指针,或用于访问其数据成员。
  • dynamic_cast<void*>的派生对象最多。
  • typeid运算符的条目应用于多态对象(特别是类的name())。
  • dynamic_cast<X*>运算符的足够信息应用于指向多态对象的指针以在运行时导航类层次结构,以定位给定的基类或派生子对象(除非X不是简单的类型转换的基类,因为它没有动态导航层次结构。)

这只是对vtable中存在的信息和vtable的种类的概述,还有其他细微之处。 (虚拟基础明显比实现级别的非虚拟基础更复杂。)

答案 2 :(得分:0)

我认为你可能会把指针声明的方式与它碰巧指向的对象的类型混淆。

暂时忘掉vtable。他们是一个实施细节。它们只是达到目的的手段。让我们来看看您的代码实际上在做什么。

所以,参考你发布的代码,这一行:

Select * 
From aTable
Where Date < someDateInThePast
order by Date desc

调用base_array[0]->print(); 的{​​{1}}实施,因为指向的对象属于Base类型。

这一行:

print()

调用Base base_array[1]->print(); 的实现,因为(是的,你猜对了)指向的对象属于Child类型。你不需要任何花哨的类型演员来实现这一点。无论如何,只要方法被声明为print(),它就会发生。

现在,在Child的主体内,编译器不知道(或关心)virtual是否指向类型为Base::print()的对象或类型为{的对象{1}}(或在一般情况下从this派生的任何其他类)。因此,它只能访问Base(或Child的任何父类,如果有的话)声明的数据成员。一旦你理解了这一点,它就足够简单了。

但是在Base的主体内部,编译器 更多地了解Base指向的内容 - 它必须是类{{1}的实例(或从Base派生的其他一些类)。现在,编译器可以安全地访问Child::print() 正文中的this - - 因此您的示例正确编译。

我认为这真的是关于它的。当您通过在编译时未知类型的指针调用该方法时,vtable仅用于调度到正确的虚方法,因为您的示例代码确实正在执行。 (*)

(*)好吧,差不多。如今优化编译器变得非常时髦,实际上有足够的信息可以直接调用相关方法,但请不要以任何方式混淆问题。