C中的函数指针如何工作?


1232

我最近对C中的函数指针有一些经验。

因此,继续回答您自己的问题的传统,我决定为需要快速深入学习该主题的人做一些基本的总结。


34
另外:有关对C指针的深入分析,请参见blogs.oracle.com/ksplice/entry/the_ksplice_pointer_challenge。此外,从头开始编程还显示了它们在机器级别的工作方式。了解C的“内存模型”对于了解C指针的工作原理非常有用。
Abbafei 2013年

8
很棒的信息。不过,按照标题,我希望能真正看到有关“函数指针如何工作”的解释,而不是它们的编码方式:)
Bogdan Alexandru

Answers:


1477

C中的函数指针

让我们从一个基本功能开始,我们将指向

int addInt(int n, int m) {
    return n+m;
}

首先,让我们定义一个指向接收2 ints并返回的函数的指针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;
}

19
感谢您提供的信息。您能否在使用函数指针的地方添加一些见解,或者碰巧特别有用?
Rich.Carpenter 2009年

326
“ functionPtr =&addInt;” 也可以写成(通常是)“ functionPtr = addInt;” 这也是有效的,因为该标准说此上下文中的函数名称被转换为函数的地址。
hlovdal

22
赫洛瓦尔(Hlovdal),在这种情况下,有趣的是,这使人们能够编写函数Ptr = ****************** addInt;
Johannes Schaub-litb

105
@ Rich.Carpenter我知道这已经晚了4年,但是我认为其他人可能会从中受益:函数指针对于将函数作为参数传递给其他函数很有用。由于种种奇怪的原因,我花了很多时间才能找到答案。因此,基本上,它提供了C伪一流的功能。
2013年

22
@ Rich.Carpenter:函数指针非常适合运行时CPU检测。具有某些功能的多个版本以利用SSE,popcnt,AVX等。在启动时,将功能指针设置为当前CPU的每个功能的最佳版本。在您的其他代码中,只需通过函数指针进行调用即可,而不必在所有CPU功能上都有条件分支。然后,即使该CPU支持pshufb,它也可以执行复杂的逻辑来确定是否正确,因为它很慢,因此较早的实现仍然更快。x264 / x265广泛使用此功能,并且是开源的。
彼得·科德斯

303

C中的函数指针可用于在C中执行面向对象的编程。

例如,以下几行用C编写:

String s1 = newString();
s1->set(s1, "hello");

是的,->缺少new操作符是一种致命的选择,但这肯定意味着我们正在将某个String类的文本设置为"hello"

通过使用函数指针,可以在C中模拟方法

这是如何完成的?

String级实际上是一个是struct与一群函数指针充当一种方式来模拟方法。以下是String该类的部分声明:

typedef struct String_Struct* String;

struct String_Struct
{
    char* (*get)(const void* self);
    void (*set)(const void* self, char* value);
    int (*length)(const void* self);
};

char* getString(const void* self);
void setString(const void* self, char* value);
int lengthString(const void* self);

String newString();

可以看出,String该类的方法实际上是指向已声明函数的函数指针。在准备的实例时String,将newString调用函数,以设置指向其各自函数的函数指针:

String newString()
{
    String self = (String)malloc(sizeof(struct String_Struct));

    self->get = &getString;
    self->set = &setString;
    self->length = &lengthString;

    self->set(self, "");

    return self;
}

例如,getString通过调用get方法调用的函数定义如下:

char* getString(const void* self_obj)
{
    return ((String)self_obj)->internal->value;
}

可以注意到的一件事是,没有对象实例的概念,并且其方法实际上是对象的一部分,因此必须在每次调用时传递“自身对象”。(而internal只是一个隐藏struct,它在前面的代码清单中被省略了,它是一种执行信息隐藏的方法,但是与函数指针无关。)

因此,s1->set("hello");必须传递对象才能对其执行操作,而不是能够这样做s1->set(s1, "hello")

有了这个次要的解释,您必须将自己的引用传递给其他人,我们将进入下一部分,即C中的继承

假设我们要成为的子类String,例如ImmutableString。为了使字符串不可变,该set方法将不可访问,同时保持对get和的访问length,并强制“构造函数”接受一个char*

typedef struct ImmutableString_Struct* ImmutableString;

struct ImmutableString_Struct
{
    String base;

    char* (*get)(const void* self);
    int (*length)(const void* self);
};

ImmutableString newImmutableString(const char* value);

基本上,对于所有子类,可用的方法再次是函数指针。这次,该set方法的声明不存在,因此,不能在中调用它ImmutableString

至于的实现ImmutableString,唯一相关的代码是“构造函数”函数newImmutableString

ImmutableString newImmutableString(const char* value)
{
    ImmutableString self = (ImmutableString)malloc(sizeof(struct ImmutableString_Struct));

    self->base = newString();

    self->get = self->base->get;
    self->length = self->base->length;

    self->base->set(self->base, (char*)value);

    return self;
}

在实例化时ImmutableString,指向getlength方法的函数指针实际上通过遍历内部存储的变量来引用String.getString.length方法。baseString

使用函数指针可以实现从超类继承方法。

我们可以进一步在C中继续多态性

例如,如果由于某种原因我们想要更改length方法的行为以使其0始终返回类中的所有时间,则ImmutableString要做的就是:

  1. 添加一个将用作替代length方法的函数。
  2. 转到“构造函数”,然后将函数指针设置为覆盖length方法。

在其中添加替代length方法ImmutableString可以通过添加lengthOverrideMethod

int lengthOverrideMethod(const void* self)
{
    return 0;
}

然后,将length构造函数中方法的函数指针连接到lengthOverrideMethod

ImmutableString newImmutableString(const char* value)
{
    ImmutableString self = (ImmutableString)malloc(sizeof(struct ImmutableString_Struct));

    self->base = newString();

    self->get = self->base->get;
    self->length = &lengthOverrideMethod;

    self->base->set(self->base, (char*)value);

    return self;
}

现在,而不是具有用于相同的行为length在方法ImmutableString类作为String类,现在length方法将是指在定义的行为lengthOverrideMethod功能。

我必须添加一个免责声明,即我仍在学习如何使用C进行面向对象的编程风格进行编写,因此可能有些地方我并没有很好地解释,或者就如何最好地实现OOP而言可能已超出标准但是我的目的是试图说明函数指针的许多用法之一。

有关如何在C中执行面向对象编程的更多信息,请参考以下问题:


22
这个答案太可怕了!这不仅意味着OO某种程度上取决于点表示法,而且还鼓励将垃圾放入对象中!
阿列克谢·阿维琴科

27
OO是好的,但是在C风格的OO之外没有。您已经彻底实现的是基于Javascript样式的基于原型的OO。要获得C ++ / Pascal风格的OO,您需要:1.为具有虚拟成员的每个的虚拟表提供一个const结构。2.在多态对象中具有指向该结构的指针。3.通过虚拟表以及所有其他方法直接调用虚拟方法-通常遵循某些ClassName_methodName函数命名约定。只有这样,您才能获得与C ++和Pascal相同的运行时和存储成本。
恢复莫妮卡

19
使用非OO语言来进行OO总是一个坏主意。如果您想要OO并且仍然有C,请使用C ++。
rbaleksandar

20
@rbaleksandar告诉Linux内核开发人员。严格来说,“总是一个坏主意”是您的观点,我对此表示坚决不同意。
乔纳森·莱因哈特

6
我喜欢这个答案,但不投的malloc

227

被解雇的指南:如何通过手动编译代码来滥用x86机器上GCC中的函数指针:

这些字符串文字是32位x86机器代码的字节。 0xC3x86 ret指令

通常,您不会手动编写这些代码,而是使用汇编语言编写代码,然后使用汇编程序nasm将其汇编为平面二进制文件,然后将其十六进制转储为C字符串文字。

  1. 返回EAX寄存器上的当前值

    int eax = ((int(*)())("\xc3 <- This returns the value of the EAX register"))();
  2. 编写交换功能

    int a = 10, b = 20;
    ((void(*)(int*,int*))"\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")(&a,&b);
  3. 写一个for循环计数器到1000,每次调用一些函数

    ((int(*)())"\x66\x31\xc0\x8b\x5c\x24\x04\x66\x40\x50\xff\xd3\x58\x66\x3d\xe8\x03\x75\xf4\xc3")(&function); // calls function with 1->1000
  4. 您甚至可以编写一个递归函数,计数为100

    const char* lol = "\x8b\x5c\x24\x4\x3d\xe8\x3\x0\x0\x7e\x2\x31\xc0\x83\xf8\x64\x7d\x6\x40\x53\xff\xd3\x5b\xc3\xc3 <- Recursively calls the function at address lol.";
    i = ((int(*)())(lol))(lol);

请注意,编译器将字符串文字放在该.rodata部分(或.rdata在Windows中)中,该部分作为文本段的一部分链接(以及功能代码)。

文本段具有读+ 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上的默认调用约定都将args传递给堆栈,而不是更有效地在寄存器中传递。但是EBX在所有正常的呼叫约定中都保留了呼叫,因此将其用作暂存寄存器而不进行保存/恢复会很容易使呼叫者崩溃。


8
注意:如果启用了“数据执行保护”(例如,在Windows XP SP2 +上),则此方法不起作用,因为通常不会将C字符串标记为可执行文件。
SecurityMatt

5
嗨,马特!根据优化级别,GCC通常会将字符串常量内联到TEXT段中,因此,即使您不禁止这种类型的优化,它也可以在较新版本的Windows上使用。(IIRC,两年多以前发布时的MINGW版本,以默认优化级别内联了字符串文字)
Lee Lee

10
有人可以解释一下这里发生了什么吗?那些看起来很奇怪的字符串文字是什么?
2014年

56
@ajay好像他正在将原始十六进制值(例如'\ x00'与'/ 0'相同,它们都等于0)写入字符串,然后将字符串转换为C函数指针,然后执行C函数指针,因为他是魔鬼。
ejk314

3
嗨,FUZxxl,我认为它可能会根据编译器和操作系统版本而有所不同。上面的代码似乎可以在codepad.org上正常运行;codepad.org/FMSDQ3ME

115

我最喜欢的函数指针用法之一是便宜又简单的迭代器-

#include <stdio.h>
#define MAX_COLORS  256

typedef struct {
    char* name;
    int red;
    int green;
    int blue;
} Color;

Color Colors[MAX_COLORS];


void eachColor (void (*fp)(Color *c)) {
    int i;
    for (i=0; i<MAX_COLORS; i++)
        (*fp)(&Colors[i]);
}

void printColor(Color* c) {
    if (c->name)
        printf("%s = %i,%i,%i\n", c->name, c->red, c->green, c->blue);
}

int main() {
    Colors[0].name="red";
    Colors[0].red=255;
    Colors[1].name="blue";
    Colors[1].blue=255;
    Colors[2].name="black";

    eachColor(printColor);
}

7
如果要以某种方式从迭代中提取任何输出(请考虑闭包),则还应该传递指向用户指定数据的指针。
阿列克谢·阿维琴科

1
同意 我所有的迭代器如下所示:int (*cb)(void *arg, ...)。迭代器的返回值还让我提前停止(如果非零)。
Jonathon Reinhart 2015年

24

一旦有了基本的声明器,函数指针就变得易于声明:

  • id ID::ID是一个
  • 指针*D::D指针
  • 功能:D(<parameters>)d功能拍摄<参数,>返回

D是使用相同规则构建的另一个声明符。最后,它以某处结尾ID(请参见下面的示例),这是已声明实体的名称。让我们尝试构建一个函数,该函数使用指向不带任何内容并返回int的函数的指针,并返回指向带char并返回int的函数的指针。使用type-def就是这样

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参数,因为它很简单。通过替换D1为添加声明符的指针*D2。请注意,我们必须在周围加上括号*D2。通过查找*-operator和函数调用运算符的优先级可以知道这一点()。没有括号,编译器会将其读取为*(D2(char p))。但是,那当然不再是D1的简单替代*D2。声明符周围总是允许有括号。因此,实际上,如果添加太多,就不会出错。

(*D2)(char);

返回类型完成!现在,让我们来取代D2由函数声明功能回吐<parameters>返回,这就是D3(<parameters>)我们现在所在的。

(*D3(<parameters>))(char)

请注意,不需要括号,因为这次我们 D3成为一个函数声明器而不是指针声明器。很好,剩下的只是它的参数。参数的设置与返回类型完全相同,只是用char代替void。所以我将其复制:

(*D3(   (*ID1)(void)))(char)

我已替换D2ID1,因为我们已经完成了该参数(它已经是一个指向函数的指针-不需要其他声明符)。ID1将是参数的名称。现在,我在上面最后告诉我们,添加一个所有这些声明器都修改的类型-一种出现在每个声明的最左侧。对于函数,这将成为返回类型。对于指向类型的指针等...写下类型很有意思,它会以相反的顺序出现在最右边:)无论如何,代之以产生完整的声明。int当然两次。

int (*ID0(int (*ID1)(void)))(char)

ID0在该示例中,我称为函数的标识符。

自顶向下

它从类型描述的最左边的标识符开始,在我们通过右边的方式包装该声明符。从函数返回参数开始<>

ID0(<parameters>)

描述中的下一件事(“返回”之后)是的指针。让我们合并一下:

*ID0(<parameters>)

然后,接下来是functon接受<参数>返回。该参数是一个简单的char,因此我们马上将其重新放置,因为它确实很简单。

(*ID0(<parameters>))(char)

请注意,我们添加了括号,因为我们想再次的*结合第一,而随后(char)。否则,它会读取功能以<参数>返回函数...。是的,甚至不允许返回函数。

现在我们只需要放入<参数>。我将展示派生的简短版本,因为我认为您现在已经知道如何执行此操作。

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内部构件。随时编辑/修复其中的内容。


24

函数指针的另一个好用法:
轻松切换版本

当您在不同的时间或不同的开发阶段需要不同的功能时,它们非常方便使用。例如,我正在具有控制台的主机上开发应用程序,但是该软件的最终版本将放在Avnet ZedBoard上(该端口具有用于显示和控制台的端口,但是不需要/不需要它们用于)。最终版本)。因此,在开发过程中,我将使用它printf来查看状态和错误消息,但完成后,我什么都不想打印了。这是我所做的:

版本

// First, undefine all macros associated with version.h
#undef DEBUG_VERSION
#undef RELEASE_VERSION
#undef INVALID_VERSION


// Define which version we want to use
#define DEBUG_VERSION       // The current version
// #define RELEASE_VERSION  // To be uncommented when finished debugging

#ifndef __VERSION_H_      /* prevent circular inclusions */
    #define __VERSION_H_  /* by using protection macros */
    void board_init();
    void noprintf(const char *c, ...); // mimic the printf prototype
#endif

// Mimics the printf function prototype. This is what I'll actually 
// use to print stuff to the screen
void (* zprintf)(const char*, ...); 

// If debug version, use printf
#ifdef DEBUG_VERSION
    #include <stdio.h>
#endif

// If both debug and release version, error
#ifdef DEBUG_VERSION
#ifdef RELEASE_VERSION
    #define INVALID_VERSION
#endif
#endif

// If neither debug or release version, error
#ifndef DEBUG_VERSION
#ifndef RELEASE_VERSION
    #define INVALID_VERSION
#endif
#endif

#ifdef INVALID_VERSION
    // Won't allow compilation without a valid version define
    #error "Invalid version definition"
#endif

在本文中,version.c我将定义2个函数原型version.h

版本号

#include "version.h"

/*****************************************************************************/
/**
* @name board_init
*
* Sets up the application based on the version type defined in version.h.
* Includes allowing or prohibiting printing to STDOUT.
*
* MUST BE CALLED FIRST THING IN MAIN
*
* @return    None
*
*****************************************************************************/
void board_init()
{
    // Assign the print function to the correct function pointer
    #ifdef DEBUG_VERSION
        zprintf = &printf;
    #else
        // Defined below this function
        zprintf = &noprintf;
    #endif
}

/*****************************************************************************/
/**
* @name noprintf
*
* simply returns with no actions performed
*
* @return   None
*
*****************************************************************************/
void noprintf(const char* c, ...)
{
    return;
}

注意函数指针是如何在原型version.h作为

void (* zprintf)(const char *, ...);

当在应用程序中引用它时,它将在其指向的任何位置(尚未定义)开始执行。

在中version.c,请注意,在board_init()函数中,zprintf根据在中定义的版本,为该函数分配了一个唯一功能(其功能签名匹配)。version.h

zprintf = &printf; zprintf调用printf进行调试

要么

zprintf = &noprint; zprintf只是返回而不会运行不必要的代码

运行代码如下所示:

主程序

#include "version.h"
#include <stdlib.h>
int main()
{
    // Must run board_init(), which assigns the function
    // pointer to an actual function
    board_init();

    void *ptr = malloc(100); // Allocate 100 bytes of memory
    // malloc returns NULL if unable to allocate the memory.

    if (ptr == NULL)
    {
        zprintf("Unable to allocate memory\n");
        return 1;
    }

    // Other things to do...
    return 0;
}

上面的代码将printf在调试模式下使用,或者在释放模式下不执行任何操作。这比遍历整个项目并注释掉或删除代码要容易得多。我需要做的就是更改版本,version.h其余代码将由代码完成!


4
U站会浪费很多性能时间。相反,您可以使用一个宏来启用和禁用基于Debug / Release的一段代码。
AlphaGoku

19

函数指针通常由定义typedef,并用作参数和返回值。

上面的答案已经解释了很多,我只举一个完整的例子:

#include <stdio.h>

#define NUM_A 1
#define NUM_B 2

// define a function pointer type
typedef int (*two_num_operation)(int, int);

// an actual standalone function
static int sum(int a, int b) {
    return a + b;
}

// use function pointer as param,
static int sum_via_pointer(int a, int b, two_num_operation funp) {
    return (*funp)(a, b);
}

// use function pointer as return value,
static two_num_operation get_sum_fun() {
    return &sum;
}

// test - use function pointer as variable,
void test_pointer_as_variable() {
    // create a pointer to function,
    two_num_operation sum_p = &sum;
    // call function via pointer
    printf("pointer as variable:\t %d + %d = %d\n", NUM_A, NUM_B, (*sum_p)(NUM_A, NUM_B));
}

// test - use function pointer as param,
void test_pointer_as_param() {
    printf("pointer as param:\t %d + %d = %d\n", NUM_A, NUM_B, sum_via_pointer(NUM_A, NUM_B, &sum));
}

// test - use function pointer as return value,
void test_pointer_as_return_value() {
    printf("pointer as return value:\t %d + %d = %d\n", NUM_A, NUM_B, (*get_sum_fun())(NUM_A, NUM_B));
}

int main() {
    test_pointer_as_variable();
    test_pointer_as_param();
    test_pointer_as_return_value();

    return 0;
}

14

C中函数指针的一大用途是调用在运行时选择的函数。例如,C运行时库有两个例程,qsortbsearch,它们使用指向一个函数的指针,该函数被调用来比较要排序的两个项目。这使您可以根据想要使用的任何标准分别对任何内容进行排序或搜索。

一个非常基本的示例,如果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


9

从头开始功能具有从其开始执行的位置的一些内存地址。在汇编语言中,它们被称为(称为“函数的内存地址”)。现在返回到C,如果函数具有内存地址,则可以由C中的指针对其进行操作。因此,按C的规则

1.首先需要声明一个指向函数的指针2.传递所需函数的地址

****注意->功能应为同一类型****

这个简单的程序将说明一切。

#include<stdio.h>
void (*print)() ;//Declare a  Function Pointers
void sayhello();//Declare The Function Whose Address is to be passed
                //The Functions should Be of Same Type
int main()
{
 print=sayhello;//Addressof sayhello is assigned to print
 print();//print Does A call To The Function 
 return 0;
}

void sayhello()
{
 printf("\n Hello World");
}

在此处输入图片说明之后,让我们看一下机器如何理解它们。上述程序在32位架构中的机器指令一览。

红色标记区域显示如何交换地址并将其存储在eax中。然后,它们是eax上的呼叫指令。eax包含函数的所需地址。


8

函数指针是包含函数地址的变量。由于它是一个指针变量,但是具有某些受限制的属性,因此您可以像使用数据结构中的任何其他指针变量一样使用它。

我能想到的唯一例外是将函数指针视为指向单个值以外的其他东西。通过递增或递减函数指针或对函数指针增加/减去偏移量来进行指针算术并不是真正的实用程序,因为函数指针仅指向单个对象,即函数的入口点。

函数指针变量的大小,该变量占用的字节数可能会根据基础架构(例如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是一个接受a int和a char *并返回an 的函数int
  • pFunc是一个函数指针,分配给该函数的函数的地址是a int和a char *并返回aint

因此从上面我们可以得到一个源代码行,其中函数的地址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样式与函数指针一起namespace使用Globalstruct

您可以使用static关键字指定名称为文件作用域的函数,然后将其分配给全局变量,以提供类似于namespaceC ++功能的方式。

在头文件中定义一个结构,它将成为我们的命名空间以及使用它的全局变量。

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

然后,通过指定全局struct变量的完整名称和成员名称来使用该名称,以访问该函数。该const改性剂的全球化,所以它不能被意外的改变。

int abcd = FuncThingsGlobal.func1 (a, b);

功能指针的应用领域

DLL库组件可以执行与C样式namespace方法类似的操作,在C 方法中,从支持创建struct包含函数指针的库接口中的工厂方法中请求特定的库接口。此库接口加载所请求的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()bsearch()用于排序的项目的列表或执行过的项目的排序列表二进制搜索提供归类顺序。比较函数委托指定排序或二进制搜索中使用的排序规则算法。

另一个用途类似于将算法应用于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消息。

需要回调的异步函数类似于事件处理程序。异步函数的用户调用异步函数以启动某些操作,并提供一个函数指针,一旦操作完成,异步函数将调用该函数指针。在这种情况下,事件是异步功能完成其任务。


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.