Подтвердить что ты не робот

ООП и интерфейсы в C

Прямо с летучей мыши. Я понимаю, что ANSI C не является ориентированным на объект языком программирования. Я хочу научиться применять конкретную технику оо с помощью c.

Например, я хочу создать несколько классов звукового эффекта, которые имеют одинаковые имена функций, но разные реализации этих функций.

Если бы я делал это на языке более высокого уровня, я бы сначала написал интерфейс, а затем выполнил его.

AudioEffectInterface

-(float) processEffect 



DelayClass

-(float) processEffect

{
 // do delay code

  return result

}

FlangerClass

-(float) processEffect

{
 // do flanger code

  return result

}



-(void) main

{
   effect= new DelayEffect()
   effect.process()

   effect = new FlangerEffect()
   effect.process()


}

Как я могу достичь такой гибкости, используя C?

4b9b3361

Ответ 1

Существует три различных способа достижения полиморфизма в C:

  • Измените код
    В функциях базового класса просто switch для идентификатора типа класса для вызова специализированных версий. Пример неполного кода:

    typedef enum classType {
        CLASS_A,
        CLASS_B
    } classType;
    
    typedef struct base {
        classType type;
    } base;
    
    typedef struct A {
        base super;
        ...
    } A;
    
    typedef struct B {
        base super;
        ...
    } B;
    
    void A_construct(A* me) {
        base_construct(&me->super);
        super->type = CLASS_A;
    }
    
    int base_foo(base* me) {
        switch(me->type) {
            case CLASS_A: return A_foo(me);
            case CLASS_B: return B_foo(me);
            default: assert(0), abort();
        }
    }
    

    Конечно, это утомительно для больших классов.

  • Сохранить указатели функций в объекте
    Вы можете избежать операторов switch, используя указатель на функцию для каждой функции-члена. Опять же, это неполный код:

    typedef struct base {
        int (*foo)(base* me);
    } base;
    
    //class definitions for A and B as above
    
    int A_foo(base* me);
    
    void A_construct(A* me) {
        base_construct(&me->super);
        me->super.foo = A_foo;
    }
    

    Теперь код вызова может просто сделать

    base* anObject = ...;
    (*anObject->foo)(anObject);
    

    В качестве альтернативы вы можете использовать макрос препроцессора по строкам:

    #define base_foo(me) (*me->foo)(me)
    

    Обратите внимание, что это дважды оценивает выражение me, так что это действительно плохая идея. Это может быть исправлено, но это выходит за рамки этого ответа.

  • Использовать vtable
    Поскольку все объекты класса имеют один и тот же набор функций-членов, все они могут использовать одни и те же указатели. Это очень близко к тому, что делает С++ под капотом:

    typedef struct base_vtable {
        int (*foo)(base* me);
        ...
    } base_vtable;
    
    typedef struct base {
        base_vtable* vtable;
        ...
    } base;
    
    typedef struct A_vtable {
        base_vtable super;
        ...
    } A_vtable;
    
    
    
    //within A.c
    
    int A_foo(base* super);
    static A_vtable gVtable = {
        .foo = A_foo,
        ...
    };
    
    void A_construct(A* me) {
        base_construct(&me->super);
        me->super.vtable = &gVtable;
    };
    

    Опять же, это позволяет коду пользователя выполнять отправку (с одной дополнительной косвенностью):

    base* anObject = ...;
    (*anObject->vtable->foo)(anObject);
    

Какой метод вы должны использовать, зависит от задачи. Подход switch легко взломать для двух или трех небольших классов, но громоздкий для больших классов и иерархий. Второй подход масштабируется намного лучше, но у него много места накладных расходов из-за дублирования указателей на функции. Подход vtable требует довольно немного дополнительной структуры и вводит еще больше косвенности (что делает код более трудным для чтения), но, безусловно, это путь для сложных иерархий классов.

Ответ 2

Можете ли вы пойти на компромисс со следующим:

#include <stdio.h>

struct effect_ops {
    float (*processEffect)(void *effect);
    /* + other operations.. */
};

struct DelayClass {
    unsigned delay;
    struct effect_ops *ops;
};

struct FlangerClass {
    unsigned period;
    struct effect_ops *ops;
};

/* The actual effect functions are here
 * Pointers to the actual structure may be needed for effect-specific parameterization, etc.
 */
float flangerEffect(void *flanger)
{
   struct FlangerClass *this = flanger;
   /* mix signal delayed by this->period with original */
   return 0.0f;
}

float delayEffect(void *delay)
{
    struct DelayClass *this = delay;
    /* delay signal by this->delay */
    return 0.0f;
}

/* Instantiate and assign a "default" operation, if you want to */
static struct effect_ops flanger_operations = {
    .processEffect = flangerEffect,
};

static struct effect_ops delay_operations = {
    .processEffect = delayEffect,
};

int main()
{
    struct DelayClass delay     = {.delay = 10, .ops = &delay_operations};
    struct FlangerClass flanger = {.period = 1, .ops = &flanger_operations};
    /* ...then for your signal */
    flanger.ops->processEffect(&flanger);
    delay.ops->processEffect(&delay);
    return 0;
}

Ответ 3

Вы реализуете интерфейсы с использованием структур указателей функций. Затем вы можете создать структуру интерфейса, встроенную в структуру данных объекта, и передать указатель интерфейса в качестве первого параметра для каждой функции члена интерфейса. В этой функции вы получаете указатель на свой класс контейнера (который специфичен для вашей реализации) с использованием макроса container_of(). Найдите "container_of linux kernel" для реализации. Это очень полезный макрос.