为什么只要函数指针的行为与函数本身相似,函数指针就会存在?

Why do function pointers exist as long as they act similar to functions themselves?

我知道有一个类似的主题,但这些答案并没有阐明我想了解的内容。因此,只要从下面的摘录中可以注意到一个函数和对该函数的引用的行为方式相同,那么同时拥有函数变量和函数指针有什么意义。

#include <stdio.h>

int f(){ return 0;}

int main() {
    printf("%p\n",f);
    printf("%p\n",&f);
    printf("%p\n",f + 1);
    printf("%p\n",&f + 1);
    return 0;
}

此外,f 和 &f 都可以作为其他函数的参数传递。

f&f是一个意思,不用&f.

但是语言设计者出于某种原因决定不让它无效,所以你有这个冗余语法。

没有“函数变量”这样的东西。

只要在表达式中使用,函数就会“退化”为指向函数的指针。该规则的唯一例外是在表达式中与 & 运算符一起使用时。然后它不衰减而是以函数指针的形式给出函数地址。

因此 f&f 在您的 printf 语句中都是相同的函数指针。至于原因,可能只是为了让它们与数组的行为保持一致,数组也会使用类似的规则衰减。 array vs &array 也给出相同的地址。

至于为什么数组首先会退化为指针,这就是语言的设计方式。发明 C 的 Dennis Ritchie 不想将地址或大小与数组一起存储(就像在古老的前身 B 语言中所做的那样),因此他想出了这种方法。

f + 1 是函数指针的指针算术,没有任何意义,就像您的编译器在拒绝编译发布的代码时告诉您的那样。

当您希望变量引用特定函数时,函数指针是必需的。例如:

#include <stdio.h>

void foo1(void)
{
    printf("foo1\n");
}

void foo2(void)
{
    printf("foo2\n");
}

int main()
{
    void (*f)(void) = foo1;
    f();
    f = foo2;
    f();
    return 0;
}

输出:

foo1
foo2

如果您尝试这样做:

void f(void) = foo1;

这将是无效的语法。如果你删除了初始值设定项:

void f(void);

你会有一个函数声明

正如其他人所指出的:&ff 是同一件事:当分配 f 或将其作为参数传递时,就像数组一样,您的值're passing/assigning 是函数(指针)的地址。

如果您要问的问题是“为什么会有函数指针”,那么您可能需要考虑这个问题:

假设您正在编写一个类似 GTK 的库。您会希望所述库的用户能够处理诸如单击按钮之类的事情。这是通过允许用户传入函数指针来完成的。单击按钮时,指针将指向要调用的函数。最简单的示例:带有退出应用程序按钮的 GTK window:

    g_signal_connect(
        G_OBJECT(close_button),
        "clicked",
        G_CCALLBACK(close_window_cb),
        G_OBJECT(sub_window));

我基本上是在告诉 GTK,如果对象 close_button 被点击,我想调用一个回调函数。此回调作为函数指针传入。我还告诉 GTK 将指向 sub_window 的指针作为第二个参数传递,因此我的函数可以关闭 window(销毁小部件)。

回调本身非常简单,如下所示:

void close_window_cb(GtkWidget *w, gpointer window)
{
    gtk_widget_destroy(GTK_WIDGET(window));
}

您可能认为将 gtk_widget_destroy 绑定到事件会更容易,但是如果我想记录某些内容或执行其他操作怎么办?

void close_window_cb(GtkWidget *w, gpointer window)
{
    printf("Button %s was clicked", gtk_widget_get_name(w));
    gtk_widget_destroy(GTK_WIDGET(window));
}

如果不支持自定义回调函数,实现起来会很麻烦。

TL;DR

不需要函数指针,但它们在很多情况下都非常有用。

一元运算符 & 和一元运算符 * 在函数和函数指针上的一些奇怪行为是有历史原因的,这都是为了增加便利性,同时保持向后兼容性旧代码。

在C语言标准化之前,需要对函数应用&运算符才能将其转换为函数指针,需要应用*运算符到函数指针,以便将其转换为可以调用的相应函数类型。例如:

/* K&R style function pointer example. */
int func();  /* external function declaration */

int foo()
{
    int (*fp)(); /* function pointer variable */
    int val;

    fp = &func;  /* assign address of function func to function pointer fp */
    val = (*fp)(42); /* dereference and call function pointer fp */
    return val;
}

关于 C 语言的标准化,“函数调用”操作 ( params... ) 不再对 函数 进行操作,而是对 函数进行操作指针。但是,因为将 & 运算符应用于函数以将其转换为可以调用的函数指针是不方便的(即调用函数 func(&func)(42) 会很不方便) ,函数类型的表达式(例如函数声明中的标识符)会自动转换为可以调用的函数指针值(即 func(42)),除非它是一元 [=14] 的操作数=]、sizeof_Alignof 运算符。这意味着像int (*fp)(int);这样的函数指针变量可以直接调用为fp(42)。这也意味着对于函数 func,表达式 func(&func) 是等价的。

