【问题标题】:Why to use function pointer? [duplicate]为什么要使用函数指针? [复制]
【发布时间】:2023-03-04 02:48:01
【问题描述】:

我希望这是一个非常重复的问题。以及我向所有觉得它烦人的观众提前的借口。

虽然我是一位经验丰富的程序员,但我不能证明使用函数指针而不是直接调用是合理的。我无法找到差异的场景是 -

1) 回调 - 同样可以通过直接调用来实现。

2) 异步或同步事件处理 - 无论如何都必须根据元素编号来识别事件。在函数指针数组中得到更新。但是同样可以通过直接调用来完成。

3) 在一些帖子中,我看到有人评论说,当它不知道要调用哪个函数时使用它。我没有得到任何适当的理由。

如果有人可以通过实际且非常简单的现实示例使用上述场景来解释我,我真的很感激。

【问题讨论】:

  • C 还是 C++?如果 C++,除非与 C 接口,否则答案几乎永远不会;因为 lambdas 和 std::function 已经取代了它们
  • 函数指针不是在 C 中传递函数的唯一方法吗?如果没有函数指针,你将如何实现回调?
  • “我没有得到任何适当的理由。”尝试编写一个库,你会很快得到你的理由:-)
  • 看qsort的实现。看看它如何允许用户定义的比较器与可能的用户定义的数据类型。
  • @Vikere 回调通常意味着为某些代码提供一个函数,让它们在未来使用它来“回调”。这必然涉及传递一个函数,或者像指针这样代表函数的东西。我不确定你在这个问题中的回调是什么意思。如果你只是在定义它的范围内调用一个函数,不,你不一定需要一个指针。

标签: c++ c programming-languages


【解决方案1】:

函数指针经常用于的其他一些事情:

  • 运行时多态性:你可以定义一个结构来封装一个函数指针,或者一个指向函数表的指针。这使您能够在运行时调用指定的函数,即使是编写库时不存在的客户端对象类型。您可以使用它来实现多分派或类似 C 中的访问者设计模式。这也是 C++ 类及其 virtual 成员函数最初在底层实现的方式。
  • 闭包:这些可以是包含函数指针及其一个或多个参数的结构。
  • 状态机:我经常发现为每个状态的处理程序提供自己的函数,而不是 switch 和 case 的每个状态标签。当前状态是你所在的函数,状态转换是尾递归调用,程序变量是参数。然后状态标签成为函数指针,您可以将其存储在表中或从函数返回。
  • 高阶函数:来自 C 标准库的两个示例是 qsort() 和 btree(),它们概括了元素的类型和比较函数。
  • 低级支持:例如,共享库加载器需要这个。

【讨论】:

  • 另一个用例可能类似于所谓的“配方工厂”:假设您有一系列您希望代码能够执行的操作,并且取决于一些动态变量/状态,你想选择这些和那些动作,并在运行时将它们组合成一个“配方”以供它执行。每种类型的指令都可以直接调用,但是当您将它们全部组合成一个配方时,您希望能够只调用一个又一个函数,而不是在每条指令上运行一个开关/状态机,这会很慢。函数指针很快。
  • 一个例子是,如果您有某种代数/函数文本框,用户可以在其中输入,例如pow(X + Y / Z, 2) - Excel 将是一个很好的例子 - 您希望非常快速或批量执行指定的算法。调用一系列准备好的函数比每次都重新评估算法要快得多。
【解决方案2】:

1) 回调——同样可以通过直接调用来实现。

不正确。对于直接调用,调用者在编译代码时必须知道函数名和签名,并且只能调用那个函数。回调在runtime 中定义并且可以动态更改,而调用者只需要知道签名,而不需要知道名称。此外,对象的每个实例可能有不同的回调,而直接调用时,所有实例都必须调用相同的函数。

2) 异步或同步事件处理 - 无论如何事件必须是 识别,基于哪个元素编号。在函数指针数组中得到 更新。但是同样可以通过直接调用来完成。

不确定您的意思,但事件处理程序只是一种回调。该事件可以由调用者和通过指针调用的不同回调处理程序来标识。只有当所有事件类型都有一个事件处理程序并且用户负责识别时,您的观点才成立。

3) 在一些帖子中,我看到人们评论说,当不知道要调用哪个函数时使用它。我没有得到任何适当的理由。

