函数指针的解引用是如何发生的?

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 类型的指针,而不是指向数组的指针。因此,取消引用它会给你一个元素,而不是一个数组,并且你表现出的疯狂不会发生。

希望这会有所帮助。

附:至于为什么函数值会被隐式转换为指针,答案是对于我们这些使用函数指针的人来说,不用到处使用&amp; 是非常方便的。还有双重便利:调用位置的函数指针会自动转换为函数值,因此您不必编写* 来通过函数指针调用。

附言与 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 运算符或一元 &amp; 运算符的操作数,否则类型为“函数返回 type”的函数指示符将转换为类型为“指向函数返回的指针”的表达式类型”。

考虑以下代码:

void func(void);

int main(void)

    void (*ptr)(void) = func;
    return 0;

这里,函数指示符func 的类型为“函数返回void”,但立即转换为类型为“返回函数的指针void”。但是,如果你写

void (*ptr)(void) = &func;

那么函数指示符func 的类型为“函数返回void”,但一元运算符&amp; 显式获取该函数的地址,最终产生“函数指针”类型返回void”。

这在 C 标准中有提到:

ISO/IEC 2011,第 6.5.3.2 节地址和间接运算符,第 3 段

一元 &amp; 运算符产生其操作数的地址。如果操作数的类型为“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] 指令。

【讨论】:

以上是关于函数指针的解引用是如何发生的?的主要内容,如果未能解决你的问题,请参考以下文章

10. 第 10 章 指针

c++异步回调函数引用传递空指针异常

引用与指针的区别

将c ++引用的地址传递给指针

virtual 函数只有在用指针或引用的方式访问,才会导致多态。

指针能作为引用参数吗?