使用功能指针的好处

时间:2009-03-20 18:38:19

标签: c# c++ c programming-languages function-pointers

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

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

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

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

11 个答案:

答案 0 :(得分:23)

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

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

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

一个简单的例子是通用排序功能。它必须有一些方法来比较两个元素,以确定它们应该如何排序。这可能是传递给sort函数的函数指针,实际上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中),你需要将一个函数传递给另一个函数。最常见的情况是回调。您可以定义希望系统在X发生时调用的函数F.因此,您创建一个指向F的函数指针,并将其传递给相关系统。

所以,真的,忘了约翰卡马克,不要以为你在他的代码中看到的任何东西都会神奇地使你的代码更好,如果你复制它。他使用了函数指针,因为你提到的游戏是用C编写的,其中没有优秀的替代品,而不是因为它们是一些神奇的成分,它们的存在使得代码运行得更快。

答案 1 :(得分: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;
}

当然还有其他用途,例如callback functions,从内存执行字节代码或创建解释语言。

在Windows上执行Intel compatible byte code,这可能对解释器有用。例如,这是一个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);

);

答案 2 :(得分:6)

无论何时在C#中使用事件处理程序或委托,您实际上都在使用函数指针。

不,他们不是速度。函数指针是关于方便的。

乔纳森

答案 3 :(得分:5)

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

那就是说,我将从我以前的一位教授那里得到一句话:

  

对待一个新的C ++功能就像你在一个拥挤的房间里对待一个装载的自动武器:永远不要使用它只是因为它看起来很漂亮。等到你明白后果,不要变得可爱,写下你所知道的,并知道你写的是什么。

答案 4 :(得分:5)

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

答案 5 :(得分:4)

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

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

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

答案 6 :(得分:3)

只是谈到C#,但是函数指针遍布C#。委托和事件(以及Lambdas等)都是功能指针,因此几乎任何C#项目都会充斥着函数指针。基本上每个LINQ查询附近的每个事件处理程序都将使用函数指针。

答案 7 :(得分:3)

功能指针是一个穷人试图发挥作用的尝试。您甚至可以创建一个参数,让函数指针使语言起作用,因为您可以使用它们编写更高阶的函数。

没有闭包和简单的语法,它们有点粗糙。所以你倾向于使用它们远远不如欲望。主要用于“回调”功能。

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

C#有闭包,所以函数指针(实际上存储一个对象所以它不仅仅是一个原始函数,而且也是类型状态)在那里可以使用得非常多。

修改 其中一条评论说应该用函数指针演示高阶函数。任何采用回调函数的函数都是高阶函数。比方说,EnumWindows

BOOL EnumWindows(          
    WNDENUMPROC lpEnumFunc,
    LPARAM lParam
);

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

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

答案 8 :(得分:3)

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

答案 9 :(得分:2)

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

考虑条件: {

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

答案 10 :(得分:1)

  
    

函数指针很快

  

在什么情况下?相比?

听起来你只是想使用函数指针才能使用它们。那会很糟糕。

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