15

我已经编程了几年,并且在某些情况下使用了函数指针。我想知道的是什么时候出于性能原因使用它们是合适的或不使用它们,我的意思是在游戏而不是商业软件的背景下。

函数指针很快,John Carmack 在 Quake 和 Doom 源代码中滥用了它们,因为他是个天才 :)

我想更多地使用函数指针,但我想在最合适的地方使用它们。

如今,在现代 c 风格语言(如 C、C++、C# 和 Java 等)中,函数指针的最佳和最实用的用途是什么?

4

11 回答 11

25

函数指针没有什么特别“快”的。它们允许您调用在运行时指定的函数。但是您的开销与从任何其他函数调用中获得的开销完全相同(加上额外的指针间接)。此外,由于要调用的函数是在运行时确定的,因此编译器通常不能像在其他任何地方那样内联函数调用。因此,在某些情况下,函数指针加起来可能比常规函数调用慢得多。

函数指针与性能无关,永远不应该用来获得性能。

相反,它们是对函数式编程范式的轻微认可,因为它们允许您将函数作为参数传递或在另一个函数中返回值。

一个简单的例子是一个通用的排序函数。它必须有某种方法来比较两个元素,以确定它们应该如何排序。这可能是传递给排序函数的函数指针,实际上 c++std::sort()可以完全这样使用。如果您要求它对未定义小于运算符的类型的序列进行排序,则必须传入一个函数指针,它可以调用它来执行比较。

这很好地引导我们找到一个更好的选择。在 C++ 中,您不仅限于函数指针。您经常使用函子代替 - 即重载 operator 的类(),以便它们可以像函数一样被“调用”。与函数指针相比​​,函子有几个很大的优势:

  • 它们提供了更大的灵活性:它们是成熟的类,具有构造函数、析构函数和成员变量。它们可以维护状态,并且可以公开周围代码可以调用的其他成员函数。
  • 它们更快:与函数指针不同,函数指针的类型只编码函数的签名(类型的变量void (*)(int)可以是任何接受 int 并返回 void 的函数。我们不知道是哪一个),函子的类型编码精确的函数那应该被调用(因为函子是一个类,称之为C,我们知道要调用的函数是,并且永远是,C::operator())。这意味着编译器可以内联函数调用。这就是使泛型std::sort与专为您的数据类型设计的手动编码排序函数一样快的魔力。编译器可以消除调用用户定义函数的所有开销。
  • 它们更安全:函数指针中几乎没有类型安全性。你不能保证它指向一个有效的函数。它可能是 NULL。指针的大多数问题也适用于函数指针。它们很危险且容易出错。

函数指针(在 C 中)或函子(在 C++ 中)或委托(在 C# 中)都解决了相同的问题,具有不同级别的优雅和灵活性:它们允许您将函数视为一等值,像您一样传递它们任何其他变量。您可以将一个函数传递给另一个函数,它会在指定的时间调用您的函数(当计时器到期时,当窗口需要重绘时,或者当它需要比较数组中的两个元素时)

据我所知(我可能是错的,因为我已经很久没有使用 Java 了),Java 没有直接的等价物。相反,您必须创建一个类,该类实现一个接口并定义一个函数(Execute()例如调用它)。然后不是调用用户提供的函数(以函数指针、仿函数或委托的形式),而是调用foo.Execute(). 原则上类似于 C++ 实现,但没有 C++ 模板的通用性,也没有允许您以相同方式处理函数指针和函子的函数语法。

这就是你使用函数指针的地方:当更复杂的替代方案不可用时(即你被困在C语言中),你需要将一个函数传递给另一个函数。最常见的场景是回调。您定义了一个函数 F,您希望系统在 X 发生时调用该函数。因此,您创建一个指向 F 的函数指针,并将其传递给相关系统。

所以说真的,忘记 John Carmack,不要假设你在他的代码中看到的任何东西都会神奇地让你的代码变得更好,如果你复制它。他使用函数指针是因为你提到的游戏是用 C 编写的,没有更好的替代品,而不是因为它们是某种神奇的成分,它的存在使代码运行得更快。

于 2009-03-20T19:48:22.883 回答
9

如果您直到运行时才知道目标平台支持的功能(例如 CPU 功能、可用内存),它们会很有用。显而易见的解决方案是编写如下函数:

int MyFunc()
{
  if(SomeFunctionalityCheck())
  {
    ...
  }
  else
  {
    ...
  }
}

如果在重要循环的深处调用此函数,那么最好为 MyFunc 使用函数指针:

int (*MyFunc)() = MyFunc_Default;

int MyFunc_SomeFunctionality()
{
  // if(SomeFunctionalityCheck())
  ..
}

int MyFunc_Default()
{
  // else
  ...
}

int MyFuncInit()
{
  if(SomeFunctionalityCheck()) MyFunc = MyFunc_SomeFunctionality;
}

当然还有其他用途,比如回调函数、从内存执行字节码或创建解释语言。

在 Windows 上执行Intel 兼容的字节码,这可能对解释器有用。例如,这是一个 stdcall 函数,返回 42 (0x2A) 存储在一个可以执行的数组中:

code = static_cast<unsigned char*>(VirtualAlloc(0, 6, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE));
// mov eax, 42
code[0] = 0x8b;
code[1] = 0x2a;
code[2] = 0x00;
code[3] = 0x00;
code[4] = 0x00;
// ret
code[5] = 0xc3;
// this line executes the code in the byte array
reinterpret_cast<unsigned int (_stdcall *)()>(code)();

...

VirtualFree(code, 6, MEM_RELEASE);

);

于 2009-03-20T18:55:00.310 回答
6

每当您在 C# 中使用事件处理程序或委托时,您实际上是在使用函数指针。

不,它们与速度无关。函数指针是为了方便。

乔纳森

于 2009-03-20T18:40:44.403 回答
6

在许多情况下,函数指针用作回调。一种用途是作为排序算法中的比较函数。因此,如果您尝试比较自定义对象,您可以提供一个指向比较函数的函数指针,该函数知道如何处理该数据。

也就是说,我将提供我从前任教授那里得到的一句话:

对待一个新的 C++ 特性,就像对待一个拥挤的房间里的自动武器一样:永远不要仅仅因为它看起来很漂亮而使用它。等你明白后果,别装可爱,写你知道的,知道你写的。

于 2009-03-20T18:41:20.640 回答
5

如今,在现代 c 风格语言中,整数的最佳和最实用的用途是什么?

于 2009-03-20T18:44:06.670 回答
5

根据我的个人经验,它们可以帮助您节省大量代码。

考虑条件:

switch(sample_var)
{

case 0:
          func1(<parameters>);
          break;

case 1:
          func2(<parameters>);
          break;



up to case n:
          funcn(<parameters>);
          break;

}

其中func1()...funcn()是具有相同原型的函数。我们可以做的是:声明一个arrFuncPoint包含函数地址的 func1()函数指针数组funcn()

然后整个开关盒将被替换为

*arrFuncPoint[sample_var];

于 2012-03-26T15:37:19.107 回答
4

在 C++ 之前的黑暗时代,我在代码中使用了一种常见模式,即定义一个带有一组函数指针的结构,这些函数指针(通常)以某种方式对该结构进行操作并为其提供特定的行为。在 C++ 术语中,我只是在构建一个 vtable。不同之处在于我可以在运行时对结构产生副作用,以根据需要动态更改单个对象的行为。这以稳定性和易于调试为代价提供了更丰富的继承模型。然而,最大的代价是只有一个人可以有效地编写这段代码:我。

我在 UI 框架中大量使用了它,它让我可以即时更改对象的绘制方式、命令的目标对象等等——这是很少有 UI 提供的。

用 OO 语言形式化这个过程在每一个有意义的方面都会更好。

于 2009-03-20T20:22:35.110 回答
3

只是说 C#,但函数指针在 C# 中使用。委托和事件(以及 Lambda 等)在底层都是函数指针,因此几乎所有 C# 项目都会充斥着函数指针。基本上每个事件处理程序,每个 LINQ 查询附近等 - 都将使用函数指针。

于 2009-03-20T18:41:35.837 回答
3

函数指针是穷人的功能性尝试。您甚至可以提出一个论点,即拥有函数指针会使语言功能化,因为您可以使用它们编写更高阶的函数。

没有闭包和简单的语法,它们有点恶心。所以你倾向于使用它们远远低于预期。主要用于“回调”功能。

有时,OO 设计通过创建一个完整的接口类型来传递所需的函数来解决使用函数的问题。

C# 有闭包,所以函数指针(它实际上存储一个对象,因此它不仅是一个原始函数,而且也是一个类型化的状态)在那里更有用。

编辑 其中一条评论说应该有一个带有函数指针的高阶函数的演示。任何采用回调函数的函数都是高阶函数。比如说,EnumWindows

BOOL EnumWindows(          
    WNDENUMPROC lpEnumFunc,
    LPARAM lParam
);

第一个参数是要传入的函数,很简单。但是由于 C 中没有闭包,我们得到了这个可爱的第二个参数:“指定要传递给回调函数的应用程序定义的值。” 该应用程序定义的值允许您手动传递无类型状态以补偿缺少闭包。

.NET 框架也充满了类似的设计。例如,IAsyncResult .AsyncState:“获取一个用户定义的对象,该对象限定或包含有关异步操作的信息。” 由于 IAR 是您在回调中获得的全部内容,没有闭包,您需要一种方法将一些数据推送到异步操作中,以便稍后将其丢弃。

于 2009-03-20T22:17:24.990 回答
3

有时使用函数指针可以加快处理速度。可以使用简单的调度表来代替长的 switch 语句或 if-then-else 序列。

于 2009-03-23T09:51:45.350 回答
1

函数指针很快

在什么情况下?相比?

听起来您只想使用函数指针来使用它们。那会很糟糕。

指向函数的指针通常用作回调或事件处理程序。

于 2009-03-20T18:43:49.497 回答