为什么我们使用函数指针结构?

Why do we use structure of function pointers?

正如他们所说,您可以从他人的代码中学习编码技术。我一直在尝试了解几个自由堆栈,它们都有一个共同点:函数指针的结构。我有以下与此架构相关的问题。

  1. 这种架构背后有什么具体原因吗?
  2. 通过函数指针调用函数是否有助于任何优化?

示例:

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。但对于编写需要灵活和响应迅速的代码来说,这是一种非常强大的技术。


  1. 我有过早泛化的倾向 - 我正在编写代码来解决我认为五年后会出现的问题,而不是 今天 的问题,我想编写往往比必要的更复杂的代码。

“计算机科学中的一切都可以通过多一层间接来解决。”

函数指针结构“模式”,让我们这样称呼它,允许运行时间选择。 SQLite 无处不在地使用它,例如,为了可移植性。如果您提供一个满足其所需语义的“文件系统”,那么您可以在其上 运行 SQLite,Posix 无处可见。

GnuCOBOL 对索引文件使用相同的想法。 Cobol 定义了 ISAM 语义,程序可以通过指定键从文件中读取记录。底层的名称-值存储可以由几个(可配置的)库提供,它们都提供相同的功能,但对它们的“读取记录”功能使用不同的名称。通过将它们包装为函数指针,Cobol 运行time 支持库可以使用这些键值系统中的任何一个,甚至可以同时使用多个键值系统(当然,对于不同的文件)。