在C语言中的动态方法调用

26

我知道这听起来很傻,也知道C语言不是面向对象的语言。

但是有没有办法在C语言中实现动态方法调度? 我考虑过使用函数指针,但不能完全理解这个想法。

我应该如何实现呢?


5
您可以通过模仿其他语言“底层”实现的方式来做到这一点。例如,在c ++中,调度是通过查找函数指针表中的索引来执行的。当您创建子类时,将您的传送器添加到表的末尾。您可以使用函数传输结构体来表示表格,并在继承时将父类的结构体放在您的结构体开头。 - Will
1
让我们通过讲述您在应用程序中想要实现的目标来进行实际讨论。然后,我们可以提供代码示例和可行的想法。您当前的问题相当模糊,可以在15分钟内通过Google回答。 - meaning-matters
1
你认为回调函数是动态方法分派吗? - jxh
1
看起来您需要阅读《ANSI C面向对象编程》一文,链接在这里:http://lazarenko.me/2013/03/20/object-oriented-programming-with-ansi-c/。 - user405725
2
两点:Objective-C是C的严格超集,并作为专门的预处理器实现;其次,一些库使用裸C和函数指针结构以及数据成员非常好地完成了面向对象编程...例如serf http库。 - Grady Player
显示剩余3条评论
4个回答

49

正如其他人所指出的那样,用 C 语言实现这个机制是完全可能的。而且,这还是相当常见的一种机制。最常用的例子可能就是 UNIX 系统中的文件描述符接口。对一个文件描述符进行 read() 调用时,会将其派发到具体设备或服务提供的 read 函数上(它是文件、套接字还是某种其他类型的设备?)。

唯一的诀窍在于从抽象类型中恢复具体类型的指针。对于文件描述符,UNIX 使用一个包含特定于该描述符信息的查找表。如果你正在使用对象的指针,则由接口用户持有的指针是“基础”类型的,而不是“派生”类型的。C 语言没有继承机制,但它确保一个结构体的第一个元素的指针等于包含该结构体的指针。因此,您可以使用这一点来通过让“基础”实例成为“派生”的第一个成员来恢复“派生”类型。

以下是一个简单的栈示例:

struct Stack {
    const struct StackInterface * const vtable;
};

struct StackInterface {
    int (*top)(struct Stack *);
    void (*pop)(struct Stack *);
    void (*push)(struct Stack *, int);
    int (*empty)(struct Stack *);
    int (*full)(struct Stack *);
    void (*destroy)(struct Stack *);
};

inline int stack_top (struct Stack *s) { return s->vtable->top(s); }
inline void stack_pop (struct Stack *s) { s->vtable->pop(s); }
inline void stack_push (struct Stack *s, int x) { s->vtable->push(s, x); }
inline int stack_empty (struct Stack *s) { return s->vtable->empty(s); }
inline int stack_full (struct Stack *s) { return s->vtable->full(s); }
inline void stack_destroy (struct Stack *s) { s->vtable->destroy(s); }

现在,如果我想使用固定大小的数组来实现堆栈,可以像这样操作:

struct StackArray {
    struct Stack base;
    int idx;
    int array[STACK_ARRAY_MAX];
};
static int stack_array_top (struct Stack *s) { /* ... */ }
static void stack_array_pop (struct Stack *s) { /* ... */ }
static void stack_array_push (struct Stack *s, int x) { /* ... */ }
static int stack_array_empty (struct Stack *s) { /* ... */ }
static int stack_array_full (struct Stack *s) { /* ... */ }
static void stack_array_destroy (struct Stack *s) { /* ... */ }
struct Stack * stack_array_create () {
    static const struct StackInterface vtable = {
        stack_array_top, stack_array_pop, stack_array_push,
        stack_array_empty, stack_array_full, stack_array_destroy
    };
    static struct Stack base = { &vtable };
    struct StackArray *sa = malloc(sizeof(*sa));
    memcpy(&sa->base, &base, sizeof(base));
    sa->idx = 0;
    return &sa->base;
}

如果我想使用列表来实现一个栈:

