C语言中的函数指针 - 特性和用法

32

我刚读到一个有趣的问题这里,它让我想起了两件事:

  1. 为什么有人要比较函数指针,因为按照设计,函数的唯一性是通过它们不同的名称来确保的呢?
  2. 编译器是否将函数指针视为特殊指针?我的意思是,它是否像指向void *的指针一样看待它们,或者它是否持有更丰富的信息(例如返回类型、参数数量和参数类型)?

3
“更丰富的信息” 存储在指针 类型 中,例如 int (*) (int)。此类型存储函数应该如何调用的信息。 - user202729
而且... using ptr = int (*) (int); ptr a = f; ptr b = f; if (a == b) { /* a 等于 b */ } - user202729
@user202729:所以你想说的是:“在使用多个指针浏览函数时可能会有用”,对吗? - Benjamin Barrois
需要进行比较的代码并不一定知道指针来自哪里,以及在源代码中它被命名为什么。 - Thorbjørn Ravn Andersen
7个回答

36

为什么有人需要比较函数指针?以下是其中一个例子:

#include <stdbool.h>

/*
 * Register a function to be executed on event. A function may only be registered once.
 * Input:
 *   arg - function pointer
 * Returns:
 *   true on successful registration, false if the function is already registered.
 */
bool register_function_for_event(void (*arg)(void));

/*
 * Un-register a function previously registered for execution on event.
 * Input:
 *   arg - function pointer
 * Returns:
 *   true on successful un-registration, false if the function was not registered.
 */
bool unregister_function_for_event(void (*arg)(void));

register_function_for_event函数的主体仅能看到arg,无法看到任何函数名称。因此,它必须比较函数指针以报告某人注册了相同的函数两次。

如果您想支持类似unregister_function_for_event的功能来补充上述内容,则唯一拥有的信息是函数地址。所以,您需要再次传递它,并进行比较,以允许删除。

至于更丰富的信息,当函数类型包含原型时,它是静态类型信息的一部分。请注意,在C中,函数指针可以声明为没有原型,但这是一种过时的特性。


19
  1. 为什么有人要比较指针?考虑以下场景 -

    你有一个函数指针的数组,比如回调链,你需要调用它们中的每一个。该列表以一个NULL 指针(或标记值)终止。您需要通过与此标记指针进行比较来判断是否已到达列表的末尾。此外,这种情况证明了先前 OP 的担忧,即即使相似的函数也应具有不同的指针。

  2. 编译器是否会将它们视为不同?是的。类型信息包括有关参数和返回类型的所有信息。

    例如,以下代码将/应被编译器拒绝 -

    void foo(int a);
    void (*bar)(long) = foo; // Without an explicit cast
    

很好,“回调链”可能是最短的答案。+1 - user541686
1
你说什么,函数指针不能为null?完全可以有一个null函数指针。具有静态存储期且没有显式初始化程序的函数指针甚至默认初始化为null指针。 - user2357112
@user2357112 我想这意味着通过引用实际函数得到的函数指针永远不会为空 -- 因此如果你有一个 void foo() {},那么 foo != NULL - anon
3
这个答案是在讨论如何终止一个函数指针数组的情境中提出的。在这种情况下,空函数指针会是很自然的选择(当然要用 0NULL 表示,而不是尝试获取实际函数的地址)。 - user2357112
我认为这里的解释相当薄弱,尽管这主要是问题没有包含更多上下文的错。所引用的问题涉及非标准的COMDAT折叠,与nullptr进行比较时不会出现问题。只有在将两个实际函数相互比较时才会出现问题。 - Voo
显示剩余4条评论

18
  1. 为什么有人会比较函数指针,毕竟按照定义,函数的唯一性是通过它们不同的名称来确保的呢?

一个函数指针可以在程序执行过程中不同的时间指向不同的函数。

如果你有一个变量,例如:

void (*fptr)(int);

它可以指向任何接受int为输入并返回void的函数。

假设你有:

void function1(int)
{
}

void function2(int)
{
}

您可以使用:

fptr = function1;
foo(fptr);
或者:
fptr = function2;
foo(fptr);

根据fptr指向的函数不同,您可能希望在foo中执行不同的操作。因此,需要:

if ( fptr == function1 )
{
    // Do stuff 1
}
else
{
    // Do stuff 2
}
  1. 编译器是否将函数指针视为特殊指针?我的意思是,它是否将它们视为指向void *的指针一样,还是它保存了更丰富的信息(例如返回类型、参数数量和参数类型)?

是的,函数指针是特殊指针,与指向对象的指针不同。

函数指针的类型在编译时拥有所有这些信息。因此,给定一个函数指针,编译器将拥有所有这些信息-返回类型、参数数量及其类型。


1
特别是函数指针的大小可能会有所不同,尤其是在哈佛架构中。函数指针(值)本身并不携带上述信息,而是类型携带。 - PlasmaHH
3
我同意@PlasmaHH所说的,有些CPU架构中函数指针比void*更大:更具体地说,在PPC上,函数指针实际上是指针对的一组指针,其中一个指向代码,另一个指向全局引用表,该表取决于加载该函数的可执行文件/共享库。因此,在PPC上无法将函数指针强制转换为void*,再转换回来。 - cmaster - reinstate monica

5

关于函数指针的经典部分已经在其他答案中讨论过:

  • 像其他指针一样,函数指针可以在不同时间指向不同的对象,因此对它们进行比较是有意义的。
  • 函数指针很特殊,不应该存储在其他指针类型(甚至不是void *,即使在C语言中也是如此)。
  • 丰富的部分(函数签名)存储在函数类型中 - 这就是上述句子的原因。

但是C语言有一个(遗留的)函数声明模式。除了完整的原型模式声明返回类型和所有参数类型外,C还可以使用所谓的参数列表模式,这是旧的K&R模式。在此模式下,声明仅声明返回类型:

int (*fptr)();

在 C 语言中,它声明了一个指针函数,返回类型为 int,并接受任意数量的参数。简单来说,如果使用错误的参数列表,则会出现未定义行为(UB)。

因此,以下是合法的 C 代码:

#include <stdio.h>
#include <string.h>

int add2(int a, int b) {
    return a + b;
}
int add3(int a, int b, int c) {
    return a + b + c;
}

int(*fptr)();
int main() {
    fptr = add2;
    printf("%d\n", fptr(1, 2));
    fptr = add3;
    printf("%d\n", fptr(1, 2, 3));
    /* fprintf("%d\n", fptr(1, 2)); Would be UB */
    return 0;
}

不要假装我建议你这样做!它现在被视为一种过时的功能,应该避免使用。我只是警告你不要使用它。在我看来,它只有极少数可接受的特殊用例。


我不想详细讨论这个功能,因为它已经过时了,但你做的很好。+1 - StoryTeller - Unslander Monica
我不知道这段代码是合法的。如果你将add2add3参数中的int替换为long,它会起作用吗?我的意思是:当你调用fptr(1,2)时,它是否盲目地将1和2作为int发送,这在上面的示例中很方便,还是它是参数类型感知的? - Benjamin Barrois
@BenjaminBarrois:这就是为什么原型被发明的原因之一:当声明不给出参数原型时,编译器盲目地接受参数类型,并且除了将比int等级低的整数类型(char、short、位域)提升为int和将float更改为double之外,不进行转换。在K&R时代,这曾经导致奇怪的错误... - Serge Ballesta
我相信大多数编译器将int (*)()视为一种与int(*)(int, int)不同的独特类型。因此,即使在C89标准中,我也不认为这是有效的标准C。当然,在实践中,指针只是地址,因此如果您确定底层调用约定,甚至可以在不同的函数指针类型之间进行疯狂的转换,并且在实践中它会起作用。 - Lundin
2
@Lundin - 6.3.2.3 表明转换是有效的。而6.7.6.3p15表明指针类型是虚无地兼容的(因为一个没有参数类型列表)。所以这都是有效和危险的C语言。 - StoryTeller - Unslander Monica
显示剩余3条评论

