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

为什么以及如何解除对函数指针的引用只是“什么都不做”?

这就是我所说的:

#include<stdio.h>


void hello() { printf("hello"); }


int main(void) {
(*****hello)();
}

来自 给你的评论:

函数指针取消引用 好吧,但是结果函数 指示器将立即 变回了函数指针


根据 给你的回答:

解除引用(以您认为的方式) 函数的指针意味着: 访问 代码内存,因为它将是一个数据 记忆。

函数指针不应该是这样的 以这种方式去引用。相反,它 叫做。

我会使用一个名称“解引用”方 和“打电话”放在一起,没关系。

无论如何: C 是这样设计的 这两个函数名称标识符都为 以及变量保持函数的 指针的意思相同: 地址到代码 记忆。它允许跳转到 内存使用 call ()语法 在标识符或变量上。


如何解除函数指针的引用?

39774 次浏览

这个问题不完全正确,至少对 C 来说,正确的问题是

在右值上下文中,函数值会发生什么变化?

(rvalue 上下文是指名称或其他引用出现在应该用作值而不是位置的任何地方ーー基本上除了赋值的左边以外的任何地方。名称本身来自作业的 手边。)

好的,那么在右值上下文中函数值会发生什么情况呢?它立即隐式地转换为指向原始函数值的指针。如果使用 *取消引用该指针,将再次获得相同的函数值,该值将立即隐式地转换为指针。你想做多少次就做多少次。

你可以尝试两个类似的实验:

  • 如果在 Lvalue上下文(作业的左边)中取消对函数指针的引用,会发生什么情况。(如果您记住函数是不可变的,那么答案就是您所期望的。)

  • 数组值也被转换为左值上下文中的指针,但是它被转换为指向 元素类型的指针,而不是指向数组的指针。因此,解除对它的引用会给你一个元素,而不是一个数组,你所表现出的疯狂并不会发生。

希望这个能帮上忙。

附言。至于 为什么,函数值被隐式地转换为指针,答案是对于我们这些使用函数指针的人来说,不必到处使用 &是一件非常方便的事情。还有一个双重便利: 调用位置的函数指针会自动转换为函数值,所以你不必编写 *来通过函数指针调用。

P.P.S. 与 C 函数不同,C + + 函数可能会被重载,我没有资格评论 C + + 中的语义是如何工作的。

C + + 034.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 操作符或 unary & 操作符的操作数之外,类型为“ function return type”的函数指示符被转换为类型为“ point to function return type”的表达式。

这更像是诺曼的回答,但值得注意的是,C99没有 rvalue 的概念。

站在编译器编写者的角度考虑问题。函数指针有一个明确的定义,它是一个指向代表机器代码的字节的指针。

当程序员解除对函数指针的引用时,你会怎么做?您是否将机器代码的第一个(或8个)字节重新解释为一个指针?这个方法行不通的几率是20亿分之一。你申报 UB 吗?已经有很多这样的人了。还是你就这么无视他们的企图?你知道答案。

函数指针的解引用到底是如何工作的?

两个步骤: 第一步是在编译时,第二步是在运行时。

在第一步中,编译器看到它有一个指针和一个上下文,在这个上下文中该指针被解引用(比如 (*pFoo)()) ,因此它为这种情况生成代码,这些代码将在第二步中使用。

在步骤2中,在运行时执行代码。指针包含一些字节,指示接下来应该执行哪个函数。这些字节以某种方式加载到 CPU 中。一种常见的情况是 CPU 使用显式的 CALL [register]指令。在这样的系统中,函数指针可以只是内存中某个函数的地址,而解除防御代码只是将该地址加载到一个寄存器中,然后执行一条 CALL [register]指令。

实际上,根据 C 标准:

ISO/IEC 2011,第6.3.2.1节 L 值、数组和函数指示器,第4段

功能指示器功能指示器是具有函数类型的表达式。除非它是 sizeof操作符或一元 &操作符的操作数,否则类型为“ function return 类型”的函数指示符将转换为类型为“ point to function return 类型”的表达式。

考虑以下代码:

void func(void);


int main(void)
{
void (*ptr)(void) = func;
return 0;
}

在这里,函数指示符 func的类型为“ function return void”,但是它会立即转换为一个表达式,该表达式的类型为“ point to function return void”。但是,如果你写

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

然后函数指示符 func的类型为“ function return void”,但是一元 &操作符显式地获取该函数的地址,最终生成类型为“ point to function return void”。

C 标准中提到了这一点:

ISO/IEC 2011,第6.5.3.2节地址和间接操作员,第3段

一元 &运算符生成其操作数的地址。如果操作数类型为“ 类型”,则结果类型为“指向 类型的指针”。

特别是,取消对函数指针的引用是多余的,根据 C 标准:

ISO/IEC 2011,第6.5.2.2节函数调用,第1段

表示被调用函数的表达式应该具有类型“指向返回 void的函数的指针”或返回数组类型以外的完整对象类型。通常,这是转换作为函数指示器的标识符的结果。

ISO/IEC 2011,第6.5.3.2节地址和间接操作员,第4段

一元 *运算符表示间接。如果操作数指向一个函数,结果是一个函数指示符。

所以当你写作的时候

ptr();

函数调用的计算没有隐式转换,因为 ptr已经函数指针。如果使用

(*ptr)();

然后解引用生成类型“ function return void”,该类型立即转换回类型“ point to function return void”,并发生函数调用。当编写由 X一元 *间接运算符组成的表达式时,如

(****ptr)();

然后重复隐式转换 X次。


调用函数涉及函数指针是有意义的。在执行一个函数之前,程序将函数的所有参数按照记录在案的相反顺序推送到堆栈上。然后程序发出 call指令,指示它希望启动哪个函数。call指令做两件事:

  1. 首先,它将下一条指令的地址(即返回地址)推送到堆栈上。
  2. 然后,它修改指令指针 %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;
}

可以在其他转换单元中隐藏库的实现,并选择仅公开封装函数指针的结构,以使用它们代替 真的函数指示器。