Function语意学

2024-01-27 01:48
文章标签 function 语意

本文主要是介绍Function语意学,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

  • 一、成员函数各种调用方式
    • 1.1、非静态成员函数
      • 1.1.1、转换函数
      • 1.1.2、转换调用
      • 1.1.3、名称的特殊处理
        • 1.1.3.1、数据成员
        • 1.1.3.2、成员函数
    • 1.2、虚成员函数
    • 1.3、静态成员函数
      • 1.3.1、特性
  • 二、虚成员函数
    • 2.1、单一继承下的虚函数
      • 2.1.1、必要的信息及其存放位置
      • 2.1.2、构建virtual table
      • 2.1.3、示例
    • 2.2、多重继承下的虚函数
      • 2.2.1、三个问题
        • 2.2.1.1、问题一
        • 2.2.1.2、问题二
        • 2.2.1.3、问题三
    • 2.3、虚继承下的虚函数
  • 三、指向成员函数的指针
    • 3.1、指向虚成员函数的指针
    • 3.2、多重继承下,指向成员函数的指针
  • 四、总结


本文主要介绍C++对象中成员函数有关内容,详细示例请参考C++对象成员函数存储示例。

一、成员函数各种调用方式

考虑如下所示的代码段:
在这里插入图片描述

C++支持三种类型的成员函数:static、nonstatic、virtual,每一种类型被调用的方式都不相同。

1.1、非静态成员函数

C++的设计准则之一就是:非静态成员函数至少必须和一般的非成员函数有相同的效率。例如,对于如下所示的两个函数:
在这里插入图片描述
选择成员函数不应该带来什么额外负担,这是因为编译器内部已将"成员函数实例"转换为对等的"非成员函数实例"

1.1.1、转换函数

转换步骤分为三步:

  • 1、改写函数的signature以安插一个额外的参数到成员函数中,用以提供一个存取管道,使类对象可以调用此函数。这个额外的参数被称为this指针
    在这里插入图片描述
    如果成员函数是const的,则变成:
    在这里插入图片描述
  • 2、将每一个"对非静态数据成员的存取操作"改为经由this指针来存取:
    在这里插入图片描述
  • 3、将成员函数重新写成一个外部函数。将函数名称经过"mangling"处理,使它在程序中成为独一无二的语汇:
    在这里插入图片描述

1.1.2、转换调用

现在这个函数已经被转换好了,而其每一个调用操作也都必须转换。如下所示:
在这里插入图片描述
对于一开始所示的代码段,normalize()函数将会被转换为下面的形式。其中假设已经声明有一个Point3d copy constructor,而named returned value(NRV)的优化也已实行:
在这里插入图片描述
一个比较有效率的做法是直接构建"normal"值,像这样:
在这里插入图片描述
在这里插入图片描述

1.1.3、名称的特殊处理

1.1.3.1、数据成员

一般而言,成员的名称前面会加上类名称,形成独一无二的命名。例如下面的声明:
在这里插入图片描述
其中的ival有可能变成这样:
在这里插入图片描述
为什么编译器要这么做?请考虑这样的派生操作:
在这里插入图片描述
有可能被转换为:
在这里插入图片描述
现在,无论要处理哪一个ival,通过"name mangling",都可以绝对清楚地指出来。

1.1.3.2、成员函数

由于成员函数可以被重载,所以需要更广泛的mangling操作,以提供绝对独一无二的名称。如果把:
在这里插入图片描述
转换为:
在这里插入图片描述
会导致两个被重载的函数实例拥有相同的名称。为了让它们独一无二,唯有再加上它们的参数列表。如果把参数类型也编码进去,就一定可以制造出独一无二的结果:
在这里插入图片描述

1.2、虚成员函数

1.2、虚成员函数

本节只介绍简单的虚成员函数情况,有关继承机制的内容请参看二、虚成员函数。

