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

How do function pointers in C work?

我最近在C语言的函数指针方面有一些经验。

因此,按照回答你自己问题的传统,我决定为那些需要快速深入主题的人做一个非常基础的小总结。


C中的函数指针

让我们从一个基本函数开始,我们将指向:

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

首先,让我们定义一个指向函数的指针,该函数接收2个ints并返回一个int

1
int (*functionPtr)(int,int);

现在我们可以安全地指出我们的功能:

1
functionPtr = &addInt;

现在我们有了一个指向函数的指针,让我们使用它:

1
int sum = (*functionPtr)(2, 3); // sum == 5

将指针传递给另一个函数基本上是相同的:

1
2
3
int add2to3(int (*functionPtr)(int, int)) {
    return (*functionPtr)(2, 3);
}

我们也可以在返回值中使用函数指针(尝试跟上,它会变得混乱):

1
2
3
4
5
6
7
8
// 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更好:

1
2
3
4
5
6
7
8
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;
}


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

例如,以下行是用C编写的:

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

是的,->和缺少new操作符是一种死的放弃,但这显然意味着我们将一些String类的文本设置为"hello"

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

这是如何完成的?

String类实际上是一个struct类,它有一组函数指针,用作模拟方法的方法。以下是String类的部分声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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函数以设置指向其各自函数的函数指针:

1
2
3
4
5
6
7
8
9
10
11
12
String newString()
{
    String self = (String)malloc(sizeof(struct String_Struct));

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

    self->set(self,"");

    return self;
}

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

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

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

因此,我们不能做s1->set("hello");,而必须把目标交给s1->set(s1,"hello")

有了这一次要的解释,我们将转到下一个部分,即C语言中的继承。

假设我们要做一个String的子类,比如说ImmutableString。为了使字符串不可变,在保持对getlength的访问的同时,set方法将不可访问,并强制"构造函数"接受char*

1
2
3
4
5
6
7
8
9
10
11
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的实现,唯一相关的代码是"constructor"函数,newImmutableString是:

1
2
3
4
5
6
7
8
9
10
11
12
13
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方法,通过遍历base变量,该变量是一个内部存储的String对象。

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

我们可以继续研究C的多态性。

例如,如果我们想改变length方法的行为,以在ImmutableString类中始终返回0,出于某种原因,所要做的就是:

  • 添加一个将用作重写length方法的函数。
  • 转到"constructor"并将函数指针设置为重写的length方法。
  • ImmutableString中添加重写的length方法可以通过添加lengthOverrideMethod来执行:

    1
    2
    3
    4
    int lengthOverrideMethod(const void* self)
    {
        return 0;
    }

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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;
    }

    现在,与ImmutableString类中的length方法与String类具有相同的行为不同,length方法将引用lengthOverrideMethod函数中定义的行为。

    我必须添加一个免责声明,我仍然在学习如何用C语言编写面向对象的编程风格,因此可能有一些点我解释得不好,或者可能只是在如何最好地在C语言中实现OOP方面做了偏离标记。但是我的目的是试图说明函数指针的多种用途之一。

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

    • C中的对象方向?
    • 你能用C语言写面向对象的代码吗?


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

    这些字符串文本是32位x86计算机代码的字节。0xC3是x86 ret指令。

    通常不需要手工编写,而是使用汇编语言编写,然后使用像nasm这样的汇编程序将其汇编成一个平面二进制文件,然后将其转换成C字符串文本。

  • 返回EAX寄存器上的当前值

    1
    int eax = ((int(*)())("\xc3 <- This returns the value of the EAX register"))();
  • 编写交换函数

    1
    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);
  • 将for循环计数器写入1000,每次调用一些函数

    1
    ((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
  • 甚至可以编写一个计数为100的递归函数

    1
    2
    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部分(或Windows上的.rdata)中,该部分作为文本段的一部分进行链接(以及函数代码)。

    文本段具有read+exec权限,因此将字符串文本强制转换为函数指针可以在不需要像动态分配内存那样调用mprotect()VirtualProtect()系统的情况下工作。(或gcc -z execstack将程序与stack+data-segment+heap可执行文件链接起来,作为一种快速的黑客攻击。)

    要分解这些,可以编译它以在字节上放置标签,并使用反汇编程序。

    1
    2
    // 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反汇编,就可以得到汇编,发现这个代码与abi的代码冲突,就是破坏了ebx(一个保留调用的寄存器),而且效率一般很低。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    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位代码工作:所有这些操作系统上的默认调用约定都在堆栈上传递参数,而不是在寄存器中更有效地传递参数。但是ebx在所有正常的调用约定中都保留了调用,因此将其用作临时寄存器而不保存/恢复它很容易使调用方崩溃。


    我最喜欢的函数指针的用法之一是使用廉价且简单的迭代器-

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    #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
    "
    , 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);
    }


    一旦有了基本声明符,函数指针就很容易声明:好的。

    • id:ID:id是一个
    • 指针:*DD指针指向
    • 函数:D()D函数取<参数>返回

    而d是另一个使用相同规则构建的声明器。最后,在某个地方,它以ID结尾(参见下面的示例),这是声明实体的名称。让我们尝试构建一个函数,获取一个指向一个不获取任何内容并返回int的函数的指针,并返回一个指向一个获取char并返回int的函数的指针。好的。

    1
    2
    3
    typedef int ReturnFunction(char);
    typedef int ParameterFunction(void);
    ReturnFunction *f(ParameterFunction *p);

    如您所见,使用typedef构建它非常容易。如果没有typedef,使用上面一致应用的声明器规则也不难。如您所见,我遗漏了指针指向的部分,以及函数返回的内容。这就是在声明的最左边出现的,并不重要:如果已经构建了声明者,则在末尾添加它。让我们这样做。始终如一地建立起来,第一个字-显示使用[]的结构:好的。

    1
    2
    3
    4
    function taking
        [pointer to [function taking [void] returning [int]]]
    returning
        [pointer to [function taking [char] returning [int]]]

    如您所见,可以通过逐个附加声明符来完全描述一个类型。施工有两种方式。一个是自下而上的,从非常正确的东西(leaves)开始,一直到标识符。另一种方法是自顶向下,从标识符开始,一直向下到树叶。我将展示两种方式。好的。自下而上

    构造从右边的东西开始:返回的东西,它是接受char的函数。为了保持声明者的不同,我要给他们编号:好的。

    1
    D1(char);

    直接插入char参数,因为它很简单。通过将D1替换为*D2,添加指向声明器的指针。注意,我们必须在*D2周围加上括号。这可以通过查找*-operator和函数调用操作符()的优先级来知道。如果没有括号,编译器将把它读作*(D2(char p))。当然,这将不再是用*D2代替d1的简单方法了。声明符周围始终允许使用括号。实际上,如果你把它们加得太多,就不会出错。好的。

    1
    (*D2)(char);

    返回类型已完成!现在,让我们用函数声明器函数来代替D2,它接受返回,即我们现在使用的D3()。好的。

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

    注意,不需要括号,因为我们希望D3是一个函数声明符,而不是指针声明符。很好,只剩下参数了。参数的操作与返回类型的操作完全相同,只是用char替换为void。所以我会复制它:好的。

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

    我已经用ID1替换了D2,因为我们已经完成了该参数(它已经是指向函数的指针,不需要另一个声明符)。ID1将是参数的名称。现在,我在最后告诉您,添加了所有声明器修改的类型——出现在每个声明的最左边的类型。对于函数,它将成为返回类型。对于指针,指向类型等…有趣的是,当写下类型时,它将以相反的顺序出现,在最右边:)无论如何,替换它将生成完整的声明。当然,两次都是。好的。

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

    在那个例子中,我调用了函数ID0的标识符。好的。自顶向下

    这从类型描述中最左边的标识符开始,在我们向右走的时候包装这个声明符。从函数开始取<参数>返回好的。

    1
    ID0(<parameters>)

    描述中的下一件事(在"返回"之后)是指向的指针。让我们把它合并起来:好的。

    1
    *ID0(<parameters>)

    然后,下一个功能是获取<参数>返回。这个参数是一个简单的字符,所以我们马上把它放进去,因为它真的很小。好的。

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

    注意我们添加的括号,因为我们再次希望*首先绑定,然后再绑定(char)。否则,它将读取函数,取<参数>返回函数….noes,甚至不允许返回函数的函数。好的。

    现在我们只需要把<参数>放入。我将展示一个派生的简短版本,因为我认为你现在已经知道如何做了。好的。

    1
    2
    pointer to: *ID1
    ... function taking void returning: (*ID1)(void)

    就像我们自下而上那样,把int放在声明人面前,我们就完了。好的。

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

    好东西

    自下而上还是自上而下更好?我习惯自下而上,但有些人可能更喜欢自上而下。我认为这是一个品味问题。顺便说一句,如果应用该声明中的所有运算符,最终将得到一个int:好的。

    1
    int v = (*ID0(some_function_pointer))(some_char);

    这是C中声明的一个很好的属性:声明声明断言,如果在使用标识符的表达式中使用这些运算符,那么它将在最左边生成类型。数组也是这样。好的。

    希望你喜欢这个小教程!现在,当人们对函数的奇怪声明语法感到疑惑时,我们可以链接到这一点。我试着尽可能少地放入C内部。请随意编辑/修复其中的内容。好的。好啊。


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

    当您希望在不同的时间或不同的开发阶段使用不同的函数时,它们非常方便。例如,我正在一台有控制台的主机上开发一个应用程序,但是软件的最终版本将被放在一个avnet zedboard上(它有用于显示和控制台的端口,但最终版本不需要/不需要)。所以在开发过程中,我将使用printf查看状态和错误消息,但完成后,我不想打印任何内容。以下是我所做的:

    版本h

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    // 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中,我将定义version.h中存在的两个函数原型。

    C版本

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    #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只返回,不会运行不必要的代码。

    运行代码如下所示:

    主丙

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    #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
    "
    );
            return 1;
        }

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

    如果处于调试模式,上述代码将使用printf,如果处于发布模式,则不执行任何操作。这比浏览整个项目、注释或删除代码容易得多。我所要做的就是更改version.h中的版本,代码将完成其余的工作!


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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    #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
    "
    , 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
    "
    , 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
    "
    , 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;
    }

    C中函数指针的一个主要用途是调用在运行时选择的函数。例如,C运行时库有两个例程,分别是qsortbsearch,它们将一个指针指向一个函数,该函数被调用来比较两个正在排序的项;这允许您根据要使用的任何条件分别对任何内容进行排序或搜索。

    一个非常基本的例子,如果有一个函数名为print(int x, int y),而该函数可能需要调用一个函数(add()sub()的类型相同),那么我们将做什么,我们将向print()函数添加一个函数指针参数,如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    #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
    "
    , (x+y+(*func)()));
    }

    int main()
    {
        int x=100, y=200;
        print(x,y,add);
        print(x,y,sub);

        return 0;
    }

    输出是:

    value is: 410
    value is: 390


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

    我能想到的唯一例外是将函数指针视为指向单个值以外的其他值。通过增加或减少一个函数指针或增加/减少一个函数指针的偏移量来进行指针算术实际上不是什么实用程序,因为函数指针只指向一个单一的东西,即一个函数的入口点。好的。

    函数指针变量的大小,即变量所占的字节数,可能因基础架构(如x32或x64或其他)的不同而有所不同。好的。

    函数指针变量的声明需要指定与函数声明相同的信息类型,以便C编译器执行其通常执行的检查类型。如果在函数指针的声明/定义中没有指定参数列表,C编译器将无法检查参数的使用。有些情况下,这种缺乏检查是有用的,但请记住,安全网已被移除。好的。

    一些例子:好的。

    1
    2
    3
    4
    5
    6
    7
    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是一个函数,它接受intchar *并返回int
    • pFunc是一个函数指针,它被分配一个函数的地址,该函数接受intchar *并返回int

    因此,从上面我们可以得到一个源行,其中函数func()的地址被分配给函数指针变量pFunc,就像在pFunc = func;中那样。好的。

    注意与函数指针声明/定义一起使用的语法,其中括号用于克服自然运算符优先规则。好的。

    1
    2
    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

    几个不同的用法示例好的。

    函数指针的一些用法示例:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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

    可以在函数指针的定义中使用可变长度参数列表。好的。

    1
    2
    int sum (int a, int b, ...);
    int (*psum)(int a, int b, ...);

    或者根本不能指定参数列表。这可能很有用,但它消除了C编译器对所提供的参数列表执行检查的机会。好的。

    1
    2
    3
    4
    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编译器可能在检查方面比较松懈,或者提供警告而不是错误。好的。

    1
    2
    3
    4
    5
    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语句检查函数指针是否等于特定的函数地址,尽管我不确定这有多有用。其他比较运算符的实用性似乎更低。好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    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编译器的警告。这也适用于函数的函数指针参数:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    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.
    }

    使用带有函数指针的全局struct的C样式namespace。好的。

    您可以使用EDCOX1的3个关键字来指定一个函数,该函数的名称是文件范围,然后将其赋值给全局变量,以提供类似于C++的EDCOX1和1的功能。好的。

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

    1
    2
    3
    4
    5
    6
    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源文件中:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    #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};

    然后,通过指定全局结构变量的完整名称和成员名称来访问函数。在全球范围内使用const修改器,以避免意外更改。好的。

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

    功能指针的应用领域好的。

    dll库组件可以执行类似于c样式的namespace方法的操作,在该方法中,从支持创建包含struct的函数指针的库接口中的工厂方法请求特定的库接口。此库接口加载请求的dll版本,创建具有必要函数指针的结构,然后将该结构返回给请求调用方以供使用。好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    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;
    }

    这可用于:好的。

    1
    2
    3
    4
    5
    6
    LibraryFuncStruct myLib = {0};
    LoadLibraryFunc (L"library.dll", &myLib);
    //  ....
    myLib.Func1();
    //  ....
    FreeLibraryFunc (&myLib);

    同样的方法可以用于为使用底层硬件的特定模型的代码定义抽象硬件层。函数指针由工厂用特定于硬件的函数填充,以提供实现抽象硬件模型中指定的函数的特定于硬件的功能。这可用于提供软件使用的抽象硬件层,该软件调用工厂函数以获取特定的硬件功能接口,然后使用提供的函数指针为底层硬件执行操作,而无需了解有关特定目标的实现细节。好的。

    用于创建委托、处理程序和回调的函数指针好的。

    可以使用函数指针作为委托某些任务或功能的方法。C中的经典示例是比较委托函数指针,与标准C库函数qsort()bsearch()一起使用,以提供排序顺序,以便对项目列表进行排序或对已排序的项目列表执行二进制搜索。比较函数委托指定排序或二进制搜索中使用的排序规则算法。好的。

    另一种用法类似于将算法应用到C++标准模板库容器中。好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    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中的指针操作。因此,根据c的规则

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

    ****注->功能类型应相同****

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    #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("
     Hello World"
    );
    }

    enter image description here之后,让我们来看看机器是如何理解它们的。

    红色标记区显示地址是如何交换和存储在EAX中的,然后它们是EAX上的调用指令。EAX包含所需的函数地址


    由于函数指针通常是类型化回调,因此您可能需要查看类型安全回调。这同样适用于非回调函数的入口点等。

    C是相当善变和宽容的同时:)