函数指针的解引用是如何发生的?
Posted
技术标签:
【中文标题】函数指针的解引用是如何发生的?【英文标题】:How does dereferencing of a function pointer happen? 【发布时间】:2011-02-17 05:50:40 【问题描述】:为什么以及如何取消引用函数指针只是“什么都不做”?
这就是我要说的:
#include<stdio.h>
void hello() printf("hello");
int main(void)
(*****hello)();
来自here的评论:
函数指针只是取消引用 很好,但是结果函数 指定人将立即 转换回函数指针
还有来自here的回答:
取消引用(以您认为的方式) 函数的指针意味着:访问一个 编码内存,因为它将是一个数据 记忆。
函数指针不应该是 以这种方式取消引用。相反,它 被调用。
我会使用“取消引用”的名称 与“呼叫”并排。没关系。
无论如何:C 是这样设计的 这两个函数名称标识符为 以及变量保持函数的 指针的含义相同:CODE的地址 记忆。它允许跳转到那个 通过使用 call() 语法来存储内存 在标识符或变量上。
究竟函数指针的解引用是如何工作的?
【问题讨论】:
【参考方案1】:这不是一个完全正确的问题。至少对于 C,正确的问题是
右值上下文中的函数值会发生什么?
(一个右值上下文是任何一个名字或其他引用出现的地方,它应该被用作一个值,而不是一个位置——基本上除了在赋值的左侧之外的任何地方。名字本身来自 右侧-作业的手边。)
好的,那么在右值上下文中函数值会发生什么?它立即隐式转换为指向原始函数值的指针。如果您使用*
取消引用该指针,您将再次获得相同的函数值,该值会立即隐式转换为指针。您可以根据需要多次执行此操作。
您可以尝试两个类似的实验:
如果您在 lvalue 上下文(赋值的左侧)中取消引用函数指针会发生什么。 (答案将取决于您的期望,如果您记住函数是不可变的。)
在左值上下文中,数组值也被转换为指针,但它被转换为指向 element 类型的指针,而不是指向数组的指针。因此,取消引用它会给你一个元素,而不是一个数组,并且你表现出的疯狂不会发生。
希望这会有所帮助。
附:至于为什么函数值会被隐式转换为指针,答案是对于我们这些使用函数指针的人来说,不用到处使用&
是非常方便的。还有双重便利:调用位置的函数指针会自动转换为函数值,因此您不必编写*
来通过函数指针调用。
附言与 C 函数不同,C++ 函数可以重载,我没有资格评论语义在 C++ 中的工作原理。
【讨论】:
您能否详细说明“...隐式转换为指向原始函数值的指针”?你指的是函数的返回值吗?如果是这样,这是否意味着编译器会自动将该返回值保留为左值,尽管函数返回值是右值。谢谢! 你写函数指针在调用位置会自动转换为函数值——但事实恰恰相反。函数调用和数组索引运算符都要求“函数”/“数组”操作数实际上需要是a pointer。【参考方案2】:C++03 §4.3/1:
函数类型 T 的左值可以转换为“指向 T 的指针”类型的右值。结果是一个指向函数的指针。
如果您尝试对函数引用进行无效操作,例如一元 *
运算符,该语言首先会尝试进行标准转换。就像在将int
添加到float
时转换它一样。在函数引用上使用 *
会导致语言取而代之的是它的指针,在你的例子中,它是正方形 1。
另一种适用的情况是在分配函数指针时。
void f()
void (*recurse)() = f; // "f" is a reference; implicitly convert to ptr.
recurse(); // call operator is defined for pointers
请注意,这不会以其他方式工作。
void f()
void (&recurse)() = &f; // "&f" is a pointer; ERROR can't convert to ref.
recurse(); // OK - call operator is *separately* defined for references
函数引用变量很好,因为它们(理论上,我从未测试过)向编译器暗示,如果在封闭范围内初始化,可能不需要间接分支。
在 C99 中,取消引用函数指针会产生函数指示符。 §6.3.2.1/4:
函数指示符是具有函数类型的表达式。除非它是 sizeof 运算符或一元 & 运算符的操作数,否则类型为“函数返回类型”的函数指示符将转换为类型为“指向函数返回类型的指针”的表达式。
这更像是 Norman 的回答,但值得注意的是 C99 没有右值的概念。
【讨论】:
"在函数引用上" 实际上,表达式不能有引用类型。表达式可以是右值或左值。【参考方案3】:它发生在一些隐式转换中。事实上,根据 C 标准:
ISO/IEC 2011,第 6.3.2.1 节左值、数组和函数指示符,第 4 段
函数指示符是具有函数类型的表达式。除非它是
sizeof
运算符或一元&
运算符的操作数,否则类型为“函数返回 type”的函数指示符将转换为类型为“指向函数返回的指针”的表达式类型”。
考虑以下代码:
void func(void);
int main(void)
void (*ptr)(void) = func;
return 0;
这里,函数指示符func
的类型为“函数返回void
”,但立即转换为类型为“返回函数的指针void
”。但是,如果你写
void (*ptr)(void) = &func;
那么函数指示符func
的类型为“函数返回void
”,但一元运算符&
显式获取该函数的地址,最终产生“函数指针”类型返回void
”。
这在 C 标准中有提到:
ISO/IEC 2011,第 6.5.3.2 节地址和间接运算符,第 3 段
一元
&
运算符产生其操作数的地址。如果操作数的类型为“type”,则结果的类型为“pointer to type”。
特别是,取消引用函数指针是多余的。根据 C 标准:
ISO/IEC 2011,第 6.5.2.2 节函数调用,第 1 段
表示被调用函数的表达式应具有“指向函数的指针返回
void
”类型或返回除数组类型之外的完整对象类型。大多数情况下,这是转换作为函数指示符的标识符的结果。ISO/IEC 2011,第 6.5.3.2 节地址和间接运算符,第 4 段
一元
*
运算符表示间接。如果操作数指向一个函数,则结果是一个函数指示符。
所以当你写的时候
ptr();
函数调用在没有隐式转换的情况下进行评估,因为ptr
已经是一个指向函数的指针。如果你明确地取消引用它
(*ptr)();
然后解引用产生类型“函数返回void
”,该类型立即转换回类型“指向函数返回void
”和函数通话发生。在编写由 x 一元 *
间接运算符如
(****ptr)();
然后你只需重复隐式转换 x 次。
调用函数涉及函数指针是有道理的。在执行函数之前,程序会将函数的所有参数以与记录相反的顺序压入堆栈。然后程序发出call
指令,指示它希望启动哪个功能。 call
指令做了两件事:
-
首先它将下一条指令的地址(即返回地址)压入堆栈。
然后,它修改指令指针
%eip
指向函数的开始。
由于调用函数确实涉及修改指令指针,即内存地址,因此编译器将函数指示符隐式转换为指向函数的指针是有意义的。
尽管这些隐式转换看起来不严谨,但它在 C 中很有用(与具有命名空间的 C++ 不同) 利用结构标识符定义的命名空间来封装变量。
考虑以下代码:
void create_person(void);
void update_person(void);
void delete_person(void);
struct Person
void (*create)(void);
void (*update)(void);
void (*delete)(void);
;
static struct Person person =
.create = &create_person,
.update = &update_person,
.delete = &delete_person,
;
int main(void)
person.create();
person.update();
person.delete();
return 0;
可以在其他翻译单元中隐藏库的实现,并选择仅公开封装函数指针的结构,以使用它们代替 实际 函数指示符。
【讨论】:
很好的解释。【参考方案4】:设身处地为编译器编写者着想。函数指针有一个定义明确的含义,它是一个指向表示机器代码的字节块的指针。
当程序员取消引用函数指针时,你会怎么做?您是否将机器代码的第一个(或 8 个)字节重新解释为指针?这行不通的可能性约为 20 亿比 1。你声明UB吗?已经有很多这样的事情了。还是您只是忽略尝试?你知道答案。
【讨论】:
如果我是编译器编写者,我会将其设为非法。这是一个有点误导性的答案。【参考方案5】:函数指针的解引用究竟是如何工作的?
两个步骤。第一步在编译时,第二步在运行时。
在第一步中,编译器看到它有一个指针和一个取消引用该指针的上下文(例如 (*pFoo)()
),因此它会针对这种情况生成代码,这些代码将在第 2 步中使用。
在第 2 步中,代码在运行时被执行。指针包含一些字节,指示接下来应该执行哪个函数。这些字节以某种方式加载到 CPU 中。一个常见的情况是 CPU 带有显式的CALL [register]
指令。在这样的系统上,函数指针可以只是函数在内存中的地址,而取消引用代码只是将该地址加载到寄存器中,然后是 CALL [register]
指令。
【讨论】:
以上是关于函数指针的解引用是如何发生的?的主要内容,如果未能解决你的问题,请参考以下文章