如果normalize()是一个虚成员函数,那么下面的调用:
在这里插入图片描述
将会被内部转换为:
在这里插入图片描述
其中:

  • vptr表示由编译器产生的指针,指向virtual table。它被安插在每一个声明有(或继承自)一个或多个虚函数的类对象中。事实上其名称也会被"mangled",因为在一个复杂的类派生体系中,可能存在多个vptr。
  • 1是virtual table slot的索引值,关联到normalize()函数
  • 第二个ptr表示this指针

类似的道理,如果magnitude()也是一个虚函数,它在normalize()之中的调用操作将被转换为:
在这里插入图片描述
此时,由于Point3d::magnitude()是在Point3d::normalize()中被调用的,而后者已经由虚拟机制而决议妥当,所以显式地调用"Point3d实例"会比较有效率,并因此压制由于虚拟机制而产生的不必要的重复调用操作:
在这里插入图片描述
如果magnitude()声明为inline函数,会更有效率。使用类范围操作符(::)显式调用一个虚函数,其决议方式会和非静态成员函数一样:
在这里插入图片描述

1.3、静态成员函数

如果Point3d::normalize()是一个静态成员函数,以下两个调用操作:
在这里插入图片描述
将会被转换为一般的非成员函数调用,像这样:
在这里插入图片描述

1.3.1、特性

静态成员函数的主要特性就是它没有this指针。以下的次要特性统统根源于其主要特性:

  • 它不能够直接存取其类中的非静态成员
  • 它不能够被声明为const、volatile或virtual
  • 它不需要经由类对象即可被调用——虽然大部分时候它是这样被调用的!

二、虚成员函数

二、虚成员函数

虚函数的一般实现模型为:每一个类有一个virtual table,内含该类中有作用的虚函数的地址,每个对象有一个vptr,指向virtual table,如1.2、虚成员函数节所示,本节主要介绍有关继承的各种情况。

2.1、单一继承下的虚函数

2.1.1、必要的信息及其存放位置

为了支持虚函数机制,必须首先能够对于多态对象有某种形式的"执行期类型判断法(runtime type resolution)"。也就是说对于调用操作:ptr->z(),将需要ptr在执行期的某些相关信息,如此一来才能够找到并调用z()的适当实例。

或许最直接了当但是成本最高的解决办法就是把必要的信息加在ptr身上。在这样的策略之下,一个指针(或一个引用)持有两项信息:

  • 1、它所参考到的对象的地址(也就是目前它所持有的东西)
  • 2、对象类型的某种编码,或是某个结构(内含某些信息,用以正确决议出z()函数实例)的地址

这个方法带来两个问题:第一,它明显增加了空间负担,即使程序并不使用多态;第二,它打断了与C程序间的链接兼容性。

如果这份额外信息不能够和指针放在一起,下一个可以考虑的地方就是把它放在对象本身。接下来考虑两个问题:1、什么时候需要这些信息呢?2、哪些对象真正需要这些信息呢?

  • 对于第一个问题,很明显是在必须支持某种形式的"执行期多态(runtime polymorphism)"的时候。

  • 对于第二个问题,由于没有导入像是polymorphic之类的新关键词,因此识别一个类是否支持多态,唯一适当的方法就是看看它是否有任何虚函数。只要类拥有一个虚函数,它就需要这份额外的执行期信息。

2.1.2、构建virtual table

下一个明显的问题是,我们需要存储什么样的额外信息?也就是说,对于调用操作:ptr->z() ,其中z()是一个虚函数,那么需要什么样的信息才能让我们在执行期调用正确的z()实例?我们需要知道:

  • ptr所指对象的真实类型,这可使我们选择正确的z()实例
  • z() 实例的位置,以便我们能够调用它

在实现上,首先我们可以在每一个多态的类对象身上增加两个成员:

  • 1、一个字符串或数字,表示类的类型
  • 2、一个指针,指向某表格,表格中持有程序的虚函数的执行期地址

表格中的虚函数地址如何被构建起来?在C++中,虚函数(可经由其对象被调用)可以在编译时期获知。此外,这一组地址是固定不变的,执行期不可能新增或替换之。由于程序执行时,表格的大小和内容都不会改变,所以其构建和存取皆可以由编译器完全掌控,不需要执行期的任何介入。

