C中的动态方法调度

Din*_*mar 17 c oop function-pointers vtable

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

但有没有办法在C中实现动态方法调度?我考虑过函数指针,但没有得到完整的想法.

我怎么能实现这个?

jxh*_*jxh 34

正如其他人所指出的那样,在C中实现这一点当然是可能的.它不仅是可能的,而且是一种相当普遍的机制.最常用的示例可能是UNIX中的文件描述符接口.read()对文件描述符的调用将调度到特定于提供该文件描述符的设备或服务的读取函数(它是一个文件吗?它是一个套接字吗?它是其他类型的设备吗?).

唯一的技巧是从抽象类型中恢复指向具体类型的指针.对于文件描述符,UNIX使用包含特定于该描述符的信息的查找表.如果使用指向对象的指针,则接口用户持有的指针是"基本"类型,而不是"派生"类型.C没有继承本身,但它确实保证指针的第一个元素struct等于所述含的指针struct.因此,您可以通过使"base"的实例成为"derived"的第一个成员来使用它来恢复"派生"类型.

这是一个带有堆栈的简单示例:

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); }
Run Code Online (Sandbox Code Playgroud)

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

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;
}
Run Code Online (Sandbox Code Playgroud)

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

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;
}
Run Code Online (Sandbox Code Playgroud)

堆栈操作的实现将简单地转换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;
}
Run Code Online (Sandbox Code Playgroud)

当堆栈的用户在堆栈实例上调用堆栈操作时,操作将调度到该堆栈中的相应操作vtable.这vtable由创建函数初始化,其功能与其特定实现相对应.所以:

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

stack_push(s1, 1);
stack_push(s2, 1);
Run Code Online (Sandbox Code Playgroud)

stack_push()s1和两个人都被召唤s2.但是,因为s1,它会派遣到stack_array_push(),而s2它将派遣到stack_list_push().

  • 另外,“vtable”成员通常是“const”,并指向常量内存(当然,如果 MMU/MPU 存在并且操作系统执行正确的操作)。 (2认同)

mea*_*ers 3

C++(最初)构建在 C 之上。第一个 C++ 编译器实际上生成 C 作为中间步骤。因此,是的,这是可能的。

下面是C++ 是如何做这样的事情的。

网上有大量可靠的信息,比我们几分钟内可以输入的还要多。“谷歌一下,你就会找到。”

您在上面的评论中说:

如果有人已经用 c 语言编写了一些代码,但要添加一些功能,那么有人会更喜欢这样做。而不是使用 OO 语言从头开始编写。

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

  • 说这是可能的而不解释如何是一个相当弱的答案。 (6认同)