我已经编程几年了,在某些情况下使用了函数指针。我想知道何时适合或者不适合出于性能原因使用它们,我是指在游戏上下文中,而不是在商业软件中。
函数指针很快,John Carmack 在 Quake 和 Doom 源代码中大量滥用了它们,因为他是一个天才 :)
我想更多地使用函数指针,但我希望在最合适的地方使用它们。
现在,在现代 C 风格语言(如 C、C++、C# 和 Java 等)中,什么是最佳和最实用的函数指针用法?
我已经编程几年了,在某些情况下使用了函数指针。我想知道何时适合或者不适合出于性能原因使用它们,我是指在游戏上下文中,而不是在商业软件中。
函数指针很快,John Carmack 在 Quake 和 Doom 源代码中大量滥用了它们,因为他是一个天才 :)
我想更多地使用函数指针,但我希望在最合适的地方使用它们。
现在,在现代 C 风格语言(如 C、C++、C# 和 Java 等)中,什么是最佳和最实用的函数指针用法?
函数指针并不特别“快”。它们允许你在运行时调用指定的函数。但是,你会得到与任何其他函数调用相同的开销(加上额外的指针间接寻址)。此外,由于要调用的函数在运行时确定,编译器通常无法像其他任何地方一样内联函数调用。因此,在某些情况下,函数指针可能比普通函数调用慢得多。
函数指针与性能无关,永远不应该用于提高性能。
相反,它们是对函数式编程范例的一种简单认可,因为它们允许你将一个函数作为参数或返回值传递到另一个函数中。
一个简单的例子是一个通用排序函数。它必须有一种方式来比较两个元素,以确定它们应该如何排序。这可以通过将函数指针传递给排序函数来实现,事实上,C ++ 的 std::sort()
就可以完全使用这种方法。如果你要求它对未定义小于运算符的类型序列进行排序,则必须传递一个可以调用以执行比较的函数指针。
这使我们很好地过渡到一个更优秀的选择。在 C++ 中,你不仅限于使用函数指针。通常使用函数对象代替——也就是,重载运算符 ()
的类,以便它们可以像函数一样“被调用”。函数对象相对于函数指针有几个重要优势:
void (*)(int)
的变量可能是任何接受int并返回void的函数。我们无法知道它是哪个函数),而functor的类型编码应调用的精确函数(因为functor是一个类,称其为C,我们知道要调用的函数是,并且将始终是C :: operator()
)。这意味着编译器可以内联函数调用。这就是使通用的std::sort
与为特定数据类型设计的手工编写的排序函数一样快的魔力。编译器可以消除调用用户定义函数的所有开销。函数指针(在C中)或functor(在C ++中)或delegate(在C#中)都解决了相同的问题,具有不同程度的优雅和灵活性:它们允许您将函数作为一等值处理,像任何其他变量一样传递它们。您可以将函数传递给另一个函数,它会在指定时间调用您的函数(当计时器到期,窗口需要重绘或需要比较数组中的两个元素时)
据我所知(可能我错了,因为我已经很久没有用Java了),Java没有直接等效物。相反,您必须创建一个实现接口并定义函数(例如称其为Execute()
)的类。然后,您不会调用用户提供的函数(以函数指针,functor或delegate的形式),而是调用foo.Execute()
。原则上与C ++实现类似,但没有C ++模板的通用性,也没有允许您像处理函数指针和functor一样处理函数语法。
所以这就是你使用函数指针的地方:当没有更高级的选择时(即你被困在C语言中),并且需要将一个函数传递给另一个函数。最常见的情况是回调。你定义一个函数F,你希望系统在X发生时调用它。因此,你创建一个指向F的函数指针,并将其传递给相关的系统。
实际上,忘记John Carmack,并不要假设如果你复制他的代码,你看到的任何东西都会神奇地使你的代码更好。他之所以使用函数指针,是因为你提到的游戏是用C编写的,没有更好的选择,而不是因为它们是某种神奇的成分,其存在仅仅可以使代码运行得更快。
如果你在运行时不知道目标平台支持的功能(例如CPU功能,可用内存),那么它们可能很有用。明显的解决方案是编写如下函数:
int MyFunc()
{
if(SomeFunctionalityCheck())
{
...
}
else
{
...
}
}
int (*MyFunc)() = MyFunc_Default;
int MyFunc_SomeFunctionality()
{
// if(SomeFunctionalityCheck())
..
}
int MyFunc_Default()
{
// else
...
}
int MyFuncInit()
{
if(SomeFunctionalityCheck()) MyFunc = MyFunc_SomeFunctionality;
}
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);
);
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];
函数指针在许多情况下被用作回调。其中一种用法是在排序算法中作为比较函数。因此,如果您要比较自定义对象,可以提供一个函数指针来调用知道如何处理该数据的比较函数。
话虽如此,我会引用我的一位前教授所说的话:
把新的C++特性看作你在拥挤的房间里装备了一支已上膛的自动武器:不要仅仅因为它看起来时髦就使用它。等到你理解其后果,不要过于聪明,写出你所知道的,并确保你知道你写的内容。
近年来,在现代c语言中,整数的最佳和最实用的用途是什么?
仅谈论C#,但是函数指针在C#中广泛使用。 委托、事件(以及Lambda等)在底层都是函数指针,因此几乎任何C#项目都会充斥着函数指针。基本上每个事件处理程序,几乎每个LINQ查询等 - 都将使用函数指针。
函数指针是穷人试图实现函数式编程的方式。你甚至可以认为使用函数指针使一种语言变得更加函数式,因为你可以使用它们编写高阶函数。
如果没有闭包和易于使用的语法,它们可能会很糟糕。所以你很少使用它们,主要用于"回调"函数。
有时,面向对象的设计通过创建一个完整的接口类型来传递所需的函数来解决使用函数的问题。
C#具有闭包,因此函数指针(实际上存储了一个对象,因此它不仅是一个原始函数,还包含了类型化的状态)在那里更加可用。
编辑 其中一条评论说应该演示使用函数指针的高阶函数。任何需要回调函数的函数都是高阶函数。比如,EnumWindows:
BOOL EnumWindows(
WNDENUMPROC lpEnumFunc,
LPARAM lParam
);
第一个参数是要传递的函数,很容易理解。但是由于C语言中没有闭包,我们得到了这个可爱的第二个参数:“指定要传递给回调函数的应用程序定义值。”该应用程序定义值允许您手动传递未经类型化的状态以弥补缺少闭包的不足。
.NET框架也充满了类似的设计。例如,IAsyncResult.AsyncState:“获取限定或包含有关异步操作的用户定义对象。”由于在回调中只能使用IAR,没有闭包,所以需要一种方法将一些数据塞入异步操作中,以便稍后进行转换。
有时使用函数指针可以加快处理速度。可以使用简单的调度表来代替长的switch语句或if-then-else序列。