然而,执行期备妥那些函数地址,只解决了问题的一半。另一半是找到那些地址。两个步骤可以完成这项任务:

  • 1、为了找到表格,每一个类对象被安插了一个由编译器内部产生的指针,指向该表格
  • 2、为了找到函数地址,每一个虚函数被指派一个表格索引值

这些工作都由编译器完成。执行期要做的,只是在特定的virtual table slot中激活虚函数。

一个类只会有一个virtual table。每一个virtual table内含其对应类对象中所有虚函数实例的地址。这些虚函数包括:

  • 这一类所定义的函数实例。它会覆盖一个可能存在的基类虚函数实例
  • 继承自基类的函数实例。这是在派生类决定不改写虚函数时才会出现的情况
  • 一个pure_virtual_called()函数实例。它既可以扮演纯虚函数的空间保卫者角色,也可以当做是执行期异常处理函数(有时候会用到)

每一个虚函数都被指派一个固定的索引值,这个索引在整个继承体系中保持与特定的虚函数的关系

2.1.3、示例

对于下面的Point类:
在这里插入图片描述
virtual destructor被指派slot1,而mult()被指派slot2,。此例并没有mult()的函数定义,所以pure_virtual_called()的函数地址会被放在slot2中。如果该函数意外地被调用,通常的操作是结束掉这个程序。y()被指派slot3,而z()被指派slot4。x()的slot是多少?答案是没有,因为x()并非虚函数。Point的内存布局和其virtual table如下所示:
在这里插入图片描述

当一个类派生自Point时,会发生什么?例如class Point2d:
在这里插入图片描述
一共有三种可能性:

  • 1、它可以继承基类所声明的虚函数的函数实例。正确地说是,该函数实例的地址会被拷贝到派生类的virtual table的相对应slot中
  • 2、它可以使用自己的函数实例。这表示它自己的函数实例地址必须放在对象的slot中
  • 3、它可以加入一个新的虚函数。这时候virtual table的尺寸会增大一个slot,而新的函数实例地址会被放进该slot中

Point2d的virtual table在slot1中指出destructor,而在slot2中指出mult()(取代纯虚函数)。它自己的y()函数实例地址放在slot3中,继承自Point的z()函数实例地址则放在slot4中。Point2d的内存布局和其virtual table如下所示:
在这里插入图片描述

类似的情况,Point3d派生自Point2d,如下:
在这里插入图片描述
其virtual table中的slot1放置Point3d的destructor,slot2放置mult()函数地址,slot3放置继承自Point2d的y()函数地址,slot4放置自己的z()函数地址。Point3d的内存布局和其virtual table如下所示:
在这里插入图片描述
对于ptr->z() ,我们如何有足够的知识在编译时期设定虚函数的调用呢?

  • 一般而言,在每次调用z()时,我们并不知道prt所指对象的真正类型。然而,经由ptr可以存取到该对象的virtual table
  • 虽然我们不知道哪一个z()函数实例会被调用,但我们知道每一个z()函数地址都被放在slot4中

这些信息使得编译器可以将调用ptr->z() 转换为:
在这里插入图片描述
在这一转换中,vptr表示编译器所安插的指针,指向virtual table;4表示z()被指派的slot编号。唯一一个在执行期才能知道的东西是:slot4所指的到底是哪一个z()函数实例。

在一个单一继承体系中,虚函数机制的行为十分良好,不但有效率而且很容易塑造出模型来。

2.2、多重继承下的虚函数

在多重继承中支持虚函数,其复杂度围绕在第二个及后继的基类身上,以及"必须在执行期调整this指针"这一点。以下面的类体系为例:
在这里插入图片描述

2.2.1、三个问题

派生类支持虚函数的困难度,统统落在Base2 subobject身上。有三个问题需要解决,以此例而言分别是(1)virtual destructor,(2)被继承下来的Base2::mumble(),(3)一组clone()函数实例。

2.2.1.1、问题一

