我最近在C语言中有一些函数指针的经验。
因此,按照回答自己问题的传统,我决定对最基本的问题做一个简短的总结,以供那些需要快速切入主题的人使用。
我最近在C语言中有一些函数指针的经验。
因此,按照回答自己问题的传统,我决定对最基本的问题做一个简短的总结,以供那些需要快速切入主题的人使用。
当前回答
函数指针是包含函数地址的变量。由于它是一个指针变量,虽然有一些限制性的财产,您可以像使用数据结构中的任何其他指针变量一样使用它。
我能想到的唯一例外是将函数指针视为指向某个值以外的其他值。通过递增或递减函数指针或向函数指针添加/减去偏移量来执行指针运算实际上没有任何用处,因为函数指针只指向一个对象,即函数的入口点。
函数指针变量的大小、该变量所占用的字节数可能会根据基础架构(例如x32或x64或其他)而变化。
函数指针变量的声明需要指定与函数声明相同类型的信息,以便C编译器执行通常执行的检查。如果没有在函数指针的声明/定义中指定参数列表,C编译器将无法检查参数的使用。在某些情况下,这种缺乏检查可能是有用的,但请记住,安全网已被移除。
一些示例:
int func (int a, char *pStr); // declares a function
int (*pFunc)(int a, char *pStr); // declares or defines a function pointer
int (*pFunc2) (); // declares or defines a function pointer, no parameter list specified.
int (*pFunc3) (void); // declares or defines a function pointer, no arguments.
前两个声明在以下方面有些相似:
func是一个接受int和char*并返回int的函数pFunc是一个函数指针,它被分配了一个函数的地址,该函数接受一个int和一个char*并返回一个int
因此,从上面我们可以得到一个源行,其中函数func()的地址被分配给函数指针变量pFunc,如pFunc=func;中所示;。
注意与函数指针声明/定义一起使用的语法,其中括号用于克服自然运算符优先规则。
int *pfunc(int a, char *pStr); // declares a function that returns int pointer
int (*pFunc)(int a, char *pStr); // declares a function pointer that returns an int
几个不同的用法示例
函数指针用法的一些示例:
int (*pFunc) (int a, char *pStr); // declare a simple function pointer variable
int (*pFunc[55])(int a, char *pStr); // declare an array of 55 function pointers
int (**pFunc)(int a, char *pStr); // declare a pointer to a function pointer variable
struct { // declare a struct that contains a function pointer
int x22;
int (*pFunc)(int a, char *pStr);
} thing = {0, func}; // assign values to the struct variable
char * xF (int x, int (*p)(int a, char *pStr)); // declare a function that has a function pointer as an argument
char * (*pxF) (int x, int (*p)(int a, char *pStr)); // declare a function pointer that points to a function that has a function pointer as an argument
可以在函数指针的定义中使用可变长度参数列表。
int sum (int a, int b, ...);
int (*psum)(int a, int b, ...);
或者根本不能指定参数列表。这可能很有用,但它消除了C编译器对提供的参数列表执行检查的机会。
int sum (); // nothing specified in the argument list so could be anything or nothing
int (*psum)();
int sum2(void); // void specified in the argument list so no parameters when calling this function
int (*psum2)(void);
C型脚轮
可以将C样式的强制转换与函数指针一起使用。但是要注意,C编译器可能对检查不严格,或者提供警告而不是错误。
int sum (int a, char *b);
int (*psplsum) (int a, int b);
psplsum = sum; // generates a compiler warning
psplsum = (int (*)(int a, int b)) sum; // no compiler warning, cast to function pointer
psplsum = (int *(int a, int b)) sum; // compiler error of bad cast generated, parenthesis are required.
将函数指针与相等值进行比较
您可以使用if语句检查函数指针是否等于特定的函数地址,尽管我不确定这有多有用。其他比较运算符的实用性似乎更低。
static int func1(int a, int b) {
return a + b;
}
static int func2(int a, int b, char *c) {
return c[0] + a + b;
}
static int func3(int a, int b, char *x) {
return a + b;
}
static char *func4(int a, int b, char *c, int (*p)())
{
if (p == func1) {
p(a, b);
}
else if (p == func2) {
p(a, b, c); // warning C4047: '==': 'int (__cdecl *)()' differs in levels of indirection from 'char *(__cdecl *)(int,int,char *)'
} else if (p == func3) {
p(a, b, c);
}
return c;
}
函数指针数组
如果你想有一个函数指针数组,其中每个元素的参数列表都有不同,那么你可以定义一个未指定参数列表的函数指针(不是void,这意味着没有参数,只是未指定),就像下面这样,尽管你可能会看到来自C编译器的警告。这也适用于指向函数的函数指针参数:
int(*p[])() = { // an array of function pointers
func1, func2, func3
};
int(**pp)(); // a pointer to a function pointer
p[0](a, b);
p[1](a, b, 0);
p[2](a, b); // oops, left off the last argument but it compiles anyway.
func4(a, b, 0, func1);
func4(a, b, 0, func2); // warning C4047: 'function': 'int (__cdecl *)()' differs in levels of indirection from 'char *(__cdecl *)(int,int,char *)'
func4(a, b, 0, func3);
// iterate over the array elements using an array index
for (i = 0; i < sizeof(p) / sizeof(p[0]); i++) {
func4(a, b, 0, p[i]);
}
// iterate over the array elements using a pointer
for (pp = p; pp < p + sizeof(p)/sizeof(p[0]); pp++) {
(*pp)(a, b, 0); // pointer to a function pointer so must dereference it.
func4(a, b, 0, *pp); // pointer to a function pointer so must dereference it.
}
使用带有函数指针的全局结构的C样式命名空间
您可以使用static关键字指定一个名称为file scope的函数,然后将其分配给一个全局变量,以提供类似于C++的命名空间功能的功能。
在头文件中定义一个结构,该结构将是我们的命名空间以及使用它的全局变量。
typedef struct {
int (*func1) (int a, int b); // pointer to function that returns an int
char *(*func2) (int a, int b, char *c); // pointer to function that returns a pointer
} FuncThings;
extern const FuncThings FuncThingsGlobal;
然后在C源文件中:
#include "header.h"
// the function names used with these static functions do not need to be the
// same as the struct member names. It's just helpful if they are when trying
// to search for them.
// the static keyword ensures these names are file scope only and not visible
// outside of the file.
static int func1 (int a, int b)
{
return a + b;
}
static char *func2 (int a, int b, char *c)
{
c[0] = a % 100; c[1] = b % 50;
return c;
}
const FuncThings FuncThingsGlobal = {func1, func2};
然后,通过指定全局结构变量的完整名称和访问函数的成员名称,可以使用该函数。常量修饰符用于全局,因此不会意外更改。
int abcd = FuncThingsGlobal.func1 (a, b);
功能指针的应用领域
DLL库组件可以做一些类似于C风格的命名空间方法的事情,在这种方法中,特定的库接口是从支持创建包含函数指针的结构的库接口中的工厂方法请求的。。此库接口加载请求的DLL版本,创建一个具有必要函数指针的结构,然后将该结构返回给请求调用方使用。
typedef struct {
HMODULE hModule;
int (*Func1)();
int (*Func2)();
int(*Func3)(int a, int b);
} LibraryFuncStruct;
int LoadLibraryFunc LPCTSTR dllFileName, LibraryFuncStruct *pStruct)
{
int retStatus = 0; // default is an error detected
pStruct->hModule = LoadLibrary (dllFileName);
if (pStruct->hModule) {
pStruct->Func1 = (int (*)()) GetProcAddress (pStruct->hModule, "Func1");
pStruct->Func2 = (int (*)()) GetProcAddress (pStruct->hModule, "Func2");
pStruct->Func3 = (int (*)(int a, int b)) GetProcAddress(pStruct->hModule, "Func3");
retStatus = 1;
}
return retStatus;
}
void FreeLibraryFunc (LibraryFuncStruct *pStruct)
{
if (pStruct->hModule) FreeLibrary (pStruct->hModule);
pStruct->hModule = 0;
}
这可用于:
LibraryFuncStruct myLib = {0};
LoadLibraryFunc (L"library.dll", &myLib);
// ....
myLib.Func1();
// ....
FreeLibraryFunc (&myLib);
相同的方法可以用于为使用底层硬件的特定模型的代码定义抽象硬件层。函数指针由工厂用硬件特定函数填充,以提供实现抽象硬件模型中指定的功能的硬件特定功能。这可以用于提供由软件使用的抽象硬件层,该软件调用工厂函数以获得特定的硬件功能接口。
用于创建委托、处理程序和回调的函数指针
您可以使用函数指针来委派某些任务或功能。C中的经典示例是与标准C库函数qsort()和bsarch()一起使用的比较委托函数指针,用于提供排序顺序,以便对项目列表进行排序或对已排序的项目列表执行二进制搜索。比较函数委托指定排序或二进制搜索中使用的排序算法。
另一种用法类似于将算法应用于C++标准模板库容器。
void * ApplyAlgorithm (void *pArray, size_t sizeItem, size_t nItems, int (*p)(void *)) {
unsigned char *pList = pArray;
unsigned char *pListEnd = pList + nItems * sizeItem;
for ( ; pList < pListEnd; pList += sizeItem) {
p (pList);
}
return pArray;
}
int pIncrement(int *pI) {
(*pI)++;
return 1;
}
void * ApplyFold(void *pArray, size_t sizeItem, size_t nItems, void * pResult, int(*p)(void *, void *)) {
unsigned char *pList = pArray;
unsigned char *pListEnd = pList + nItems * sizeItem;
for (; pList < pListEnd; pList += sizeItem) {
p(pList, pResult);
}
return pArray;
}
int pSummation(int *pI, int *pSum) {
(*pSum) += *pI;
return 1;
}
// source code and then lets use our function.
int intList[30] = { 0 }, iSum = 0;
ApplyAlgorithm(intList, sizeof(int), sizeof(intList) / sizeof(intList[0]), pIncrement);
ApplyFold(intList, sizeof(int), sizeof(intList) / sizeof(intList[0]), &iSum, pSummation);
另一个例子是GUI源代码,其中通过提供函数指针来注册特定事件的处理程序,该函数指针在事件发生时实际调用。Microsoft MFC框架及其消息映射使用类似的方法来处理传递到窗口或线程的Windows消息。
需要回调的异步函数类似于事件处理程序。异步函数的用户调用异步函数以启动某些操作,并提供一个函数指针,一旦操作完成,异步函数将调用该指针。在这种情况下,事件是完成其任务的异步函数。
其他回答
由于函数指针通常是类型化回调,因此您可能需要查看类型安全回调。这同样适用于非回调函数的入口点等。
C相当善变,同时也很宽容:)
C中的函数指针
让我们从一个我们将要指向的基本函数开始:
int addInt(int n, int m) {
return n+m;
}
首先,让我们定义一个指向接收2个int并返回一个int的函数的指针:
int (*functionPtr)(int,int);
现在我们可以安全地指出我们的功能:
functionPtr = &addInt;
现在我们有一个指向函数的指针,让我们使用它:
int sum = (*functionPtr)(2, 3); // sum == 5
将指针传递给另一个函数基本上是相同的:
int add2to3(int (*functionPtr)(int, int)) {
return (*functionPtr)(2, 3);
}
我们也可以在返回值中使用函数指针(尽量跟上,这会变得很混乱):
// this is a function called functionFactory which receives parameter n
// and returns a pointer to another function which receives two ints
// and it returns another int
int (*functionFactory(int n))(int, int) {
printf("Got parameter %d", n);
int (*functionPtr)(int,int) = &addInt;
return functionPtr;
}
但使用typedef要好得多:
typedef int (*myFuncDef)(int, int);
// note that the typedef name is indeed myFuncDef
myFuncDef functionFactory(int n) {
printf("Got parameter %d", n);
myFuncDef functionPtr = &addInt;
return functionPtr;
}
被解雇的指南:如何在x86机器上通过手工编译代码滥用GCC中的函数指针:
这些字符串文字是32位x86机器代码的字节。0xC3是x86 ret指令。
你通常不会用手工编写这些,你会用汇编语言编写,然后使用像nasm这样的汇编程序将其汇编成一个平面二进制,然后将其十六进制转储成一个C字符串文字。
返回EAX寄存器上的当前值int eax=((int(*)())(“\xc3<-返回eax寄存器的值”)();编写交换函数整数a=10,b=20;((void(*)(int*,int*))“\x8b\x44\x04\x8b\x5c\x24\x08\x80\x80\x8b\ux1b\x31\x31\xc3\xcb\x4c\x24\x04\x09\x01\xcb\x4c\x04\x08\x49\xc3<-这将交换a和b的值”)(-a,-b);将for循环计数器写入1000,每次调用一些函数((int(*)())“\x66\x31\xc0\x8b\x5c\x24\x64\x66\x40\x50\xxf\xd3\x58\x66\x3d\xC8\x03\x75\xf4\xc3”)(函数);//调用函数1->1000您甚至可以编写一个计数为100的递归函数const char*lol=“\x8b\x5c\x24\x4\x3\x8\x3\x0\x0\x7e\x2\x31\x80\x83\x48\x64\x7d\x6\x40\x53\xdf\xd3\x5b\xc3\xc3<-递归调用地址lol处的函数。”;i=((int(*)())(lol))(lol);
请注意,编译器将字符串文本放在.rodata部分(或Windows上的.rata),该部分作为文本段的一部分链接(以及函数代码)。
文本段具有Read+Exec权限,因此将字符串文本转换为函数指针是有效的,而无需像动态分配内存那样进行mprotect()或VirtualProtect()系统调用。(或者gcc-z execstack将程序与堆栈+数据段+堆可执行文件链接起来,作为快速破解。)
要反汇编这些,您可以编译它以在字节上添加标签,并使用反汇编程序。
// at global scope
const char swap[] = "\x8b\x44\x24\x04\x8b\x5c\x24\x08\x8b\x00\x8b\x1b\x31\xc3\x31\xd8\x31\xc3\x8b\x4c\x24\x04\x89\x01\x8b\x4c\x24\x08\x89\x19\xc3 <- This swaps the values of a and b";
用gcc-c-m32-foo.c编译,用objdump-D-rwC-Mintel反汇编,我们可以得到程序集,并发现该代码通过破坏EBX(一个调用保留寄存器)而违反了ABI,通常效率很低。
00000000 <swap>:
0: 8b 44 24 04 mov eax,DWORD PTR [esp+0x4] # load int *a arg from the stack
4: 8b 5c 24 08 mov ebx,DWORD PTR [esp+0x8] # ebx = b
8: 8b 00 mov eax,DWORD PTR [eax] # dereference: eax = *a
a: 8b 1b mov ebx,DWORD PTR [ebx]
c: 31 c3 xor ebx,eax # pointless xor-swap
e: 31 d8 xor eax,ebx # instead of just storing with opposite registers
10: 31 c3 xor ebx,eax
12: 8b 4c 24 04 mov ecx,DWORD PTR [esp+0x4] # reload a from the stack
16: 89 01 mov DWORD PTR [ecx],eax # store to *a
18: 8b 4c 24 08 mov ecx,DWORD PTR [esp+0x8]
1c: 89 19 mov DWORD PTR [ecx],ebx
1e: c3 ret
not shown: the later bytes are ASCII text documentation
they're not executed by the CPU because the ret instruction sends execution back to the caller
在Windows、Linux、OS X等系统上,这段机器代码(可能)可以在32位代码中工作:所有这些OS上的默认调用约定都在堆栈中传递参数,而不是在寄存器中更有效地传递参数。但EBX在所有正常的调用约定中都保留了调用,因此将其用作临时寄存器而不保存/恢复它很容易使调用者崩溃。
C中函数指针的一个主要用途是调用在运行时选择的函数。例如,C运行时库有两个例程,qsort和bsearch,它们获取一个指向函数的指针,该函数被调用来比较正在排序的两个项目;这允许您根据希望使用的任何条件分别对任何内容进行排序或搜索。
一个非常基本的例子是,如果有一个名为print(int x,int y)的函数,反过来可能需要调用一个函数(add()或sub(),它们是相同类型的),那么我们将做什么,我们将向print()函数添加一个函数指针参数,如下所示:
#include <stdio.h>
int add()
{
return (100+10);
}
int sub()
{
return (100-10);
}
void print(int x, int y, int (*func)())
{
printf("value is: %d\n", (x+y+(*func)()));
}
int main()
{
int x=100, y=200;
print(x,y,add);
print(x,y,sub);
return 0;
}
输出为:
值为:410值为:390
一旦有了基本的声明器,函数指针就很容易声明:
id:id:id是指针:*D:D指针指向函数:D(<parameters>):D函数获取<parameter>返回
而D是使用相同规则构建的另一个声明器。最后,在某个地方,它以ID结尾(参见下面的示例),这是声明实体的名称。让我们尝试构建一个函数,该函数获取指向不获取任何内容并返回int的函数的指针,并返回指向获取char并返回int函数的指针
typedef int ReturnFunction(char);
typedef int ParameterFunction(void);
ReturnFunction *f(ParameterFunction *p);
如您所见,使用typedef构建它非常容易。如果没有typedef,上面的声明器规则也不难,而且应用一致。如您所见,我漏掉了指针指向的部分,以及函数返回的内容。这是出现在声明最左边的内容,并不重要:如果已经建立了声明人,则在末尾添加。让我们这样做吧。始终如一地构建它,首先使用[和]显示结构:
function taking
[pointer to [function taking [void] returning [int]]]
returning
[pointer to [function taking [char] returning [int]]]
如您所见,可以通过一个接一个地附加声明符来完全描述类型。施工有两种方式。一种是自下而上的,从非常正确的事情(树叶)开始,一直到标识符。另一种方式是自上而下的,从标识符开始,一直到叶子。我会双向展示。
自下而上
构造从右边的东西开始:返回的东西,它是取char的函数。为了保持声明人的不同,我将对他们进行编号:
D1(char);
直接插入了char参数,因为它很简单。通过用*D2替换D1来添加指向声明器的指针。注意,我们必须用括号括住*D2。通过查找*-运算符和函数调用运算符()的优先级可以知道这一点。如果没有括号,编译器会将其读为*(D2(char p))。当然,这不会再简单地用*D2取代D1。声明符周围始终允许使用括号。实际上,如果你添加了太多,你不会出错。
(*D2)(char);
返回类型已完成!现在,让我们用返回<parameters>的函数声明器函数来替换D2,这就是我们现在的D3(<parameter>)。
(*D3(<parameters>))(char)
注意,不需要括号,因为我们希望D3这次是函数声明器,而不是指针声明器。很好,唯一剩下的就是它的参数。该参数的处理方式与我们处理返回类型的处理方式完全相同,只是将char替换为void。所以我会复制它:
(*D3( (*ID1)(void)))(char)
我已经用ID1替换了D2,因为我们已经完成了该参数(它已经是一个指向函数的指针,不需要另一个声明器)。ID1将是参数的名称。现在,我在上面的结尾告诉我们,一个添加了所有声明者修改的类型,即出现在每个声明的最左边的类型。对于函数,这将成为返回类型。对于指向类型等的指针……写下类型时很有趣,它将以相反的顺序出现在最右边:)无论如何,替换它会产生完整的声明。当然,这两次都是int。
int (*ID0(int (*ID1)(void)))(char)
在该示例中,我调用了函数ID0的标识符。
自上而下
这从类型描述中最左边的标识符开始,当我们从右边走过时,将该声明符包装起来。从函数获取<parameters>开始返回
ID0(<parameters>)
描述中的下一件事(在“返回”之后)是指针指向。让我们合并它:
*ID0(<parameters>)
然后,下一件事是functon获取<parameters>返回。该参数是一个简单的字符,因此我们再次将其放入,因为它非常简单。
(*ID0(<parameters>))(char)
注意我们添加的括号,因为我们再次希望先绑定*,然后绑定(char)。否则它将读取获取<parameters>返回函数的函数。。。。不,甚至不允许返回函数的函数。
现在我们只需要放置<parameters>。我将展示一个简短的嘲笑版本,因为我认为你现在已经知道该怎么做了。
pointer to: *ID1
... function taking void returning: (*ID1)(void)
就像我们使用自底向上方法一样,只需将int放在声明符之前,我们就完成了
int (*ID0(int (*ID1)(void)))(char)
好东西
自下而上还是自上而下更好?我习惯于自下而上,但有些人可能更喜欢自上而下。我认为这是品味问题。顺便说一句,如果您在该声明中应用所有运算符,您将得到一个int:
int v = (*ID0(some_function_pointer))(some_char);
这是C中声明的一个很好的属性:声明断言,如果在使用标识符的表达式中使用这些运算符,那么它将在最左边生成类型。阵列也是如此。
希望你喜欢这个小教程!现在,当人们对函数奇怪的声明语法感到疑惑时,我们可以联系到这一点。我尽量少放C内部构件。随意编辑/修复其中的内容。