再次C++函数指针.关于语法的混淆

C++ function pointers, again. Confusion regarding syntax

本文关键字:语法 函数 C++ 指针 于语法 再次      更新时间:2023-10-16

在这个页面上,我发现了一个很好的函数指针示例 C++ (以及函子,但这个问题与函子无关)。下面是该页面的一些复制粘贴。

#include <iostream>
double add(double left, double right) {
return left + right;
}
double multiply(double left, double right) {
return left * right;
}
double binary_op(double left, double right, double (*f)(double, double)) {
return (*f)(left, right);
}
int main( ) {
double a = 5.0;
double b = 10.0;
std::cout << "Add: " << binary_op(a, b, add) << std::endl;
std::cout << "Multiply: " << binary_op(a, b, multiply) << std::endl;
return 0;
}

我一般理解代码,但有几件事我一直觉得令人困惑。函数binary_op()*f接受函数指针,但是当使用它时,例如在第 19 行binary_op(a, b, add),函数符号add被传入,而不是人们认为的指针,&add。现在你可能会说这是因为符号add是一个指针;它是对应于函数add()的代码位的地址。很好,但是这里似乎仍然存在类型差异。函数binary_op()*f,这意味着f是指向某物的指针。我传入add,它本身就是一个指向代码的指针。(对吧?那么f被赋值add,这使得f成为代码的指针,这意味着f是一个像add一样的函数,这意味着f应该像f(left, right)一样调用,确切地说add应该如何调用,但在第 12 行,它像(*f)(left, right)一样调用, 这对我来说似乎不对,因为它就像编写(*add)(left, right)*add不是函数,而是add指向的代码的第一个字符。(对吧?

我知道用以下内容替换binary_op()的原始定义也是有效的。

double binary_op(double left, double right, double f(double, double)) {
return f(left, right);
}

事实上,这对我来说更有意义,但正如我上面解释的那样,原始语法没有意义。

那么,为什么使用(*f)而不仅仅是f在语法上是正确的?如果符号func本身就是指针,那么短语"函数指针"或"指向函数的指针"究竟是什么意思?按照目前的原始代码,当我们写double (*f)(double, double)时,f什么样的东西呢?指向指针的指针(因为(*f)本身就是指向一段代码的指针)?符号add(*f)是同一类东西,还是和f是同一类东西?

现在,如果所有这些问题的答案是"是的,C++语法很奇怪,只需记住函数指针语法,不要质疑它">,那么我会勉强接受它,但我真的很想对我在这里的想法有一个适当的解释。

我已经阅读了这个问题,我想我理解了这一点,但发现它对解决我的困惑没有帮助。我也读过这个问题,这个问题也没有帮助,因为它没有直接解决我的类型差异问题。我可以继续阅读互联网上的信息海洋来找到我的答案,但是嘿,这就是堆栈溢出的目的,对吗?

这是因为 C 函数指针很特殊。

首先,表达式add将衰减为指针。就像对数组的引用将衰减为指针一样,对函数的引用将衰减为指向函数的指针。

然后,奇怪的东西在那里:

return (*f)(left, right);

那么,为什么使用(*f)而不仅仅是f在语法上是正确的呢?

两者都有效,您可以像这样重写代码:

return f(left, right);

这是因为取消引用运算符将返回对函数的引用,并且对函数的引用或函数指针都被视为可调用。

有趣的是,函数引用很容易衰减,以至于在调用取消引用运算符时它会衰减回指针,从而允许根据需要多次取消引用函数:

return (*******f)(left, right); // ah! still works

按照目前的原始代码,当我们写double (*f)(double, double)时,f什么样的东西呢?

f的类型是double (*)(double, double)即它是指向类型为double(double,double)的函数的指针。

因为(*f)本身就是一个指针

不是。

问:当您通过指针间接(例如在*f中)时,您会得到什么?答:你会得到一个左值引用。例如,给定一个对象指针int* ptr,表达式*ptr的类型是int&即对int的左值引用。

函数指针也是如此:当您间接通过函数指针时,您将获得对指向函数的左值引用。在*f的情况下,类型是double (&)(double, double)即引用类型double(double,double)的函数。

符号add(*f)是同一种东西,还是和f是同一类东西?

非限定 id 表达式add*f是同一种东西,即它是一个左值:

标准草案 [expr.prim.id.unqual]

。如果实体是一个函数,则表达式为左值...


函数符号add被传入,而不是人们认为的指针,&add。现在你可能会说这是因为符号add是一个指针;

不。这不是原因。

add不是指针。这是一个左值。但是函数类型的 lvalues 隐式转换为指针(这称为衰减):

标准草案 [会议功能]

函数类型 T 的左值可以转换为类型为"指向 T 的指针"的 prvalue。结果是指向函数的指针。

因此,以下内容在语义上是等效的:

binary_op(a, b,  add); // implicit function-to-pointer conversion
binary_op(a, b, &add); // explicit use of addressof operator

那么,为什么使用(*f)而不仅仅是f在语法上是正确的?

事实证明,调用函数左值与调用函数指针具有相同的语法:

标准草案 [expr.call]

函数调用是一个后缀表达式,后跟括号,其中包含可能为空的、以逗号分隔的初始值设定项子句列表,这些初始值设定项子句构成函数的参数。 后缀表达式应具有函数类型或函数指针类型。 对于对非成员函数或静态成员函数的调用,后缀表达式应为引用函数的左值(在这种情况下,后缀表达式上禁止显示函数到指针的标准转换 ([conv.func]),或者具有函数指针类型

这些都是相同的函数调用:

add(parameter_list);    // lvalue
(*f)(parameter_list);   // lvalue
(&add)(parameter_list); // pointer
f(parameter_list);      // pointer

附言这两个声明是等效的:

double binary_op(double, double, double (*)(double, double))
double binary_op(double, double, double    (double, double))

这是因为以下规则是对隐式衰减为函数指针的补充:

标准草案 [dcl.fct]

函数的类型使用以下规则确定。 每个参数(包括函数参数包)的类型由其自己的 decl-specifier-seq 和声明符确定。 确定每个参数的类型后,任何类型为"T 数组"或函数类型 T 的参数都将调整为"指向 T 的指针">......

首先,当编译器确定参数的类型时,将指定为函数声明的函数参数调整为指向函数的指针。例如以下函数声明

void f( void h() );
void f( void ( *h )() );

是等效的,并声明相同的一个函数。

考虑以下演示程序

#include <iostream>
void f( void h() );
void f( void ( *h )() );
void h() { std::cout << "Hello Rayn"; }
void f( void h() ) { h(); }
int main()
{
f( h );
}

从 c++ 17 标准(11.3.5 函数):

5 函数的类型使用以下规则确定。这 每个参数(包括函数参数包)的类型为 由其自己的 decl-specifier-seq 和声明符确定。后 确定每个参数的类型,任何类型为"数组"的参数 T"或函数类型 T 的调整为"指向 T 的指针"。

另一方面,根据C++ 17标准

9 当给定参数没有参数时,参数为 以接收函数可以获得值的方式传递 通过援引va_arg(21.11)来论证。[注:本段 不适用于传递给函数参数包的参数。 函数参数包在模板实例化期间展开 (17.6.3),因此每个这样的参数都有一个相应的参数,当 实际上调用了函数模板专用化。— 尾注 ] 左值到右值 (7.1)、数组到指针 (7.2) 和函数到指针 (7.3)对参数表达式执行标准转换

那么这两个声明有什么区别呢?

void f( void h() );
void f( void ( *h )() );

对于第一个声明,您可以将函数体内的参数h视为函数指针的 typedef。

typedef void ( *H )();

例如

#include <iostream>
void f( void h() );
void f( void ( *h )() );
void h() { std::cout << "Hello Rayn"; }

typedef void ( *H )();
void f( H h ) { h(); }
int main()
{
f( h );
}

根据 C++ 17 标准(8.5.1.2 函数调用)

1 函数调用是后缀表达式,后跟括号 包含一个可能为空的逗号分隔列表 构成函数参数的初始值设定项子句。 后缀表达式应具有函数类型或函数指针 类型

所以你也可以定义这样的函数

void f( void h() ) { ( *h )(); }

甚至喜欢

void f( void h() ) { ( ******h )(); }

因为当运算符 * 应用于函数名称时,函数名称将隐式耦合到函数的 pijnter。