为啥函数不是对象?
Posted
技术标签:
【中文标题】为啥函数不是对象?【英文标题】:Why is a function not an object?为什么函数不是对象? 【发布时间】:2017-10-13 18:07:08 【问题描述】:我阅读了标准 n4296(草案)§ 1.8 第 7 页:
对象是一个存储区域。 [注意:函数不是对象, 不管它是否以这样的方式占用存储空间 对象做。 ——尾注]
我在网上花了几天时间寻找排除这种情况的充分理由,但没有运气。也许是因为我不完全理解对象。所以:
-
为什么函数不是对象?有何不同?
这与函子(函数对象)有什么关系吗?
【问题讨论】:
语言定义者可以选择术语的含义。对于 C++,函数和对象的含义完全不同,因为设计者选择以这种方式定义它们。在其他语言中,情况可能并非如此。 C 和 C++ 被设计为能够在冯诺依曼架构(指令存储在 RAM 中)和哈佛架构(指令不存储在 RAM 中,嗯,有时它们只是哈佛架构)上运行机器有两个独立的内存访问,指令(函数)只能使用面向指令的指令(如调用、转到、分支等)进行访问,而不能使用面向数据的指令(如加载、存储等)进行访问。 【参考方案1】:很多不同之处在于指针和寻址。在 C++¹ 中,指向函数的指针和指向对象的指针是严格分开的。
C++ 要求您可以将指向任何对象类型的指针转换为指向void
的指针,然后将其转换回原始类型,结果将等于您开始使用的指针²。换句话说,不管他们到底是怎么做的,实现必须确保从指针到对象类型到指针到空的转换是无损的,所以不管原来是什么,它包含的任何信息都可以重新创建,这样您就可以通过从T*
转换为void *
并返回到T*
来恢复与开始时相同的指针。
不是,但对于指向函数的指针,如果你将指针指向函数,将其转换为void *
,然后将其转换回指向函数的指针,您可能会在此过程中丢失一些信息。您可能无法取回原始指针,并且取消引用您所取回的内容会给您带来未定义的行为(简而言之,不要那样做)。
无论如何,您可以不过,将指向一个函数的指针转换为指向不同类型函数的指针,然后将该结果转换回原始类型,这样就可以了保证结果和你开始的一样。
虽然它与手头的讨论并不特别相关,但还有一些其他差异可能值得注意。例如,您可以复制大多数对象,但不能复制任何函数。
就与函数对象的关系而言:嗯,除了一点之外真的没有太多的东西:函数对象支持看起来像函数调用的语法——但它仍然是一个对象,而不是函数。因此,指向函数对象的指针仍然是指向对象的指针。例如,如果您将 1 转换为 void *
,然后将其转换回原始类型,您仍然可以保证返回原始指针值(对于指向函数的指针,情况并非如此)。
至于为什么指向函数的指针(至少可能)不同于指向对象的指针:其中一部分归结为现有系统。例如,在 MS-DOS(以及其他)上,有四种完全独立的内存模型:小型、中型、紧凑型和大型。小型模型对函数或数据使用 16 位寻址。 Medium 使用 16 位地址存储数据,使用 20 位地址存储代码。 Compact 颠倒了这一点(代码为 16 位地址,数据为 20 位地址)。大型代码和数据都使用 20 位地址。因此,无论是紧凑模型还是中等模型,在指向代码的指针和指向函数的指针之间进行转换确实会并且确实会导致问题。
最近,相当多的 DSP 使用完全独立的内存总线来存储代码和数据,并且(与 MS-DOS 内存模型一样)它们通常具有不同的宽度,在两者之间进行转换可能而且确实会丢失信息。
-
这些特殊的规则是从 C 到 C++ 的,所以在 C 中也是如此,不管它有什么价值。
尽管这不是直接必需的,但按照事情的工作方式,从原始类型到指向
char
的指针的转换以及返回的转换几乎都是一样的,无论值多少钱。
【讨论】:
好答案。可能值得放弃哈佛架构。 @Kevin:我考虑过这样做,但认为它可能会做更多的混淆而不是阐明。问题是很多人用它来指代当前的 x86/x64 之类的东西,其中数据和代码具有单独的 缓存,但仍然存在于相同的实际内存中。在我所说的 DSP 上,它们有独立的内存总线与完全独立的内存阵列通信(并不是这意味着它不是哈佛架构,但它与使用该术语的人不同)。 然后有一天有人必须设计和实现dlsym()
,事情变得非常棘手。
Harvard architectures 的典型代码和数据的单独地址空间。
@JerryCoffin:这就是哈佛架构的定义。其他人如何使用“哈佛建筑”一词?【参考方案2】:
为什么函数不是对象?有什么不同?
为了理解这一点,让我们从所涉及的抽象方面从下到上移动。所以,您有自己的地址空间,您可以通过它定义内存的状态,我们必须记住,从根本上说,这一切都与您操作的状态有关。
好的,让我们在抽象方面提高一点。我还没有考虑编程语言强加的任何抽象(像对象、数组等),只是作为一个外行,我想记录一部分内存,让我们称之为Ab1
和另一个叫 Ab2
。
两者基本上都有一个状态,但我打算以不同方式操纵/利用状态。
不同...为什么以及如何?
为什么?
由于我的要求(例如,执行 2 个数字的加法并将结果存储回来)。我将使用Ab1
作为长期使用状态,Ab2
作为相对较短的使用状态。所以,我将为Ab1
(添加两个数字)创建一个状态,然后使用这个状态来填充Ab2
的一些状态(暂时复制它们 >) 并对Ab2
(添加它们) 执行进一步操作,并将生成的Ab2
的一部分保存到Ab1
(添加的结果)。发布 Ab2
变得无用,我们重置它的状态。
怎么做?
我将需要对这两个部分进行一些管理,以跟踪从Ab1
中选择哪些单词并复制到Ab2
等等。在这一点上,我意识到我可以让它执行一些简单的操作,但一些严重的事情需要一个布局规范来管理这个内存。
所以,我寻找这样的管理规范,结果发现存在多种这样的规范(有些具有内置的内存模型,有些提供了自己管理内存的灵活性)更好的设计。事实上,因为他们(甚至没有直接规定如何管理内存)已经成功地定义了这个长寿命存储的封装以及如何以及何时创建和销毁它的规则。
Ab2
也是如此,但他们呈现它的方式让我觉得这与Ab1
有很大不同。事实上,事实证明确实如此。他们使用堆栈对Ab2
进行状态操作,并从堆中为Ab1
保留内存。 Ab2
一段时间后死掉。(执行完成后)。
此外,您定义如何处理Ab2
的方式是通过另一个名为Ab2_Code
的存储部分完成的,Ab1
的规范同样涉及Ab1_Code
我想说,这太棒了!我得到了很多便利,可以解决很多问题。
现在,我仍然是从外行的角度来看,所以我对经历了这一切的思考过程并不感到惊讶,但如果你从上到下质疑事情,事情可能会变得有点难以透视。 (我怀疑你的情况就是这样)
顺便说一句,我忘了提到 Ab1
被正式称为 object 而 Ab2
是一个 函数堆栈 而 Ab1_Code
是 类定义 和Ab2_Code
是函数定义代码。
正是由于 PL 强加的这些差异,你会发现它们是如此不同。(你的问题)
注意:不要将我对Ab1
/Object
的表示作为规则或具体事物的长存储抽象 - 这是从外行的角度来看的。编程语言在管理对象的生命周期方面提供了更大的灵活性。因此,对象可以像Ab1
一样部署,但也可以更多。
这与函子(函数对象)有什么关系吗?
请注意,第一部分的答案通常对许多编程语言(包括 C++)有效,这部分与 C++(您引用的规范)有关。所以你有一个函数的指针,你也可以有一个对象的指针。它只是 C++ 定义的另一种编程结构。请注意,这是关于拥有一个指向Ab1
、Ab2
的指针来操作它们,而不是使用另一个不同的抽象来操作。
你可以在这里阅读它的定义和用法:
C++ Functors - and their uses
【讨论】:
您的前两段很难阅读,可以用几句话概括。我猜你没有得到选票,因为大多数人只是在到达“不同......为什么和如何?”之前停止阅读。 @YSC :我做了一些修改。你现在怎么看?你能看一下并提出修改建议以使其更具可读性吗? 这样更好:)。【参考方案3】:让我用更简单的语言(术语)回答这个问题。
函数包含什么?
它基本上包含做某事的指令。在执行指令时,该函数可以临时存储和/或使用一些数据 - 并可能返回一些数据。
虽然指令存储在某处 - 这些指令本身不被视为对象。
那么,对象是什么?
通常,对象是包含数据的实体 - 由函数(指令)操作/更改/更新。
为什么会有差异?
因为计算机的设计方式是指令不依赖于数据。
为了理解这一点,让我们考虑一个计算器。我们使用计算器进行不同的数学运算。比如说,如果我们想添加一些数字,我们将这些数字提供给计算器。无论数字是多少,计算器都会按照相同的指令以相同的方式将它们相加(如果结果超出计算器的存储容量,则会显示错误 - 但这是由于计算器存储结果的限制(数据),而不是因为它的添加说明)。
计算机的设计方式类似。这就是为什么当您在与该函数兼容的某些数据上使用库函数(例如qsort()
)时,您会得到与预期相同的结果 - 并且如果数据发生变化,函数的功能不会改变 -因为函数的指令保持不变。
函数和函子的关系
函数是一组指令;在执行它们时,可能需要存储一些临时数据。换句话说,一些对象可能是在执行函数时临时创建的。这些临时对象是函子。
【讨论】:
以上是关于为啥函数不是对象?的主要内容,如果未能解决你的问题,请参考以下文章
为啥我的 Dart 构造函数返回的是动态对象而不是类型对象?
为啥Java的"函数"叫做方法?(为啥用'method',而不是'function')