函数类型表达式的另一个示例(标准定义术语 函数指示符 表示函数类型的表达式)来自应用一元 * 运算符或数组下标运算符 [ index ] 到函数指针。这意味着调用函数指针变量指向的函数的旧的、准标准的方法仍然像往常一样工作。例如,在调用(*fp)(42)中,fp的函数指针值被一元运算符*转换为函数类型的表达式,但是函数类型的表达式(a 函数指示符) 自动转换回函数指针类型的表达式。这意味着以下所有(和类似的)调用函数的方式 func 实际上是等价的:

func(42);
(&func)(42);
(*func)(42);
func[0](42);
(&*&*&*&*&func)(42);
(*&*&*&*&*func)(42);

对于函数指针变量 fp 以下调用函数的类似方法实际上是等效的:

fp(42);
(*fp)(42);
(&*fp)(42);
fp[0](42);
(*&*&*&*&*fp)(42);

请注意,函数指针变量 fp(&fp)(42) 不是有效的函数调用,因为 &fp 是指向函数指针的 指针 并且函数调用操作仅适用于函数指针。

对于标准C,前面的K&R风格的函数指针例子可以重写如下:

/* New style function pointer example. */
int func(int);  /* external function declaration */

int foo(void)
{
    int (*fp)(int); /* function pointer variable */
    int val;

    fp = func;  /* assign pointer to function func to function pointer fp */
    val = fp(42); /* call function pointer fp */
    return val;
}

至于函数指针类型的变量(或参数),正如其他人指出的那样,它们有其用途。

函数指针是指代函数的变量。这个概念在许多场景中都很有用。 C是静态链接的,每个常规函数调用都解析为链接器分配的固定地址。函数指针允许在 运行 时间决定调用哪个函数。

一些示例:

  • 标准库 qsort() 使用函数指针使其能够对任意对象类型进行操作,而不是为您可能想要排序的每种对象类型都需要一个单独的函数。

  • 中断处理程序可以是函数指针-写入中断向量table,允许您在运行时动态安装一个中断处理程序。

  • 它可以通过用查找 table 或关联映射替换大的 switch-case 或 if-else if-else 结构来简化命令解析器或菜单处理程序。

  • 事件处理程序。例如,一个库可能会生成一个事件,该事件需要一个独立于库编写的用户定义的处理程序。例如 POSIX signal() 函数。

我同意函数指示符上的 & 运算符是多余的,因为 f&f 的计算结果相同 和类型 (不同于数组)。我怀疑它的存在更多是为了保持一致性。


函数指针本身有很多用途。

它们允许您将函数作为参数传递给另一个函数以执行该函数(a.k.a。回调)。典型的例子是标准库函数 qsort:

void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *));

qsort调用compar指向的函数来决定数组元素的排序方式。您可以通过传递不同的回调来更改顺序。

我使用过的几个基于 C 的 GUI 框架都有回调。

函数指针允许您在运行时通过加载共享库向应用程序添加功能:

#include <dlfcn.h>
...
void *libhandle = dlopen( "libfoo.so", RTLD_LAZY | RTLD_GLOBAL );
...
int (*do_something)(void) = dlsym( libhandle, "some_function_name" );
if ( do_something )
{
  int x = do_something();
  ...
}

它们允许你将一个行为附加到一个对象上——例如,我可以将一个比较函数附加到一个列表对象上,这样我就可以控制列表的排序(类似于传递 compar 函数到 qsort):

struct node {
  data_t data; // for some arbitrary data type
  struct node *next;
};

struct list {
  struct node *head;
  int (*compar)( const data_t, const data_t );
};
...
void insert( struct list *l, data_t value )
{
  struct node *new = new_node( value ); // allocates node and assigns data
  struct node *cur = l->head, *prev = NULL;
  while ( l->compar( cur->data, new->data ) <= 0 )
  {
    prev = cur;
    cur = cur->next;
  }
  new->next = cur;

  if ( prev )
  {
    prev->next = new;
  }
  else
  {
    l->head = new;
  }      
}

所以我可以创建两个列表:

struct list a, b;

我可以通过附加不同的比较函数来订购不同的方式:

int sort_ascending( const data_t a, const data_t b )
{
  ...
}

int sort_descending( const data_t a, const data_t b )
{
  ...
}

a.compar = sort_ascending;
b.compar = sort_descending;

您可以构建可根据一个或多个条件执行函数的查找 tables,添加新功能只需编写新函数并将它们添加到 table ,而不是破解应用程序逻辑本身:

void (*do_something[])( void ) = { do_A, do_B, do_C };

...

int x = get_some_value();
do_something[x]();