2

1)有很多情况。以有限状态机的典型实现为例:

typedef void state_func_t (void);

const state_func_t* STATE_MACHINE[] =
{
  state_init,
  state_something,
  state_something_else
};

...

for(;;)
{
  STATE_MACHINE[state]();
}

您可能需要在调用程序中包含一些额外的代码来处理特定情况:
if(STATE_MACHINE[state] == state_something)
{
  print_debug_stuff();
}

2) 是的,C编译器将它们视为不同类型。实际上,函数指针比其他类型的C更具有严格的类型安全性,因为它们不能像对象类型的指针那样隐式转换为/从void*(参见C11 6.3.2.3 / 1)。也不能显式地将其强制转换为/从void* - 这样做会调用非标准扩展。

函数指针中的所有内容都很重要,以确定其类型:返回值的类型,参数的类型和参数的数量。所有这些都必须匹配,否则两个函数指针就不兼容。


1
使用FSM作为示例的问题在于可能的转换列表在编译时已知,因此您应该使用枚举上的switch而不是函数指针列表。 - user541686
@Mehrdad,恐怕你还没有提供帮助。如果对于你的披萨问题,回答是“例如,它可以防止你在热披萨奶酪上烫伤手指”,那就是一个好的回答。它并不涵盖所有情况,但它只是一个可以推广到其他类似情况的例子。只要它是有效的(而且它是有效的;意大利人总是用餐具吃披萨!),那么它就是一个好的例子。如果你反对FSM示例,因为它不是一个有效的场景(就像我说的那样),那么好吧。如果你反对它,因为它不能涵盖每种可能的情况,那就不明智了。 - Graham
@Mehrdad 如果您能使用原始问题和原始答案来解释您的反对意见,而不是使用无关的类比,也许我们可以看看是否同意您的观点。我以为我理解了您的反对意见,因为有限状态机通常不会进行函数指针比较,因此答案中的示例与问题实际上并不相关。但现在您说这不是原因?毫不想冒犯地说,我必须说您在这里沟通失败了。 - Graham
@Graham:告诉人们他们没有成功地沟通并不是无礼的行为。无礼的是同时宣称他们的比喻(与他们自己的观点相关!)是“不相关的”。如果你不理解,那么你应该暂缓评判。如果你理解了,那么你也可以提供另一种解释,然后坦诚地说你理解但不同意。你不必撒谎,浪费他们的时间,并假装你不理解你不喜欢或不同意的东西。 - user541686
让我们在聊天中继续这个讨论 - Graham
显示剩余9条评论

2
想象一下,如果你要实现与 WNDCLASS 类似的功能,该怎么做呢?
它有一个 lpszClassName 用于区分不同的窗口类,但是假设您不需要(或没有)可用于区分不同类之间的字符串。
您所拥有的是窗口类过程 lpfnWndProc(类型为 WindowProc)。
那么,如果有人使用相同的 lpfnWndProc 两次调用 RegisterClass,你会怎么做呢?
您需要以某种方式检测相同类的重新注册并返回错误。
这就是逻辑上需要比较回调函数的情况之一。

1
你知道吗,15年前这将是一个非常好的例子。但我认为现在许多新程序员没有接触到win32 api...现在一切都围绕着更高级别的东西。 - Jules
1
@Jules:我想这就是为什么我说“类似于WNDCLASS的功能”而不是“WNDCLASS”本身。老实说,我不确定C语言本身是否首先是现代抽象的典范... - user541686

1

函数指针是变量。为什么要比较变量,毕竟按照概念,变量的唯一性由它们不同的名称保证?然而,有时两个变量可以具有相同的值,你想找出是否是这种情况。

C语言认为具有相同参数列表和返回值的函数指针属于相同类型。


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