struct StackList {
    struct Stack base;
    struct StackNode *head;
};
struct StackNode {
    struct StackNode *next;
    int data;
};
static int stack_list_top (struct Stack *s) { /* ... */ }
static void stack_list_pop (struct Stack *s) { /* ... */ }
static void stack_list_push (struct Stack *s, int x) { /* ... */ }
static int stack_list_empty (struct Stack *s) { /* ... */ }
static int stack_list_full (struct Stack *s) { /* ... */ }
static void stack_list_destroy (struct Stack *s) { /* ... */ }
struct Stack * stack_list_create () {
    static const struct StackInterface vtable = {
        stack_list_top, stack_list_pop, stack_list_push,
        stack_list_empty, stack_list_full, stack_list_destroy
    };
    static struct Stack base = { &vtable };
    struct StackList *sl = malloc(sizeof(*sl));
    memcpy(&sl->base, &base, sizeof(base));
    sl->head = 0;
    return &sl->base;
}

堆栈操作的实现将简单地将 struct Stack *转换为它知道应该是什么。例如:

static int stack_array_empty (struct Stack *s) {
    struct StackArray *sa = (void *)s;
    return sa->idx == 0;
}

static int stack_list_empty (struct Stack *s) {
    struct StackList *sl = (void *)s;
    return sl->head == 0;
}

当堆栈的用户在堆栈实例上调用堆栈操作时,该操作将分派到vtable中相应的操作。 这个vtable是由创建函数初始化的,其中包含与其特定实现相对应的函数。因此:

Stack *s1 = stack_array_create();
Stack *s2 = stack_list_create();

stack_push(s1, 1);
stack_push(s2, 1);

stack_push()会被同时调用在s1s2上。但是,对于s1,它将分派到stack_array_push(),而对于s2,它将分派到stack_list_push()


2
此外,vtable 成员通常是 const,并指向一个常量内存(当然,如果有 MMU/MPU 并且操作系统做得对的话)。 - user405725
你写成了: struct StackList *sl = malloc(sizeof(*sl)); 但我认为应该是: struct StackList *sl = malloc(sizeof(*StackList)); - LORDTEK
@LORDTEK 我向您保证,我所写的是正确的。 - jxh
在Stack结构中,你使用了指针来保存函数指针,而不是在结构中定义函数指针。这样做只是为了节省内存吗? - undefined
@jxh 那是一个很好的解释,谢谢。 - undefined
显示剩余3条评论

3

C++最初是在C的基础上构建的。最早的C++编译器实际上是生成C作为中间步骤的。因此,是的,这是可能的。

这里介绍了C++如何实现这样的功能。

有很多可靠的在线信息可用,远远超过我们在几分钟内可以一起打字的数量。 "谷歌一下你就会发现。"

你在上面的评论中说:

如果有人已经在C中编写了一些代码,但需要添加一些功能,他们可能更喜欢这种方法,而不是从头开始使用OO语言编写。

要在C中拥有这样的功能,您基本上需要重新实现OO语言功能。让人们使用这种新的OO方法是影响可用性的最大因素。换句话说,通过创建另一种重用方法,您实际上会使事情变得不太可重用。


6
只说“可能”而不解释其方式是一个相当薄弱的回答。 - Chris Stratton
1
@ChrisStratton 我同意,我会扩展。 - meaning-matters
我尝试了在线搜索,但没有找到有效的结果。只显示了C++和Java的概念! - Dineshkumar

3

可以轻松实现。您需要使用函数指针数组,然后使用这些函数指针进行调用。如果您想要“覆盖”函数,只需将相应的槽设置为指向新函数即可。这正是C++实现虚函数的方式。


1
这是一个不错的开始 - 但你还需要跟踪你的数据类型,以便知道使用哪个版本。 - Chris Stratton
在http://www.cs.rit.edu/~ats/books/ooc.pdf‎上可以找到C语言面向对象编程的相当全面的描述。 - cmaster - reinstate monica

0

我有点惊讶,没有人将glib和/或整个gtk作为示例添加进来。请查看:http://www.gtk.org/features.php

截至2021年7月7日的有效链接:https://developer.gnome.org/glib/2.26/

我知道使用gtk需要相当多的样板代码,并且第一次使用时不太容易掌握。但是,如果你已经使用过它,它就非常显著。唯一要记住的是在函数的第一个参数中使用一种“对象”。但是,如果您浏览API,您会发现它无处不在。在我看来,这真的是一个很好的例子,可以展示面向对象编程的优点和问题。


链接未找到!你能否用新的编辑它? - Vishwajith.K

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接