提问人:gramm 提问时间:4/7/2010 最后编辑:Nategramm 更新时间:8/11/2022 访问量:18221
函数指针的意义何在?
What is the point of function pointers?
问:
我很难看到函数指针的效用。我想它在某些情况下可能有用(毕竟它们存在),但我想不出使用函数指针更好或不可避免的情况。
您能否举例说明如何使用函数指针(在 C 或 C++ 中)?
答:
例子:
- 自定义排序/搜索
- 不同 模式(如策略、观察者)
- 回调
评论
std::sort
的 comp
参数描述为策略
函数指针可以在 C 语言中使用来创建要编程的接口。根据运行时所需的特定功能,可以为函数指针分配不同的实现。
在 C 语言中,经典用法是 qsort 函数,其中第四个参数是指向用于在排序中执行排序的函数的指针。在 C++ 中,人们倾向于使用函子(看起来像函数的对象)来做这种事情。
评论
函数指针有用的“经典”示例是 C 库函数,它实现了快速排序。为了对用户可能想到的任何和所有数据结构具有通用性,它需要几个指向可排序数据的空指针和一个指向知道如何比较这些数据结构的两个元素的函数的指针。这允许我们为作业创建我们选择的函数,实际上甚至允许在运行时选择比较函数,例如用于升序或降序排序。qsort()
大多数示例归结为回调:您调用一个函数传递另一个函数的地址,并调用某个特定任务。如果传递 的地址,则将改为回调。f()
g()
f()
g()
f()
h()
f()
h()
基本上,这是一种参数化函数的方法:它的某些行为部分不是硬编码到 ,而是硬编码到 回调函数中。调用方可以通过传递不同的回调函数来使行为不同。经典来自 C 标准库,它将其排序标准作为指向比较函数的指针。f()
f()
qsort()
在 C++ 中,这通常使用函数对象(也称为函子)来完成。这些是重载函数调用运算符的对象,因此您可以像调用函数一样调用它们。例:
class functor {
public:
void operator()(int i) {std::cout << "the answer is: " << i << '\n';}
};
functor f;
f(42);
这背后的想法是,与函数指针不同,函数对象不仅可以携带算法,还可以携带数据:
class functor {
public:
functor(const std::string& prompt) : prompt_(prompt) {}
void operator()(int i) {std::cout << prompt_ << i << '\n';}
private:
std::string prompt_;
};
functor f("the answer is: ");
f(42);
另一个优点是,有时对函数对象的内联调用比通过函数指针调用更容易。这就是为什么 C++ 中的排序有时比 C 中的排序更快的原因。
评论
同意以上所有内容,另外...... 在运行时动态加载 dll 时,需要函数指针来调用函数。
评论
我要在这里逆流而上。
在 C 语言中,函数指针是实现自定义的唯一方法,因为没有 OO。
在 C++ 中,可以使用函数指针或函子(函数对象)来获得相同的结果。
由于其对象性质,函子与原始函数指针相比具有许多优点,特别是:
- 它们可能会出现
operator()
- 它们可以具有对现有变量的状态/引用
- 它们可以在现场构建(和
lambda
bind
)
我个人更喜欢函子而不是函数指针(尽管有样板代码),主要是因为函数指针的语法很容易变得毛茸茸的(来自函数指针教程):
typedef float(*pt2Func)(float, float);
// defines a symbol pt2Func, pointer to a (float, float) -> float function
typedef int (TMyClass::*pt2Member)(float, char, char);
// defines a symbol pt2Member, pointer to a (float, char, char) -> int function
// belonging to the class TMyClass
我唯一一次看到函数指针在函子无法使用的地方是在 Boost.Spirit 中。他们完全滥用了语法,将任意数量的参数作为单个模板参数传递。
typedef SpecialClass<float(float,float)> class_type;
但是由于可变参数模板和 lambda 指日可待,我不确定我们是否会在纯 C++ 代码中使用函数指针很长时间。
评论
bind
function
好吧,我通常在跳转表中(专业地)使用它们(另请参阅此 StackOverflow 问题)。
跳转表通常(但不限于)用于有限状态机,以使其成为数据驱动。代替嵌套的开关/外壳
switch (state)
case A:
switch (event):
case e1: ....
case e2: ....
case B:
switch (event):
case e3: ....
case e1: ....
您可以创建函数指针的 2D 数组,然后调用handleEvent[state][event]
我最近使用函数指针来创建一个抽象层。
我有一个用纯 C 编写的程序,可以在嵌入式系统上运行。它支持多种硬件变体。根据我运行的硬件,它需要调用某些函数的不同版本。
在初始化时,程序会确定它运行在什么硬件上,并填充函数指针。程序中的所有高级例程都只调用指针引用的函数。我可以添加对新硬件变体的支持,而无需触及更高级别的例程。
我曾经使用 switch/case 语句来选择正确的函数版本,但随着程序发展到支持越来越多的硬件变体,这变得不切实际。我不得不到处添加案例陈述。
我还尝试了中间函数层来确定要使用哪个函数,但它们没有多大帮助。每当我们添加新变体时,我仍然必须在多个地方更新案例陈述。使用函数指针,我只需要更改初始化函数。
我广泛使用函数指针来模拟具有 1 字节操作码的微处理器。包含 256 个函数指针的数组是实现此目的的自然方法。
我对它们的主要用途是 CALLBACKS:当您需要保存有关函数的信息以供以后调用时。
假设你正在写炸弹人。人投掷炸弹 5 秒后,它应该爆炸(调用函数)。explode()
现在有 2 种方法可以做到这一点。一种方法是“探测”屏幕上的所有炸弹,看看它们是否准备好在主循环中爆炸。
foreach bomb in game
if bomb.boomtime()
bomb.explode()
另一种方法是将回调附加到时钟系统。当炸弹被放置时,你添加一个回调,让它在时机成熟时调用 bomb.explode()。
// user placed a bomb
Bomb* bomb = new Bomb()
make callback( function=bomb.explode, time=5 seconds ) ;
// IN the main loop:
foreach callback in callbacks
if callback.timeToRun
callback.function()
这里可以是任何函数,因为它是一个函数指针。callback.function()
评论
函数指针的一种用法可能是我们可能不想修改调用函数的代码(这意味着调用可能是有条件的,在不同的条件下,我们需要做不同类型的处理)。 这里的函数指针非常方便,因为我们不需要在调用函数的地方修改代码。我们只需使用带有适当参数的函数指针调用函数即可。 函数指针可以有条件地指向不同的函数。(这可以在初始化阶段的某个位置完成)。此外,如果我们无法修改调用它的代码(假设它是一个我们无法修改的库 API),上面的模型非常有用。API 使用函数指针来调用相应的用户定义函数。
就像 Rich 上面说的,Windows 中的函数指针引用存储函数的某个地址是很常见的。
当您在Windows平台上编程时,您基本上会在主存储器中加载一些DLL文件(使用),并且要使用存储在DLL中的函数,您需要创建函数指针并指向这些地址(使用)。C language
LoadLibrary
GetProcAddress
引用:
对于面向对象语言,在后台执行多态调用(我想这在某种程度上也适用于 C)。
此外,它们在运行时将不同的行为注入到另一个函数 (foo) 非常有用。这使得函数成为高阶函数。除了它的灵活性之外,它还使 foo 代码更具可读性,因为它可以让您从中提取“if-else”的额外逻辑。
它在 Python 中启用了许多其他有用的东西,如生成器、闭包等。
除了其他好的答案之外,还有一个不同的观点:
在 C 语言中,您只使用函数指针,而不(直接)使用函数。
我的意思是,你写了函数,但你不能操作函数。没有可以使用的函数的运行时表示形式。你甚至不能调用“函数”。当你写:
my_function(my_arg);
您实际上说的是“使用指定的参数执行对指针的调用”。您正在通过函数指针进行调用。这种衰减到函数指针意味着以下命令等效于前面的函数调用:my_function
(&my_function)(my_arg);
(*my_function)(my_arg);
(**my_function)(my_arg);
(&**my_function)(my_arg);
(***my_function)(my_arg);
依此类推(感谢@LuuVinhPhuc)。
因此,您已经在使用函数指针作为值。显然,你会希望为这些值设置变量 - 这是其他metion的所有用途的来源:多态性/自定义(如qsort),回调,跳转表等。
在 C++ 中,事情有点复杂,因为我们有 lambda 和带有 的对象,甚至还有一个类,但原理仍然基本相同。operator()
std::function
评论
(&my_function)(my_arg)
(*my_function)(my_arg)
(**my_function)(my_arg)
(&**my_function)(my_arg)
(***my_function)(my_arg)
使用函数指针
根据用户输入动态调用函数。 在本例中,通过创建字符串和函数指针的映射。
#include<iostream>
#include<map>
using namespace std;
//typedef map<string, int (*)(int x, int y) > funMap;
#define funMap map<string, int (*)(int, int)>
funMap objFunMap;
int Add(int x, int y)
{
return x+y;
}
int Sub(int x, int y)
{
return x-y;
}
int Multi(int x, int y)
{
return x*y;
}
void initializeFunc()
{
objFunMap["Add"]=Add;
objFunMap["Sub"]=Sub;
objFunMap["Multi"]=Multi;
}
int main()
{
initializeFunc();
while(1)
{
string func;
cout<<"Enter your choice( 1. Add 2. Sub 3. Multi) : ";
int no, a, b;
cin>>no;
if(no==1)
func = "Add";
else if(no==2)
func = "Sub";
else if(no==3)
func = "Multi";
else
break;
cout<<"\nEnter 2 no :";
cin>>a>>b;
//function is called using function pointer based on user input
//If user input is 2, and a=10, b=3 then below line will expand as "objFuncMap["Sub"](10, 3)"
int ret = objFunMap[func](a, b);
cout<<ret<<endl;
}
return 0;
}
这样,我们在实际的公司代码中使用了函数指针。 你可以写'n'个函数,并用这个方法调用它们。
输出:
Enter your choice( 1. Add 2. Sub 3. Multi) : 1 Enter 2 no :2 4 6 Enter your choice( 1. Add 2. Sub 3. Multi) : 2 Enter 2 no : 10 3 7 Enter your choice( 1. Add 2. Sub 3. Multi) : 3 Enter 2 no : 3 6 18
我将尝试在这里给出一个更全面的列表:
回调:使用用户提供的代码自定义某些(库)功能。典型的例子是 ,但对于处理事件(例如单击时调用回调的按钮)或启动线程 () 所必需的事件也很有用。
qsort()
pthread_create()
多态性:C++ 类中的 vtable 只不过是函数指针表。C 程序也可以选择为其某些对象提供 vtable:
struct Base; struct Base_vtable { void (*destruct)(struct Base* me); }; struct Base { struct Base_vtable* vtable; }; struct Derived; struct Derived_vtable { struct Base_vtable; void (*frobnicate)(struct Derived* me); }; struct Derived { struct Base; int bar, baz; }
然后,构造函数将其成员变量设置为一个全局对象,该对象具有派生类的 和 的实现,并且需要构造 的代码只需调用 ,这将调用析构函数的正确版本,而与派生类实际指向的无关。
Derived
vtable
destruct
frobnicate
struct Base*
base->vtable->destruct(base)
base
如果没有函数指针,就需要用大量开关结构来编码多态性,例如
switch(me->type) { case TYPE_BASE: base_implementation(); break; case TYPE_DERIVED1: derived1_implementation(); break; case TYPE_DERIVED2: derived2_implementation(); break; case TYPE_DERIVED3: derived3_implementation(); break; }
这很快就会变得相当笨拙。
动态加载的代码:当程序将模块加载到内存中并尝试调用其代码时,它必须通过函数指针。
我所看到的函数指针的所有用法都完全属于这三大类之一。
它们增强了代码的重用和模块化,从而使代码更易于维护、可读且不易出错。
使用函数指针:
请注意,我们有一个方法,该方法被传递给一个函数指针。这个函数指针告诉我们应该如何处理列表中的每个元素。iterator
#include <iostream>
#include <vector>
int square(int x) {
return x * x;
}
int root(int x) {
return sqrt(x);
}
int negative(int x) {
return -x;
}
std::vector<int> listIterator(std::vector<int> list, int (*itemOperation)(int)) {
for (int i = 0; i < list.size(); i++) {
list[i] = itemOperation(list[i]);
}
return list;
}
int main() {
std::vector<int> list = { 9, 16, 4, 25 };
for (int i : listIterator(list, square)) {
std::cout << i << ' ';
}
std::cout << std::endl;
for (int i : listIterator(list, root)) {
std::cout << i << ' ';
}
std::cout << std::endl;
for (int i : listIterator(list, negative)) {
std::cout << i << ' ';
}
return 0;
}
不带函数指针:
如果没有函数指针,则需要在每个 和 方法中包含一个迭代器。square
root
negative
下一个:我应该如何将对象传递给函数?
评论