使用函数指针的好处

16

我已经编程几年了,在某些情况下使用了函数指针。我想知道何时适合或者不适合出于性能原因使用它们,我是指在游戏上下文中,而不是在商业软件中。

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

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

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


用JavaScript编写一些函数式的代码,你会爱上它的。这里有一个小例子:我有一个监视设置文件更改的类。当你创建这个类时,你需要传递一个委托来调用每次文件更改时的操作。 - core
11个回答

27

函数指针并不特别“快”。它们允许你在运行时调用指定的函数。但是,你会得到与任何其他函数调用相同的开销(加上额外的指针间接寻址)。此外,由于要调用的函数在运行时确定,编译器通常无法像其他任何地方一样内联函数调用。因此,在某些情况下,函数指针可能比普通函数调用慢得多。

函数指针与性能无关,永远不应该用于提高性能。

相反,它们是对函数式编程范例的一种简单认可,因为它们允许你将一个函数作为参数或返回值传递到另一个函数中。

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

这使我们很好地过渡到一个更优秀的选择。在 C++ 中,你不仅限于使用函数指针。通常使用函数对象代替——也就是,重载运算符 () 的类,以便它们可以像函数一样“被调用”。函数对象相对于函数指针有几个重要优势:

  • 它们提供更多的灵活性:它们是完整的类,具有构造函数、析构函数和成员变量。它们可以维护状态,并且可以公开其他代码可以调用的成员函数。
  • 它们更快: 与函数指针不同,函数指针类型仅编码函数签名(类型为void (*)(int)的变量可能是任何接受int并返回void的函数。我们无法知道它是哪个函数),而functor的类型编码应调用的精确函数(因为functor是一个类,称其为C,我们知道要调用的函数是,并且将始终是C :: operator())。这意味着编译器可以内联函数调用。这就是使通用的std::sort与为特定数据类型设计的手工编写的排序函数一样快的魔力。编译器可以消除调用用户定义函数的所有开销。
  • 它们更安全: 函数指针的类型安全性很低。您无法保证它指向有效的函数。它也可能为NULL。大部分指针问题也适用于函数指针。它们是危险和容易出错的。
  • 函数指针(在C中)或functor(在C ++中)或delegate(在C#中)都解决了相同的问题,具有不同程度的优雅和灵活性:它们允许您将函数作为一等值处理,像任何其他变量一样传递它们。您可以将函数传递给另一个函数,它会在指定时间调用您的函数(当计时器到期,窗口需要重绘或需要比较数组中的两个元素时)

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

    所以这就是你使用函数指针的地方:当没有更高级的选择时(即你被困在C语言中),并且需要将一个函数传递给另一个函数。最常见的情况是回调。你定义一个函数F,你希望系统在X发生时调用它。因此,你创建一个指向F的函数指针,并将其传递给相关的系统。

    实际上,忘记John Carmack,并不要假设如果你复制他的代码,你看到的任何东西都会神奇地使你的代码更好。他之所以使用函数指针,是因为你提到的游戏是用C编写的,没有更好的选择,而不是因为它们是某种神奇的成分,其存在仅仅可以使代码运行得更快。


1
函数指针与性能无关,不应该被用于提高性能。实际上,它们可以用于提高性能,同样也可以使用functor/delegate等方式。请看我回答中的例子...为什么这样做是不好的?我不明白其中的问题所在。 - jheriko
1
没错。我的观点只是简单地指出,仅仅调用函数指针而不是直接调用函数并不能提高速度,因为这似乎是OP所暗示的。如果你用它来改变程序的逻辑,那么它也会影响性能。 - jalf
在我的回答中获得了一些声望提升之后,重新阅读让我思考,实际上性能取决于函数调用机制,有时会有很大的差异 - 例如,在C++中,虚函数调用和虚继承会添加额外的间接层,甚至可能污染整个缓存。尽管如此,这是许多其他原因导致的糟糕代码...函数指针可以实现相同的功能(复杂继承),具有额外的内存开销,但仅保留动态调用,例如仅命中指令和数据缓存一次,而不是2..n次。 - jheriko
@jheriko 当然,不同的调用机制具有不同的性能特征。我在我的回答中所说的意思只是函数指针并不能使函数更快,因为OP似乎认为它可以。相反,额外的间接引用和相关的缓存污染使其比直接调用更慢。 - jalf
你本可以为你的类名选用不同的字母。只是说说而已 ;) - Juan Campa

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);

);


