题
我最近对C中的函数指针有一些经验。
所以继续回答你自己的问题的传统,我决定做一个非常基础的小总结,对于那些需要快速深入到这个主题的人。
解决方案
C语言中的函数指针
让我们从一个基本功能开始,我们将指向: 通用标签
首先,让我们定义一个指向接收2个int
并返回一个int
的函数的指针:
通用标签
现在我们可以安全地指向我们的功能了: 通用标签
现在我们有一个指向函数的指针,让我们使用它: 通用标签
将指针传递给另一个函数基本上是相同的: 通用标签
我们也可以在返回值中使用函数指针(尝试保持一致,它会变得凌乱): 通用标签
但是使用typedef
会更好:
通用标签
其他提示
C中的函数指针可用于在C中执行面向对象的编程。
例如,以下几行用C编写: 通用标签
是的,->
的存在和缺少new
运算符是一个致命的放弃,但它似乎肯定意味着我们正在将某些String
类的文本设置为"hello"
。
通过使用函数指针,可以在C语言中模拟方法。
这是如何完成的?
String
类实际上是一个struct
,它带有一堆函数指针,这些指针充当模拟方法的一种方式。以下是String
类的部分声明:
通用标签
可以看出,String
类的方法实际上是指向已声明函数的函数指针。在准备String
实例时,将调用newString
函数,以建立指向它们各自功能的函数指针:
通用标签
例如,通过调用getString
方法调用的get
函数定义如下:
通用标签
可以注意到的一件事是,没有对象实例的概念,并且没有实际上是对象一部分的方法,因此必须在每次调用时传递“自身对象”。 (而且internal
只是一个隐藏的struct
,它在前面的代码清单中被省略了,它是一种执行信息隐藏的方法,但是与函数指针无关。)
因此,除了能够执行s1->set("hello");
之外,还必须传递对象以对s1->set(s1, "hello")
执行操作。
由于该次要说明中必须包含对自己的引用,因此我们将转到下一部分,即 C的继承。
比方说,我们想做一个String
的子类,比如ImmutableString
。为了使字符串不可变,将无法访问set
方法,同时保持对get
和length
的访问,并强制“构造函数”接受char*
:
通用标签
基本上,对于所有子类,可用的方法再次是函数指针。这次,set
方法的声明不存在,因此,不能在ImmutableString
中调用它。
对于ImmutableString
的实现,唯一相关的代码是“构造函数”函数newImmutableString
:
通用标签
在实例化ImmutableString
时,指向get
和length
方法的函数指针实际上通过内部存储的String.get
对象的String.length
变量引用了base
和String
方法。
使用函数指针可以从超类继承方法。
我们可以继续在C中实现多态性。
例如,如果由于某种原因我们想更改length
方法的行为以始终返回0
类中的ImmutableString
,那么要做的就是:
- 添加一个函数,该函数将用作最重要的
length
方法。 - 转到“构造函数”,并将函数指针设置为覆盖的
length
方法。可以通过添加
length
在ImmutableString
中添加主要的lengthOverrideMethod
方法: 通用标签然后,将构造函数中的
length
方法的函数指针连接到lengthOverrideMethod
: G
现在,length
类中的ImmutableString
方法的行为不同于String
类的行为,现在,length
方法将引用lengthOverrideMethod
函数中定义的行为。
我必须添加一个免责声明,即我仍在学习如何使用C进行面向对象的编程风格的编写,因此可能有些地方我并没有很好地解释,或者可能就最好的方面而言是错误的在C中实现OOP。但是我的目的是试图说明函数指针的许多用法。
有关如何在C语言中执行面向对象编程的更多信息,请参考以下问题:
被解雇的指南:如何通过手工编译代码来滥用x86机器上GCC中的函数指针:
这些字符串文字是32位x86机器代码的字节。 0xC3
是 x86 ret
指令。
您通常不会手工编写这些代码,而是使用汇编语言编写,然后使用诸如nasm
之类的汇编程序将其汇编为一个平面二进制文件,然后将其十六进制转储为C字符串文字。
-
返回EAX寄存器上的当前值
int eax = ((int(*)())("\xc3 <- This returns the value of the EAX register"))();
-
编写交换函数
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,每次调用一些函数
((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的递归函数
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()
系统调用,就像您需要动态分配的内存一样。 (或者作为快速hack,gcc -z execstack
将程序与堆栈+数据段+堆可执行文件链接在一起。)
要反汇编这些文件,可以对其进行编译以在字节上放置标签,然后使用反汇编程序。 通用标签
使用
gcc -c -m32 foo.c
进行编译并通过objdump -D -rwC -Mintel
进行反汇编,我们可以得到汇编,并发现该代码通过破坏EBX(调用保留的寄存器)而违反了ABI,并且通常效率低下。 通用标签此机器代码将(可能)在Windows,Linux,OS X等上以32位代码运行:所有这些OS上的默认调用约定将args传递给堆栈,而不是更有效地在寄存器中传递。但是EBX在所有普通的呼叫约定中都被保留呼叫,因此将其用作临时寄存器而不保存/恢复它很容易使呼叫者崩溃。
我最喜欢的函数指针用法之一是便宜又简单的迭代器- 通用标签
一旦有了基本的声明器,函数指针就变得易于声明:
- id:
ID
: ID是一个 - 指针:
*D
: D指向 的指针 - 函数:
D(<parameters>)
: D函数采用<
参数返回>
虽然D是使用相同规则构建的另一个声明符。最后,它在某个地方以
ID
结尾(请参见下面的示例),这是已声明实体的名称。让我们尝试构建一个函数,该函数使用一个指向不带任何内容并返回int的函数的指针,并返回一个指向带一个char并返回int的函数的指针。使用type-def就是这样 通用标签如您所见,使用typedef构建它非常容易。如果没有typedef,使用上述声明符规则(始终应用)也不难。如您所见,我错过了指针指向的部分以及函数返回的内容。那是在声明的最左边出现的内容,并且没有意义:如果已经建立了声明器,则将其添加到末尾。来做吧。始终如一地建立起来,首先要罗--使用
[
和]
显示结构: 通用标签如您所见,可以通过一个接一个的添加声明符来完全描述一个类型。可以通过两种方式进行构建。一是自下而上,从最正确的事情(叶子)开始,一直到标识符。另一种方法是自顶向下,从标识符开始,一直到树叶。我会展示两种方式。
自下而上
构造从右边的东西开始:返回的东西,即使用char的函数。为了使声明符与众不同,我将对它们进行编号: 通用标签
直接插入char参数,因为它很简单。通过将
D1
替换为*D2
,可以向声明符添加指针。请注意,我们必须在*D2
周围加上括号。这可以通过查找*-operator
和函数调用操作符()
的优先级来知道。没有我们的括号,编译器会将其读取为*(D2(char p))
。但是,那当然不再是用*D2
普通替换D1了。声明符周围总是允许有括号。因此,实际上,如果添加太多,就不会出错。 通用标签返回类型完成!现在,让我们用函数声明符 function取回
D2
来替换<parameters>
,这就是我们现在使用的D3(<parameters>)
。 通用标签请注意,不需要括号,因为我们想要
D3
这次是函数声明符,而不是指针声明符。太好了,剩下的就是它的参数了。该参数的执行方式与返回类型完全相同,只是将char
替换为void
。所以我将其复制: 通用标签由于我们已经完成了该参数(因为它已经是一个指向函数的指针-不需要其他声明符),因此我已将
D2
替换为ID1
。ID1
将是参数的名称。现在,我在上面最后告诉我们,添加一个所有这些声明器都修改的类型-一种出现在每个声明的最左侧。对于函数,这将成为返回类型。对于指向类型的指针等...写下类型很有趣,它会以相反的顺序出现在最右边:)无论如何,代之以产生完整的声明。两次都当然会生成标签代码。 通用标签在该示例中,我称为函数
int
的标识符。 <
这从类型描述的最左边的标识符开始,在我们从右边经过时包装该声明符。从函数开始,使用ID0
参数返回<
返回
通用标签
描述中的下一件事情(“返回”之后)是指向的指针。让我们合并它: 通用标签
然后接下来是 functon接受了>
参数和<
返回。该参数是一个简单的char,由于它确实很简单,因此我们立即将其重新放置。
通用标签
请注意我们添加的括号,因为我们再次希望首先绑定基因基因标签代码,然后然后结合基因基因标签代码。否则,它将读取函数,并带有>
parameters*
返回函数... 。是的,甚至不允许返回函数。
现在,我们只需要输入(char)
parameters<
。我将展示衍生的一个简短版本,因为我认为您现在已经知道如何执行此操作。
通用标签
只需像我们自下而上那样将>
放在声明符之前,就可以完成
通用标签
好东西
自下而上或自上而下更好吗?我习惯于自下而上,但有些人可能更习惯自上而下。我认为这是一个品味问题。顺便说一句,如果在该声明中应用所有运算符,最终将得到一个int:
通用标签
这是C语言中声明的一个不错的属性:声明断言,如果在使用标识符的表达式中使用这些运算符,则它会在最左边产生类型。数组也是如此。
希望您喜欢这个小教程!现在,当人们想知道函数的奇怪声明语法时,我们可以链接到此。我试图尽可能少地使用C内部构件。随意编辑/修复其中的内容。
函数指针的另一个很好的用途:
无痛地在版本之间切换
当您在不同的时间或不同的开发阶段需要不同的功能时,它们非常方便。例如,我正在一台有控制台的主机上开发一个应用程序,但该软件的最终版本将放在Avnet ZedBoard上(它有显示器和控制台的端口,但最终版本不需要/需要它们)。所以在开发过程中,我会使用 printf
以查看状态和错误消息,但是当我完成后,我不希望打印任何内容。这就是我所做的:
版本。h
// 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
版本。c
#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只是返回,不会运行不必要的代码
运行代码将如下所示:
主进程。c
#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
剩下的就由代码来完成!
函数指针通常由typedef
定义,并用作参数和返回值。
以上答案已经解释了很多,我仅举一个完整的例子: 通用标签
C中函数指针的一大用途是调用在运行时选择的函数。例如,C运行时库具有两个例程, qsort
和bsearch
,它指向一个函数的指针,该函数被调用来比较两个项目排序这使您可以根据希望使用的任何标准分别对任何内容进行排序或搜索。
一个非常基本的示例,如果有一个名为print(int x, int y)
的函数又可能需要调用一个函数(add()
或sub()
,它们属于同一类型),那么我们将执行的操作将添加一个函数指针参数到print()
函数,如下所示:
通用标签
输出为:
值是:410
值是:390
函数指针是包含函数地址的变量。由于它是一个指针变量,但是具有某些受限制的属性,因此您可以像使用数据结构中的任何其他指针变量一样使用它。
我能想到的唯一例外是将函数指针视为指向单个值以外的东西。通过递增或递减函数指针或对函数指针增加/减去偏移量来进行指针算术并不是真正的实用程序,因为函数指针仅指向单个对象,即函数的入口点。
函数指针变量的大小,该变量占用的字节数可能会根据基础架构而有所不同,例如x32或x64或其他任何版本。
函数指针变量的声明需要指定与函数声明相同的信息,以便C编译器执行通常执行的检查。如果未在函数指针的声明/定义中指定参数列表,则C编译器将无法检查参数的使用。在某些情况下,缺乏检查很有用,但是请记住,已经删除了安全网。
一些例子: 通用标签
前两个声明有点类似:
-
func
是一个函数,它接受一个int
和一个char *
并返回一个int
-
pFunc
是一个函数指针,为该函数分配了一个函数的地址,该函数采用int
和char *
并返回int
因此从上面我们可以看到一个源代码行,其中将
func()
函数的地址分配给了函数指针变量pFunc
,就像pFunc = func;
一样。请注意函数指针声明/定义所使用的语法,其中括号用于克服自然运算符优先级规则。 通用标签
几个不同的用法示例
使用函数指针的一些示例: 通用标签
您可以在函数指针的定义中使用可变长度参数列表。 通用标签
或者根本无法指定参数列表。这可能很有用,但它消除了C编译器对提供的参数列表执行检查的机会。 通用标签
C型演员表
您可以将C样式强制转换与函数指针一起使用。但是请注意,C编译器可能对检查比较松懈,或者提供警告而不是错误。 通用标签
比较功能指针是否相等
您可以使用
if
语句检查函数指针是否等于特定函数地址,尽管我不确定这样做是否有用。其他比较运算符似乎没有什么用。 通用标签功能指针数组
如果您要有一个函数指针数组,每个参数列表中的每个元素都有不同,则可以定义一个函数指针,其中参数列表未指定(不是
void
,这意味着没有参数,只是未指定),如下所示以下内容,尽管您可能会看到C编译器的警告。这也适用于指向函数的函数指针参数: 通用标签使用带有功能指针的全局
namespace
的C风格struct
您可以使用
static
关键字指定名称为文件范围的函数,然后将其分配给全局变量,以提供类似于C +的namespace
功能的方式
在头文件中定义一个结构,它将成为我们的命名空间以及使用它的全局变量。 通用标签
然后在C源文件中: 通用标签
然后将通过指定全局struct变量的完整名称和成员名称来使用它来访问该函数。将const
修饰符用于全局变量,以免意外更改。
通用标签
功能指针的应用领域
DLL库组件可以执行类似于C样式的namespace
方法,在该方法中,从库接口中的工厂方法请求特定的库接口,该接口支持创建包含函数指针的struct
。请求的DLL版本,使用必要的函数指针创建一个结构,然后将该结构返回给发出请求的调用方以供使用。
通用标签
,它可以用于: 通用标签
可以使用相同的方法为使用底层硬件的特定模型的代码定义抽象硬件层。工厂用功能特定的功能填充功能指针,以提供实现特定的硬件模型中指定功能的功能。这可用于提供由软件使用的抽象硬件层,该软件调用工厂函数以获取特定的硬件功能接口,然后使用提供的功能指针对基础硬件执行操作,而无需了解有关特定目标的实现细节
用于创建委托,处理程序和回调的功能指针
您可以使用函数指针来委派某些任务或功能。 C语言中的经典示例是与标准C库函数qsort()
和bsearch()
一起使用的比较委托函数指针,以提供排序项目列表或对项目排序列表执行二进制搜索的排序顺序。比较函数委托指定用于排序或二进制搜索的排序规则算法。
另一种用法类似于将算法应用于C ++标准模板库容器。 通用标签
另一个示例是GUI源代码,其中通过提供事件发生时实际调用的函数指针来注册特定事件的处理程序。 Microsoft MFC框架及其消息映射使用类似的方法来处理传递到窗口或线程的Windows消息。
需要回调的异步函数类似于事件处理程序。异步函数的用户调用异步函数以启动某些动作,并提供一个函数指针,一旦动作完成,异步函数将调用该指针。在这种情况下,事件是完成任务的异步功能。
从头开始功能具有从其开始执行的位置的一些内存地址。在汇编语言中,它们被称为(称为“函数的内存地址”)。现在返回C,如果函数具有一个内存地址,则它们可以由C中的Pointers操纵。因此,按C规则
1。首先需要声明一个指向函数的指针 2.传递所需功能的地址
****注意->函数应为同一类型****
这个简单的程序将说明一切。 通用标签
之后,让我们看看机器如何理解它们。上述程序的机器指令摘要在32位架构中。
红色标记区域显示如何交换地址并将其存储在eax中。然后,它们是eax上的呼叫指令。eax包含所需的功能地址。
由于函数指针通常是类型化的回调,因此您可能想看看类型安全的回调。这同样适用于非回调函数的入口点等。
C很善变,同时又很宽容:)