首先,我们把一个从heap中配置而得的派生类对象的地址,指定给一个Base2指针:
在这里插入图片描述
新的派生类对象的地址必须调整以指向其Base2 subobject。编译时期会产生以下的代码:
在这里插入图片描述
如果没有这样的调整,指针的任何"非多态运用"(像下面那样)都将失败:
在这里插入图片描述
当要删除pbase2所指的对象时:
在这里插入图片描述
指针必须被再一次调整,以求再一次指向派生类对象的起始处。然而上述的offset加法不能够在编译时期直接设定,因为pbase2所指的真正对象只有在执行期才能确定。

一般规则是,经由指向"第二或后继基类"的指针(或引用)来调用派生类的虚函数。其所连带的必要的"this指针调整"操作,必须在执行期完成。也就是说,offset的大小,以及把offset加到this指针上的那一小段程序代码,必须由编译器在某个地方插入。问题是,在哪个地方?

Bjarne原先实施于cfront编译器中的方法是将virtual table加大,使它容纳此处所需的this指针,调整相关事物。每一个virtual table slot,不再是一个指针,而是一个集合体,内含可能的offset以及地址。于是虚函数的调用操作由:
在这里插入图片描述
改变为:
在这里插入图片描述
这个做法的缺点是,它相当与连坐处罚了所有的虚函数调用操作,不管它们是否需要offset的调整。

比较有效率的解决方法是利用所谓的thunk所谓thunk是一小段assemble代码,用来(1)以适当的offset值调整this指针,(2)跳到虚函数去。例如,经由一个Base2指针调用派生类析构函数,其相关的thunk可能看起来是这个样子:
在这里插入图片描述
Bjarne并不是不知道thunk技术,问题是thunk只有以assembly代码完成才有效率可言。由于cfront使用C作为其程序代码产生语言,所以无法提供一个有效率的thunk编译器。

thunk技术允许virtual table slot继续内含一个简单的指针,因此多重继承不需要任何空间上的额外负担。slots中的地址可以直接指向虚函数,也可以指向一个相关的thunk(如果需要调整this指针的话)。

调整this指针的第二个额外负担就是,由于两种不同的可能:(1)经由派生类(或第一个基类)调用,(2)经由第二个(或其后继)基类调用,同一函数在virtual table中可能需要多笔对应的slots。例如:
在这里插入图片描述
虽然两个delete操作导致相同的派生类destructor,但它们需要两个不同的virtual table slots:

  • 1、pbase1不需要调整this指针(因为Base1是最左端的基类,所以它已经指向派生类对象的起始处)。其virtual table slot需放置真正的destructor地址
  • 2、pbase2需要调整this指针。其virtual table slot需要相关的thunk地址。

在多重继承之下,一个派生类内含n-1个额外的virtual table,n表示其上一层基类的个数(因此,单一继承将不会有额外的virtual table)。对于本例的派生类而言,会有两个virtual table被编译器产生出来:

  • 1、一个主要实例,与Base1(最左端基类)共享
  • 2、一个次要实例,与Base2(第二个基类)有关

针对每一个virtual table,派生类对象中有对应的vptr。如下所示:
在这里插入图片描述
有三种情况,第二或后继的基类会影响对虚函数的支持。第一种情况是,通过一个指向第二个基类的指针,调用派生类虚函数。即上述讨论的virtual destructor情况,例如:
在这里插入图片描述
从上图可以看到这个调用操作的重点:ptr指向派生类对象中的Base2 subobject;为了能够正确执行,ptr必须调整指向派生类对象的起始处。

2.2.1.2、问题二

第二种情况是第一种情况的变体,通过一个指向派生类的指针,调用第二个基类中一个继承而来的虚函数。在此情况下,派生类指针必须调整,以指向第二个base subobject。例如:
在这里插入图片描述

2.2.1.3、问题三