应该使用策略模式或其他类似的装饰者设计模式。当然,如果我们谈论C ++的话。 - Mykola Golubyev

7
根据我的个人经验,它们可以帮助您节省大量的代码行。
考虑下面的情况:
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()的地址。

然后,整个 switch case 将被替换为:

*arrFuncPoint[sample_var];


非常好的解释和简洁的用法。我会使用这个快捷方式。谢谢。 - cyberspider789

6
任何时候在C#中使用事件处理程序或委托,实际上都是在使用函数指针。
而且,它们并不是关于速度的。函数指针是为了方便。
Jonathan

6

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

话虽如此,我会引用我的一位前教授所说的话:

把新的C++特性看作你在拥挤的房间里装备了一支已上膛的自动武器:不要仅仅因为它看起来时髦就使用它。等到你理解其后果,不要过于聪明,写出你所知道的,并确保你知道你写的内容。


哈哈,太对了!这让我想起了http://www.gotw.ca/publications/advice97.htm :) - Johannes Schaub - litb
这正是他所引用的内容!太棒了。 - Kyle Walsh
如果你独自完成项目,“写下你所知道的”是起作用的。但是,如果你擅长数组而不熟悉STL,那么在一个团队中工作就不太好了。对于在大项目中周围的纯C函数等类似事情也是如此。 - Mykola Golubyev

5

近年来,在现代c语言中,整数的最佳和最实用的用途是什么?


我很抱歉,但我不明白您的意思。 - Brock Woolf
如果你需要一个整数,就使用整数。如果你需要一个函数指针,就使用函数指针。这两者(像所有编程结构一样)只是工具,没有任何特别快或神奇的地方。 - anon
我个人很喜欢你的讽刺,先生。 - Marcin
1
给自己一个轻拍背部的鼓励 高尔夫掌声 - Brock Woolf

4
在C++出现之前的那个模糊的黑暗时代,我在我的代码中使用了一种常见的模式,即定义一个带有一组函数指针的结构体,这些函数指针(通常)以某种方式操作该结构体并为其提供特定的行为。就C++而言,我只是在构建一个虚函数表。不同之处在于,我可以在运行时对结构体进行副作用以根据需要动态更改单个对象的行为。这提供了一种更丰富的继承模型,但稳定性和调试的易用性却受到了影响。然而,最大的代价是只有我能够有效地编写此代码。
我在一个UI框架中广泛使用它,该框架让我可以在飞行中更改对象的绘制方式、命令的目标等等——这是很少有UI提供的功能。
在面向对象的语言中将这个过程正式化,每个方面都更好。

没错。我已经使用了几种语言中的代码引用来模拟面向对象编程。顺便说一下:并不是所有的语言都“关闭”类。Ruby、Javascript和Objective C让你像你所说的那样在运行时更改实现。 - Roboprog

3

仅谈论C#,但是函数指针在C#中广泛使用。 委托、事件(以及Lambda等)在底层都是函数指针,因此几乎任何C#项目都会充斥着函数指针。基本上每个事件处理程序,几乎每个LINQ查询等 - 都将使用函数指针。


3

函数指针是穷人试图实现函数式编程的方式。你甚至可以认为使用函数指针使一种语言变得更加函数式,因为你可以使用它们编写高阶函数。

如果没有闭包和易于使用的语法,它们可能会很糟糕。所以你很少使用它们,主要用于"回调"函数。

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

C#具有闭包,因此函数指针(实际上存储了一个对象,因此它不仅是一个原始函数,还包含了类型化的状态)在那里更加可用。

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

BOOL EnumWindows(          
    WNDENUMPROC lpEnumFunc,
    LPARAM lParam
);

第一个参数是要传递的函数,很容易理解。但是由于C语言中没有闭包,我们得到了这个可爱的第二个参数:“指定要传递给回调函数的应用程序定义值。”该应用程序定义值允许您手动传递未经类型化的状态以弥补缺少闭包的不足。

.NET框架也充满了类似的设计。例如,IAsyncResult.AsyncState:“获取限定或包含有关异步操作的用户定义对象。”由于在回调中只能使用IAR,没有闭包,所以需要一种方法将一些数据塞入异步操作中,以便稍后进行转换。


我很好奇为什么有人投票将这个回答降到了-1。看起来是一个正确(并且经过深思熟虑的)答案。那么我在这里加上+1以进行补偿。 - jalf
我没有点踩,不过也许他应该演示如何使用fps构建高阶函数? - anon

3

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


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