为什么我们使用函数指针结构?
Why do we use structure of function pointers?
正如他们所说,您可以从他人的代码中学习编码技术。我一直在尝试了解几个自由堆栈,它们都有一个共同点:函数指针的结构。我有以下与此架构相关的问题。
- 这种架构背后有什么具体原因吗?
- 通过函数指针调用函数是否有助于任何优化?
示例:
void do_Command1(void)
{
// Do something
}
void do_Command2(void)
{
// Do something
}
方案一:直接执行以上功能
void do_Func(void)
{
do_Command1();
do_Command2();
}
选项2:通过函数指针间接执行上述函数
// Create structure for function pointers
typedef struct
{
void (*pDo_Command1)(void);
void (*pDo_Command2)(void);
}EXECUTE_FUNC_STRUCT;
// Update structure instance with functions address
EXECUTE_FUNC_STRUCT ExecFunc = {
do_Command1,
do_Command2,
};
void do_Func(void)
{
EXECUTE_FUNC_STRUCT *pExecFunc; // Create structure pointer
pExecFun = &ExecFunc; // Assign structure instance address to the structure pointer
pExecFun->pDo_Command1(); // Execute command 1 function via structure pointer
pExecFun->pDo_Command2(); // Execute command 2 function via structure pointer
}
虽然选项 1 易于理解和实施,但为什么我们需要使用选项 2?
最好通过示例进行解释。
示例 1:
假设您想使用 draw() 方法实现一个 Shape class,那么您需要一个函数指针才能实现。
struct Shape {
void (*draw)(struct Shape*);
};
void draw(struct Shape* s) {
s->draw(s);
}
void draw_rect(struct Shape *s) {}
void draw_ellipse(struct Shape *s) {}
int main()
{
struct Shape rect = { .draw = draw_rect };
struct Shape ellipse = { .draw = draw_ellipse };
struct Shape *shapes[] = { &rect, &ellipse };
for (int i=0; i < 2; ++i)
draw(shapes[i]);
}
示例 2:
FILE *file = fopen(...);
FILE *mem = fmemopen(...); /* POSIX */
没有函数指针,就无法实现文件和内存流的通用接口。
附录
好吧,还有另一种方法。基于形状示例:
enum ShapeId {
SHAPE_RECT,
SHAPE_ELLIPSE
};
struct Shape {
enum ShapeId id;
};
void draw(struct Shape *s)
{
switch (s->id) {
case SHAPE_RECT: draw_rect(s); break;
case SHAPE_ELLIPSE: draw_ellipse(s); break;
}
}
第二个示例的优点可能是,编译器可以内联函数,这样您就可以省略函数调用的开销。
While Option 1 is easy to understand and implement, why do we need to use Option 2?
选项 1 不允许您在不更改代码的情况下更改行为 - 每次执行程序时,它将始终以相同的顺序执行相同的功能。有时,这是正确的答案。
选项 2 使您可以灵活地执行不同的功能,或在 do_Command1
之前执行 do_Command2
,基于运行时的决策(比如读取配置文件后,或基于另一个配置文件的结果)操作等)。
来自个人经验的真实示例 - 我正在开发一个应用程序,该应用程序将读取由 Labview 驱动的仪器生成的数据文件并将它们加载到数据库中。有四种不同的仪器,每种仪器有两种类型的文件,一种用于校准,另一种包含实际数据。文件命名约定使我可以 select 基于文件名的解析例程。现在,我可以编写我的代码:
void parse ( const char *fileName )
{
if ( fileTypeIs( fileName, "GRA" ) && fileExtIs( fileName, "DAT" ) )
parseGraDat( fileName );
else if ( fileTypeIs( fileName, "GRA" ) && fileExtIs ( fileName, "CAL" ) )
parseGraCal( fileName );
else if ( fileTypeIs( fileName, "SON" ) && fileExtIs ( fileName, "DAT" ) )
parseSonDat( fileName );
// etc.
}
那会很好用。但是,当时可能会在以后添加新的乐器,并且可能会有其他乐器的文件类型。所以,我决定不使用长的 if-else 链,而是使用查找 table。这样,如果我确实需要添加新的解析例程,我所要做的就是编写新例程并将它的条目添加到查找 table - 我不必修改任何主程序逻辑。 table 看起来像这样:
struct lut {
const char *type;
const char *ext;
void (*parseFunc)( const char * );
} LUT[] = { {"GRA", "DAT", parseGraDat },
{"GRA", "CAL", parseGraCal },
{"SON", "DAT", parseSonDat },
{"SON", "CAL", parseSonCal },
// etc.
};
然后我有一个函数可以获取文件名、搜索查找 table 和 return 适当的解析函数(如果文件名无法识别则为 NULL):
void (*parse)(const char *) = findParseFunc( LUT, fileName );
if ( parse )
parse( fileName );
else
log( ERROR, "No parsing function for %s", fileName );
同样,我没有理由不使用 if-else 链,现在回想起来,这可能是我应该 为那个特定应用程序所做的1。但对于编写需要灵活和响应迅速的代码来说,这是一种非常强大的技术。
- 我有过早泛化的倾向 - 我正在编写代码来解决我认为五年后会出现的问题,而不是 今天 的问题,我想编写往往比必要的更复杂的代码。
“计算机科学中的一切都可以通过多一层间接来解决。”
函数指针结构“模式”,让我们这样称呼它,允许运行时间选择。 SQLite 无处不在地使用它,例如,为了可移植性。如果您提供一个满足其所需语义的“文件系统”,那么您可以在其上 运行 SQLite,Posix 无处可见。
GnuCOBOL 对索引文件使用相同的想法。 Cobol 定义了 ISAM 语义,程序可以通过指定键从文件中读取记录。底层的名称-值存储可以由几个(可配置的)库提供,它们都提供相同的功能,但对它们的“读取记录”功能使用不同的名称。通过将它们包装为函数指针,Cobol 运行time 支持库可以使用这些键值系统中的任何一个,甚至可以同时使用多个键值系统(当然,对于不同的文件)。
正如他们所说,您可以从他人的代码中学习编码技术。我一直在尝试了解几个自由堆栈,它们都有一个共同点:函数指针的结构。我有以下与此架构相关的问题。
- 这种架构背后有什么具体原因吗?
- 通过函数指针调用函数是否有助于任何优化?
示例:
void do_Command1(void)
{
// Do something
}
void do_Command2(void)
{
// Do something
}
方案一:直接执行以上功能
void do_Func(void)
{
do_Command1();
do_Command2();
}
选项2:通过函数指针间接执行上述函数
// Create structure for function pointers
typedef struct
{
void (*pDo_Command1)(void);
void (*pDo_Command2)(void);
}EXECUTE_FUNC_STRUCT;
// Update structure instance with functions address
EXECUTE_FUNC_STRUCT ExecFunc = {
do_Command1,
do_Command2,
};
void do_Func(void)
{
EXECUTE_FUNC_STRUCT *pExecFunc; // Create structure pointer
pExecFun = &ExecFunc; // Assign structure instance address to the structure pointer
pExecFun->pDo_Command1(); // Execute command 1 function via structure pointer
pExecFun->pDo_Command2(); // Execute command 2 function via structure pointer
}
虽然选项 1 易于理解和实施,但为什么我们需要使用选项 2?
最好通过示例进行解释。
示例 1:
假设您想使用 draw() 方法实现一个 Shape class,那么您需要一个函数指针才能实现。
struct Shape {
void (*draw)(struct Shape*);
};
void draw(struct Shape* s) {
s->draw(s);
}
void draw_rect(struct Shape *s) {}
void draw_ellipse(struct Shape *s) {}
int main()
{
struct Shape rect = { .draw = draw_rect };
struct Shape ellipse = { .draw = draw_ellipse };
struct Shape *shapes[] = { &rect, &ellipse };
for (int i=0; i < 2; ++i)
draw(shapes[i]);
}
示例 2:
FILE *file = fopen(...);
FILE *mem = fmemopen(...); /* POSIX */
没有函数指针,就无法实现文件和内存流的通用接口。
附录
好吧,还有另一种方法。基于形状示例:
enum ShapeId {
SHAPE_RECT,
SHAPE_ELLIPSE
};
struct Shape {
enum ShapeId id;
};
void draw(struct Shape *s)
{
switch (s->id) {
case SHAPE_RECT: draw_rect(s); break;
case SHAPE_ELLIPSE: draw_ellipse(s); break;
}
}
第二个示例的优点可能是,编译器可以内联函数,这样您就可以省略函数调用的开销。
While Option 1 is easy to understand and implement, why do we need to use Option 2?
选项 1 不允许您在不更改代码的情况下更改行为 - 每次执行程序时,它将始终以相同的顺序执行相同的功能。有时,这是正确的答案。
选项 2 使您可以灵活地执行不同的功能,或在 do_Command1
之前执行 do_Command2
,基于运行时的决策(比如读取配置文件后,或基于另一个配置文件的结果)操作等)。
来自个人经验的真实示例 - 我正在开发一个应用程序,该应用程序将读取由 Labview 驱动的仪器生成的数据文件并将它们加载到数据库中。有四种不同的仪器,每种仪器有两种类型的文件,一种用于校准,另一种包含实际数据。文件命名约定使我可以 select 基于文件名的解析例程。现在,我可以编写我的代码:
void parse ( const char *fileName )
{
if ( fileTypeIs( fileName, "GRA" ) && fileExtIs( fileName, "DAT" ) )
parseGraDat( fileName );
else if ( fileTypeIs( fileName, "GRA" ) && fileExtIs ( fileName, "CAL" ) )
parseGraCal( fileName );
else if ( fileTypeIs( fileName, "SON" ) && fileExtIs ( fileName, "DAT" ) )
parseSonDat( fileName );
// etc.
}
那会很好用。但是,当时可能会在以后添加新的乐器,并且可能会有其他乐器的文件类型。所以,我决定不使用长的 if-else 链,而是使用查找 table。这样,如果我确实需要添加新的解析例程,我所要做的就是编写新例程并将它的条目添加到查找 table - 我不必修改任何主程序逻辑。 table 看起来像这样:
struct lut {
const char *type;
const char *ext;
void (*parseFunc)( const char * );
} LUT[] = { {"GRA", "DAT", parseGraDat },
{"GRA", "CAL", parseGraCal },
{"SON", "DAT", parseSonDat },
{"SON", "CAL", parseSonCal },
// etc.
};
然后我有一个函数可以获取文件名、搜索查找 table 和 return 适当的解析函数(如果文件名无法识别则为 NULL):
void (*parse)(const char *) = findParseFunc( LUT, fileName );
if ( parse )
parse( fileName );
else
log( ERROR, "No parsing function for %s", fileName );
同样,我没有理由不使用 if-else 链,现在回想起来,这可能是我应该 为那个特定应用程序所做的1。但对于编写需要灵活和响应迅速的代码来说,这是一种非常强大的技术。
- 我有过早泛化的倾向 - 我正在编写代码来解决我认为五年后会出现的问题,而不是 今天 的问题,我想编写往往比必要的更复杂的代码。
“计算机科学中的一切都可以通过多一层间接来解决。”
函数指针结构“模式”,让我们这样称呼它,允许运行时间选择。 SQLite 无处不在地使用它,例如,为了可移植性。如果您提供一个满足其所需语义的“文件系统”,那么您可以在其上 运行 SQLite,Posix 无处可见。
GnuCOBOL 对索引文件使用相同的想法。 Cobol 定义了 ISAM 语义,程序可以通过指定键从文件中读取记录。底层的名称-值存储可以由几个(可配置的)库提供,它们都提供相同的功能,但对它们的“读取记录”功能使用不同的名称。通过将它们包装为函数指针,Cobol 运行time 支持库可以使用这些键值系统中的任何一个,甚至可以同时使用多个键值系统(当然,对于不同的文件)。