为什么函数指针定义可以在任意数量的 '&' 或 '*' 下工作?

248

为什么以下代码可以正常工作?

void foo() {
    cout << "Foo to you too!\n";
};

int main() {
    void (*p1_foo)() = foo;
    void (*p2_foo)() = *foo;
    void (*p3_foo)() = &foo;
    void (*p4_foo)() = *&foo;
    void (*p5_foo)() = &*foo;
    void (*p6_foo)() = **foo;
    void (*p7_foo)() = **********************foo;

    (*p1_foo)();
    (*p2_foo)();
    (*p3_foo)();
    (*p4_foo)();
    (*p5_foo)();
    (*p6_foo)();
    (*p7_foo)();
}
5个回答

251

这其中有几个要点,使得所有这些运算符的组合都以相同的方式工作。

让所有这些工作的根本原因是一个函数(如foo)可以隐式转换为指向该函数的指针。这就是为什么 void (*p1_foo)() = foo; 起作用的原因: foo 隐式转换为指向自己的指针,然后将该指针赋给了 p1_foo

一元运算符 & 应用于函数时,与应用于对象时一样,会产生指向该函数的指针。对于普通函数的指针而言,它总是多余的,因为存在隐式从函数到函数指针的转换。无论如何,这就是为什么 void (*p3_foo)() = &foo; 起作用的原因。

一元运算符 * 应用于函数指针时,与应用于普通对象指针时一样,会产生指向函数的指针。

这些规则可以结合使用。考虑你倒数第二个例子,**foo:

  • 首先,foo 隐式转换为指向自己的指针,然后第一个 * 应用于该函数指针,再次产生函数 foo
  • 然后,结果再次隐式转换为指向自己的指针,并应用第二个 *,再次产生函数 foo
  • 然后,它再次隐式转换为函数指针,并赋给变量。

你可以添加任意数量的 *,结果总是相同的。 * 越多,越好。

我们也可以考虑你的第五个例子 &*foo:

  • 首先,foo 隐式转换为指向自己的指针;一元运算符 * 应用于该指针,再次产生 foo
  • 接着,& 被应用于 foo 上,生成指向 foo 的指针,该指针被赋值给该变量。

但是注意,& 只能应用于函数,而不能应用于已转换为函数指针的函数(除非函数指针是变量,在这种情况下结果是指向函数的指针的指针;例如,您可以将 void (**pp_foo)() = &p7_foo; 添加到列表中)。

这就是为什么 &&foo 不起作用的原因:因为 &foo 不是一个函数,它是一个函数指针,是一个右值。然而,&*&*&*&*&*&*foo&******&foo 都可以工作,因为在这两个表达式中,& 总是应用于函数,而不是应用于右值函数指针。

还要注意,您不需要使用一元运算符 * 来通过函数指针进行调用;(*p1_foo)();(p1_foo)(); 产生相同的结果,这又是因为函数到函数指针的转换。


2
你也不能对对象链接 & 运算符:给定 int p;&p 产生一个指向 p 的指针并且是一个右值表达式;& 运算符需要一个左值表达式。 - James McNellis
2
"什么是rvalues,lvalues,xvalues,glvalues和prvalues?"的答案中有许多关于lvalues和rvalues的好解释。这个问题涉及到C++0x的特性,但是答案很好地解释了rvalues和lvalues之间的区别。 - James McNellis
14
我不同意。星号越多,越不开心。 - Seth Carnegie
32
请不要编辑我的例子的语法。我特别挑选了这些例子来展示语言的特点。 - James McNellis
14
顺便提一下,C标准明确规定&*的组合会相互抵消(6.5.3.2):“一元&运算符返回它操作数的地址。” /--/ “如果操作数是一元*运算符的结果,则既不评估该运算符也不评估&运算符,结果就好像两者都被省略了一样,但是仍然适用于运算符的约束,并且结果不是左值。” - Lundin
显示剩余10条评论

13

我认为,记住C语言只是底层计算机的抽象之一,这也是这种抽象泄漏的地方之一,这一点也很有帮助。

从计算机的角度来看,函数只是一个内存地址,如果被执行,就会执行其他指令。因此,在C语言中,函数本身被建模为一个地址,这可能导致设计上函数“与其指向的地址相同”的观念。


2

如果你仍然对@JamesMcNellis的答案不太信服,这里有一个证明。这是Clang编译器中的AST(抽象语法树)。抽象语法树是编译器内部表示程序结构的一种方式。

void func1() {};
void test() {
    func1();
    (*func1)();
    (&func1)();

    void(*func1ptr)(void) = func1;
    func1ptr();
    (*func1ptr)();
    //(&func1ptr)();//error since func1ptr is a variable, &func1ptr is its address which is not callable.
}

AST:

//func1();
|-CallExpr //call the pointer
| `-ImplicitCastExpr //implicitly convert func1 to pointer
|   `-DeclRefExpr //reference func1

//(*func1)();
|-CallExpr //call the pointer
| `-ImplicitCastExpr //implicitly convert the funtion to pointer
|   `-ParenExpr //parentheses
|     `-UnaryOperator //* operator get function from the pointer
|       `-ImplicitCastExpr //implicitly convert func1 to pointer
|         `-DeclRefExpr //reference func1

//(&func1)();
|-CallExpr //call the pointer
| `-ParenExpr //parentheses
|   `-UnaryOperator //& get pointer from func1
|     `-DeclRefExpr //reference func1

//void(*func1ptr)(void) = func1;
|-DeclStmt //define variable func1ptr
| `-VarDecl //define variable func1ptr
|   `-ImplicitCastExpr //implicitly convert func1 to pointer
|     `-DeclRefExpr  //reference func1

//func1ptr();
|-CallExpr  //call the pointer
| `-ImplicitCastExpr //implicitly convert func1ptr to pointer
|   `-DeclRefExpr //reference the variable func1ptr

//(*func1ptr)();
`-CallExpr //call the pointer 
  `-ImplicitCastExpr //implicitly convert the function to pointer
    `-ParenExpr //parentheses
      `-UnaryOperator //* get the function from the pointer
        `-ImplicitCastExpr //implicitly convert func1ptr to pointer
          `-DeclRefExpr //reference the variable func1ptr

1

&*是在C中声明为函数的符号上的幂等操作,这意味着func == *func == &func == *&func,因此*func == **func,但它们具有不同的类型,所以你会收到警告。

传递给函数的函数地址的参数类型可以是int ()int (*)(),并且可以作为*funcfunc&func传递。调用(&func)()func()(*func)()相同。Godbolt链接。

*&在函数符号上没有意义,编译器会将其解释为func的地址,而不会产生错误。函数不像数组符号一样存在一个单独的指针,因此&arrarr相同,因为它不是运行时具有地址的物理指针,而是编译器级别的逻辑指针。此外,*func将读取函数代码的第一个字节,这是一个代码段,编译器不会产生编译错误或允许它成为运行时错误分段错误,而是将其解释为函数的地址。

然而,在声明为函数指针的符号上使用&将获得指针的地址(因为它现在是一个实际的指针变量,体现在堆栈或数据段上),而funcp*funcp仍将被解释为函数的地址。


-1
在使用指针调用函数foo时,即使括号和星号也可以省略,就像直接使用原始名称调用函数一样,即(*p1_foo)()等同于p1_foo()

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接