参见上面的 (1) 和 (2)。通常,这是一种将独立于平台的第三方库代码挂接到特定平台的方法,而无需交付源代码或用于需要用户/应用程序定义的处理程序的系统事件。

不过,我不会担心 - 如果您的所有应用程序需求都可以在不使用指向函数的指针的情况下得到解决,那么您就不需要指向函数的指针。当你需要一个时,你可能会知道。当您必须在自己实现需要它的接口之前使用需要它的 API 时,您很可能会遇到它。例如,在标准库中,qsort() 函数需要一个指向函数的指针,以便定义如何对任意类型的两个对象进行排序 - 允许 qsort() 支持任何类型的对象 - 这是 C 中的一种方式制作一个函数“多态”。 C++ 直接支持多态性,因此在 C++ 中通常不需要显式函数指针 - 尽管在任何情况下内部多态性都是使用函数指针实现的。

【讨论】:

    【解决方案3】:

    编程中有一个概念叫做 DRY——不要重复自己。

    假设您的 UI 中有 121 个按钮。他们每个人的行为都差不多,除了当你按下按钮时,会发生不同的操作。

    您可以 (A) 使用虚拟继承来调度正确的操作(每个按钮需要一个类),或者 (B) 使用存储在类中的函数指针(或 std::function)来调用正确的“点击时”处理程序,或者 (C) 让每个按钮都是不同的类型。

    我检查过的每个编译器都将虚函数实现为一个复杂的表,最终它是函数指针的集合。

    因此,您的选择是函数指针或生成 121 个完全不同的按钮,它们的行为几乎相同。

    在任何想要解耦调用者和被调用者的情况下,都必须使用类似于函数指针的东西。有很多可笑的情况,从工作队列到线程关闭任务、回调等等。

    在所有内容都经过硬编码的小型程序中,每次调用都可以进行硬编码。但是像这样的硬编码的东西无法扩展。当您想要更新每个手动实现的 121 个按钮时,了解它们的定制点将非常困难。而且它们将不同步。

    而 121 是一个适中的按钮数量。有 10,000 的应用程序呢?并且您想更新每个按钮的行为以处理基于触摸的输入?

    更重要的是,当您键入擦除时,您可以显着减小二进制大小。实现按钮的类的 121 个副本将占用比 1 个类更多的可执行空间,每个类都存储一个或两个函数指针。

    函数指针只是“类型擦除”的一种。类型擦除减少了二进制大小,在提供者和消费者之间提供了更清晰的合同,并且更容易重构围绕类型擦除数据的行为。

    【讨论】:

    • 类型擦除并不是说函数没有类型——因为它们不是没有类型(它存储在函数指针中)——而是它们应该替换的多态类,是现在可以从存在中删除的所有类型。这是您提供的一个很好的示例,但还有许多其他替换函数指针用作不充当类型擦除。
    【解决方案4】:

    如果没有函数指针,你将如何实现一个计算任何实值函数积分的函数?

    typedef double (*Function)(double);
    
    double Integral(Function f, double a, double b);
    

    【讨论】:

    • 我不确定你是否在这里提出了你的观点,当然也没有解决问题中描述的函数指针的具体用法。您已经介绍了一种额外的用途,这很好,但在他询问的特定情况下似乎不太可能让 OP 相信它们的实用性。
    • 我不知道你为什么把那个链接发给我,或者它与你的回答或我之前的评论有什么关系。我的观点很简单,这个问题相当广泛,但你的答案却要窄得多。此外,这是一个很好的例子,但也许不是一个完整的答案——也有点简洁——你已经用一个问题回答了一个问题,鉴于原始问题的性质,我怀疑 OP 甚至会理解你的答案。
    • @Clifford 在第一段中,发帖人质疑回调的使用。我提供了一个例子来说明它的必要性。我怀疑任何人都不清楚。
    【解决方案5】:

    1) 回调——同样可以通过直接调用来实现。

    并非在所有情况下,因为调用者在编译时可能不知道必须调用什么函数。例如,这在库中很常见,因为它们无法提前知道您的代码。

    但是,它也可能发生在您自己的代码中:每当您想部分重用某个函数时,您可以:

    • 创建该函数的多个版本,每个版本调用不同的函数。重复代码,非常糟糕的维护。除非受到代码膨胀的影响,否则性能良好。
    • 传递函数指针(或在 C++ 中通常可调用)。灵活、代码更少,在某些情况下性能可能会受到影响。
    • 创建一组分支(if/switch 链),如果您事先知道要调用的可能函数集。刚性,但可能比少量分支的函数指针更快。
    • 在 C++ 中,创建模板版本。与第一种情况相同,但自动化;这么好的维护。代码膨胀可能是个问题。
    • 分解出通用代码,以便调用者可以逐段调用他们需要的任何内容。有时这是不可能/容易的——特别是在参数化您希望保持可重用的复杂算法时(例如qsort())。在 C++ 中,请参阅 STL(标准模板库)。

    2) 异步或同步事件处理 - 无论如何都必须根据元素编号来识别事件。在函数指针数组中得到更新。但同样可以通过直接调用来完成。

    某些事件系统的设计目的是让您只需配置在给定事件发生时将触发哪些功能。如果这是一个带有 C 接口的外部库,他们别无选择,只能使用函数指针。

    其他一些系统允许您创建自己的事件循环,然后您可以通过某种方式获取事件并对它们做任何您想做的事情;所以他们避免回调。

    3) 在一些帖子中,我看到人们评论说它是在不知道调用哪个函数时使用的。我没有得到任何适当的理由。

    见第一种情况。

    【讨论】:

      【解决方案6】:

      感谢大家积极参与本次讨论。感谢您提供实际示例,例如 -

      1) 实现库函数

      2) 看qsort

      3) 参考 Linux 内核

      4) C 中的通用堆数据结构

      我觉得 qsort() void qsort(void *base, size_t nitems, size_t size, int (*compar)(const void *, const void*)) s 足以清除我的 1) & 3) 点。

      1) 回调——同样可以通过直接调用来实现。 3)在一些帖子中,我看到人们评论它是在不知道调用哪个函数时使用的。我没有得到任何适当的理由。

      主要通过回调 - 它是调用尚未定义主体的函数的规定。它预计函数的定义将在运行时稍后提供。因此,编译不会因为缺少函数定义而受到阻碍。如果有人考虑上述 qsort() 函数,则实际使用。在此,用户负责为 compare() 提供函数定义,例如 -

      int compare (int* a, int* b)
      {
        //User defined body based on problem requirement
      }
      

      让我们考虑一个实际场景,其中多个线程具有各自的比较功能。在直接调用的情况下,每个线程都需要实现自己的排序函数,或者如果是一个通用函数,那么实现会更加庞大。但是通过使用回调方法,所有线程都可以使用相同的函数进行排序,因为所有线程的排序算法都是相同的。 考虑到分层架构,主要是较高层具有较低层的抽象视图。因此,如果说我们在应用程序层实现了 qsort() 函数 [用户定义的 qsort],并且假设底层应用程序有一个 ADC 驱动程序层,它捕获样本并提供给应用程序进行排序。那么对于应用程序来说,没有必要了解负责收集和提供样本的功能的定义。但应用程序将只专注于获取样本。因此,该主应用程序将不知道要调用哪个函数。相应的 ADC 驱动程序将简单地使用 qsort() 调用应用程序并提供所需的数据。

      关于 2 点仍然很困惑 - 2) 异步或同步事件处理 - 无论如何都必须根据元素编号来识别事件。在函数指针数组中得到更新。但同样可以通过直接调用来完成。

      从上面的讨论我得出结论,如果事件处理程序指向某个库函数,那么它需要通过指向函数的指针来实现。其次,要创建独立且方便的代码,必须维护函数指针。假设在应用程序和驱动程序之间我们有一个接口层。因此,如果任何一个应用程序或驱动程序在任何时候发生变化,它都不会相互影响或至少相互影响。而这个接口层是使用函数指针来实现的。但请考虑以下情况 -

      int (*fptr[10]) (void) =
      {
          function1;     //function for starting LED
          function2;     //function for relay operation
            .
            .
          function10;    //function for motor control
      }
      

      假设我们有 GPIO0.0 - GPIO0.10 已映射到函数指针数组。即 GPIO0.0 - fptr 的第 0 个元素 . . GPIO0.10 - fptr 的第 10 个元素 这些 GPIO 引脚已配置为电平触发中断,它们各自的 ISR 将更新数组元素编号。 i=GPIO_Value; 调度程序还有一个线程将调用函数指针数组-

      fptr[i]();
      

      这里使用函数指针合理吗??

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2011-03-05
        • 1970-01-01
        • 2020-11-16
        • 2023-04-02
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-01-20
        相关资源
        最近更新 更多