繁体   English   中英

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

[英]How does dereferencing of a function pointer happen?

为什么以及如何取消引用函数指针只是“什么都不做”?

这就是我要说的:

#include<stdio.h>

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

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

来自这里的评论:

函数指针解引用就好了,但是生成的函数指示符将立即转换回函数指针


这里的答案:

取消引用(以您认为的方式)函数的指针意味着:访问 CODE 内存,就像访问 DATA 内存一样。

函数指针不应该以这种方式取消引用。 相反,它被称为。

我会与“调用”并排使用名称“取消引用”。 没关系。

无论如何:C 的设计方式是,函数名称标识符以及保存函数指针的变量都具有相同的含义:指向 CODE 内存的地址。 它允许通过在标识符或变量上使用 call () 语法跳转到该内存。


函数指针的解除引用究竟是如何工作的?

这不是一个完全正确的问题。 至少对于 C,正确的问题是

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

(右边的值上下文是在任何地方的名称或其它引用出现在它应该被用作一个值,而不是一个位置-基本上任何地方除外在赋值的左手侧的名称本身来自-手侧一个任务。)

好的,那么右值上下文中的函数值会发生什么? 它会立即隐式转换为指向原始函数值的指针。 如果使用*取消引用该指针,则会再次返回相同的函数值,该值会立即隐式转换为指针。 您可以根据需要多次执行此操作。

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

  • 如果在左值上下文(赋值的左侧)中取消引用函数指针会发生什么。 (如果您牢记函数是不可变的,那么答案将与您的期望有关。)

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

希望这可以帮助。

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

PPS 与 C 函数不同,C++ 函数可以重载,我没有资格评论 C++ 中的语义如何工作。

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

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

让自己站在编译器作者的角度。 函数指针具有明确定义的含义,它是指向表示机器代码的字节 blob 的指针。

当程序员取消引用一个函数指针时,你会怎么做? 您是否将机器代码的第一个(或 8 个)字节重新解释为指针? 这行不通的可能性大约是 20 亿比 1。 你申报UB吗? 已经有很多这样的事情了。 或者你只是忽略了这种尝试? 你知道答案。

函数指针的解除引用究竟是如何工作的?

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

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

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

它发生在一些隐式转换中。 事实上,根据 C 标准:

ISO/IEC 2011,第 6.3.2.1 节 Lvalues、数组和函数指示符,第 4 段

函数指示符是具有函数类型的表达式。 除非它是sizeof运算符或一元&运算符的操作数,否则类型为“函数返回类型”的函数指示符将转换为类型为“指向函数返回类型的指针”的表达式。

考虑以下代码:

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 ”,则结果的类型为“指向类型的指针”。

特别是,取消引用函数指针是多余的。 根据 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指令做了两件事:

  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;
}

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM