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

倖福魔咒の 提交于 2019-12-11 11:28:47

【推荐】2019 Java 开发者跳槽指南.pdf(吐血整理) >>>

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

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


#1楼

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

当您在不同的时间或不同的开发阶段需要不同的功能时,它们非常方便使用。 例如,我正在具有控制台的主机上开发应用程序,但是该软件的最终版本将放在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我将定义version.h存在的2个函数原型

版本号

#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()函数中的功能,其中根据version.h定义的版本,为zprintf分配了唯一函数(其功能签名匹配)

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中的version.h ,其余的代码将完成!


#2楼

从头开始功能具有从其开始执行的位置的一些内存地址。 在汇编语言中,它们被称为(称为“函数的内存地址”)。现在返回C,如果函数具有内存地址,则它们可以由C中的Pointers操纵。因此,按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包含函数的所需地址。


#3楼

函数指针通常由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;
}

#4楼

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


#5楼

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

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

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

因此从上面我们可以得到一个源代码行,其中将func()函数的地址分配给函数指针变量pFuncpFunc = 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将全局struct与功能指针一起使用

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

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

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 namespace方法中,从库接口中的工厂方法请求特定的库接口,该接口支持创建包含函数指针的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消息。

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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!