第三种情况发生于一个语言扩充性质之下:允许一个虚函数的返回值类型有所变化,可能是基类类型,也可能是派生类类型。以Derived::clone()函数为例。clone函数的派生类版本传回一个派生类指针,默默地改写了它的两个基类函数实例。当我们通过指向第二个基类的指针来调用clone()时,this指针的offset问题于是诞生了:
在这里插入图片描述
当执行pb1->clone()时,pb1会被调整指向派生类对象的起始地址,于是clone()的派生类版本会被调用;它会传回一个指针,指向一个新的派生类对象;该对象的地址在被指定给pb2之前,必须先经过调整,以指向Base2 subobject。

2.3、虚继承下的虚函数

考虑下面的虚基类派生体系,从Point2d派生出Point3d:
在这里插入图片描述
虽然Point3d有唯一一个(同时也是最左边的)基类,也就是Point2d,但Point3d和Point2d的起始部分并不像非虚拟的单一继承情况那样。这种情况如下所示:
在这里插入图片描述
当一个虚基类从另一个虚基类派生而来,而且两者都支持虚函数和非静态数据成员时,编译器对于虚基类的支持简直就像进了迷宫一样。因此作者建议我们:不要在一个虚基类中声明非静态数据成员。

三、指向成员函数的指针

取一个非静态成员函数的地址,如果该函数是nonvirtual,得到的结果是它在内存中真正的地址。然而这个地址是不完全的。它需要被绑定于某个类对象的地址上,才能够通过它调用该函数。所有的非静态成员函数都需要对象的地址(以参数this指出)。

使用一个成员函数指针,如果并不用于虚函数、多重继承、虚基类等情况的话,并不会比使用一个非成员函数指针的成本更高。

3.1、指向虚成员函数的指针

考虑下面的代码:
在这里插入图片描述
pmf是一个指向成员函数的指针,被初始化为Point:: z()(一个虚函数)的地址。prt则被指定一个Point3d对象。如果我们直接经由ptr调用z():
在这里插入图片描述
被调用的是Point3d:: z()。但是如果我们从pmf间接调用z()呢?
在这里插入图片描述
虚拟机制仍然能够在使用指向成员函数的指针情况下运行。问题是如何实现的呢?

对一个非静态成员函数取其地址,将获得该函数在内存中的地址。然而面对一个虚函数,其地址在编译时期是未知的,所能知道的仅是虚函数在其相关的virtual table中的索引值。也就是说,对一个虚成员函数取其地址,所能获得的只是一个索引值。

以下面的Point声明为例:
在这里插入图片描述
然后取destructor的地址:
在这里插入图片描述
得到的结果是1。取x()或y()的地址:
在这里插入图片描述
得到的则是函数在内存中的地址,因为它们不是虚函数。取z()的地址:
在这里插入图片描述
得到的结果是2。通过pmf来调用z(),会被内部转换为一个编译时期的式子,一般形式如下:
在这里插入图片描述
对一个指向成员函数的指针评估求值(evaluated),会因为该值有两种意义而复杂化:既可以指向虚函数,也可以指向非虚函数;其调用操作也将有别于常规调用操作。pmf的内部定义是:
在这里插入图片描述
必须允许此函数能够寻址出非虚函数x()和虚函数z()两个成员函数,而那两个函数有着相同的原型:
在这里插入图片描述
只不过其中一个代表内存地址,另一个代表virtual table中的索引值。因此,编译器必须定义pmf,使它能够(1)持有两种数值,(2)更重要的是其数值可以被区别代表内存地址还是virtual table中的索引值。

在cfront2.0非正式版中,这两个值被内含在一个普通的指针内。cfront使用了以下技巧:
在这里插入图片描述
这种实现技巧必须假设继承体系中最多只有128个虚函数。这并不是我们所希望的,但却证明是可行的。然而,多重继承的引入,导致需要更一般化的实现模式,并趁机去除对虚函数的个数限制。

3.2、多重继承下,指向成员函数的指针

为了让指向成员函数的指针能够支持多重继承和虚继承,Bjarne设计了下面一个结构体:
在这里插入图片描述
index和faddr分别(不同时)持有virtual table索引和非虚成员函数地址(为了方便,当index不指向virtual table时,会被设为-1)。在此模型之下,像这样的调用操作:
在这里插入图片描述
会变成:
在这里插入图片描述

四、总结

在这里插入图片描述

这篇关于Function语意学的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/648703

相关文章

Java function函数式接口的使用方法与实例

《Javafunction函数式接口的使用方法与实例》:本文主要介绍Javafunction函数式接口的使用方法与实例,函数式接口如一支未完成的诗篇,用Lambda表达式作韵脚,将代码的机械美感... 目录引言-当代码遇见诗性一、函数式接口的生物学解构1.1 函数式接口的基因密码1.2 六大核心接口的形态学

C++11的函数包装器std::function使用示例

《C++11的函数包装器std::function使用示例》C++11引入的std::function是最常用的函数包装器,它可以存储任何可调用对象并提供统一的调用接口,以下是关于函数包装器的详细讲解... 目录一、std::function 的基本用法1. 基本语法二、如何使用 std::function

AutoGen Function Call 函数调用解析(一)

目录 一、AutoGen Function Call 1.1 register_for_llm 注册调用 1.2 register_for_execution 注册执行 1.3 三种注册方法 1.3.1 函数定义和注册分开 1.3.2 定义函数时注册 1.3.3  register_function 函数注册 二、实例 本文主要对 AutoGen Function Call

(function() {})();只执行一次

测试例子: var xx = (function() {     (function() { alert(9) })(); alert(10)     return "yyyy";  })(); 调用: alert(xx); 在调用的时候,你会发现只弹出"yyyy"信息,并不见弹出"10"的信息!这也就是说,这个匿名函数只在立即调用的时候执行一次,这时它已经赋予了给xx变量,也就是只是

js私有作用域(function(){})(); 模仿块级作用域

摘自:http://outofmemory.cn/wr/?u=http%3A%2F%2Fwww.phpvar.com%2Farchives%2F3033.html js没有块级作用域,简单的例子: for(var i=0;i<10;i++){alert(i);}alert(i); for循环后的i,在其它语言像c、java中,会在for结束后被销毁,但js在后续的操作中仍然能访

rtklib.h : RTKLIB constants, types and function prototypes 解释

在 RTKLIB 中,rtklib.h 是一个头文件,包含了与 RTKLIB 相关的常量、类型和函数原型。以下是该头文件的一些常见内容和翻译说明: 1. 常量 (Constants) rtklib.h 中定义的常量通常包括: 系统常量: 例如,GPS、GLONASS、GALILEO 等系统的常量定义。 时间常量: 如一年、一天的秒数等。 精度常量: 如距离、速度的精度标准。 2. 类型

【AI大模型应用开发】2.1 Function Calling连接外部世界 - 入门与实战(1)

Function Calling是大模型连接外部世界的通道,目前出现的插件(Plugins )、OpenAI的Actions、各个大模型平台中出现的tools工具集,其实都是Function Calling的范畴。时下大火的OpenAI的GPTs,原理就是使用了Function Calling,例如联网检索、code interpreter。 本文带大家了解下Function calling,看

Vite + Vue3 +Vant4出现Toast is not a function

今天写前端的时候出现了这个问题搞了我一会 搜集原因: 1:是vant版本的问题,Toast()的方法是vant3版本的写法,而我用的是vant4,vant4中的写法改成了showToast()方法,改正过来 import {showToast} from "vant";  发现还是报错,说是找不到对应的样式文件 2:Vant 从 4.0 版本开始不再支持 babel-plugin-i

Ollama Qwen2 支持 Function Calling

默认 Ollama 中的 Qwen2 模型不支持 Function Calling,使用默认 Qwen2,Ollama 会报错。本文将根据官方模板对 ChatTemplate 进行改进,使得Qwen2 支持 Tools,支持函数调用。 Ollama 会检查对话模板中是否存在 Tools,如果不存在就会报错,下面的代码是 Ollama 解析模板的代码。 Ollama 3.1 是支持 Tools

android kotlin复习 Anonymous function 匿名函数

1、还是先上个图,新建kt: 2、代码: package com.jstonesoft.myapplication.testfun main(){val count = "helloworld".count()println(count);println("------------------------")var count2 = "helloworld".count(){it ==