Pull to refresh

Object oriented C

Reading time4 min
Views30K
Вам дали задание написать программу на С, а вы уже забыли как может работать программа, в тексте которой нет ни одного слова class или virtual? Или может быть вы влюблены в простоту и строгость ANSI C, но иногда вам не хватает объектно-ориентированных свойств языков более высокого уровня? Или просто интересно взглянуть на старый добрый С с немного другой стороны? В любом случае в данной статье я покажу несколько простых приемов, с помощью которых на C вполне можно думать и писать объекто-ориентированно.

Допустим нам нужно написать фукцию сжимающую данные. Данные могут сжиматься разными алгоритмами. При смене алгоритмов код вызывающей функции не должен меняться. Желательно также чтобы алгоритмы можно было менять «на лету».
На С++ это выглядело бы примерно так:

class compressor
{
  public:
    virtual void compress(void* data)=0;
    virtual void uncompress(void* data)=0;
    virtual void status()const=0;
};

class rar_compressor : public compressor
{...};

class zip_compressor : public compressor
{...};

void do_compress(compressor* c, void* in_data, void* out_data)
{
  c->compress(in_data);
  c->uncompress(out_data);
  c->status();
}


Или так:

template<typename Cmp>
void do_compress(Cmp& c, void* in_data, void* out_data)
{
  c.compress(in_data);
  c.uncompress(out_data);
  c.status();
}

Попробуем реализовать эту схему на С.

#compressor_interface.h

typedef struct compressor_t
{
  // `public` interface
  void (*compress)(struct compressor_t*, void* data);
  void (*uncompress)(struct compressor_t*, void* data);
  void (*status)(struct compressor_t*);
  // `private` part
  void* impl_;
} compressor_t;


Как видите, пользователю доступен только открытый интерфейс, все детали реализации
скрыты за `impl_`.
Реализуем конкретные «классы».
#rar.h
void rar_init(compressor_t* c, int cr);
void rar_free(compressor_t* c);


#rar.c

typedef struct
{
  int compressed_ratio;
  int error;
  int time_to_finish;
} rar_impl_t;

////////////////////////////////////////////////////////////////////////////////

// Удобный макрос для повторяющегося кода
#define PREPARE_IMPL(c) \
  assert(c); \
  assert(c->impl_); \
  rar_impl_t* impl = (rar_impl_t*)c->impl_;

////////////////////////////////////////////////////////////////////////////////

static void compress(compressor_t* c, void* data)
{
  PREPARE_IMPL(c)
  printf("RAR: compressor working. Compressed ratio: %d\n",
          impl->compressed_ratio);
}

////////////////////////////////////////////////////////////////////////////////

static void uncompress(compressor_t* c, void* data)
{
  PREPARE_IMPL(c)
  printf("RAR: uncompressor working. Will be finished in %d.\n",
          impl->time_to_finish);
}

////////////////////////////////////////////////////////////////////////////////

static void status(compressor_t* c)
{
  PREPARE_IMPL(c)
  printf("Compressed ratio: %d, error: %d\n", impl->compressed_ratio,
         impl->error);
}

////////////////////////////////////////////////////////////////////////////////

// Конструктор
void rar_init(compressor_t* c, int cr)
{
  assert(c);
  c->impl_ = malloc(sizeof(rar_impl_t));
  rar_impl_t* impl = (rar_impl_t*)c->impl_;
  
  // Инициализируем `private` члены
  impl->time_to_finish = 5;
  impl->compressed_ratio = cr;
  impl->error = 0;

  // И `public` функции
  c->compress = &compress;
  c->uncompress = &uncompress;
  c->status = &status;
}

////////////////////////////////////////////////////////////////////////////////


//Деструктор
void rar_free(compressor_t* c)
{
  PREPARE_IMPL(c)
  free(impl);
}


«Класс» zip устроен таким же образом, поэтому приводить его код тут не буду. Единственно, что хотелось бы подчеркнуть «конструкторы» этих классов, в отличие от функций-членов, не обязаны иметь одинаковую сигнатуру, более того, их может быть несколько для каждого «класса».

И теперь код использования:

void work(compressor_t* c, void* data_to_compress, void* data_to_decompress)
{
  c->compress(c, data_to_compress);
  c->uncompress(c, data_to_decompress);
  c->status(c);
}

int main()
{
    void* uncompressed_data[DATA_SIZE];
    void* compressed_data[DATA_SIZE];

    compressor_t rar;
    rar_init(&rar, COMP_RATIO);
    work(&rar, uncompressed_data, compressed_data);
    rar_free(&rar);

    printf("\n");

    compressor_t zip;
    zip_init(&zip, VOLUMES);
    work(&zip, uncompressed_data, compressed_data);
    zip_free(&zip);

    return 0;
}


Проницательный читатель наверняка уже заметил, что в принципе можно было и не создавать второй объект compressor_t zip, а использовать существующий rar, и более того, при желании мы можем подменить одну из функций существующего объекта «на лету». Это делает схему более гибкой чем классические классы, например, в С++. Также вы наверняка заметили, что этот подход является практически классической
реализацией паттерна «Стратегия».

Так хорошо, с двумя из трех составляющих ООП (вы конечно же помните их наизусть?) мы разобрались. Осталось последняя — наследование. В принципе, наследование можно реализовать многими разными способами, я опишу здесь тот, который мне представляется наиболее элегантным.

Реализация опирается на особенность С, позволяющую использовать анонимные члены-структуры в качестве членов других структур (в gcc вам, возможно, потребуется поставить флаг "-std=c11" или "-fms-extensions"). Напишем «класс», «наследующий» от zip compressor'a

typedef struct logging_compressor_t
{
  union
  {
    struct compressor_t;
    struct compressor_t base;
  };
  void (*log)(struct logging_compressor_t* lc);
  void *impl_; // Если нужно
} logging_compressor_t;


Теперь мы можем получить доступ к «базовому» «классу» как напрямую
logging_compressor_t lc;
lc.compress(...)

так и через член данных
lc.base.compress(...)


Использовать же данный «класс» полиморфно в существующем коде можно следующим образом

work(&lc.base, uncompressed_data, compressed_data);


Конечно, возразит мне, дотошный читатель, в такой схеме многое держится на аккуратности программиста. Например, можно все испортить вызвав «деструктор» zip для rar и наоборот, и наделать много других глупостей. Но хочу напомнить, что во-первых С-программист и должен быть аккуратным, а во-вторых целью статьи не было показать то, что С «круче» других языков, а лишь то, что при желании в старом добром С можно с успехом использовать концепции из более высокоуровневых языков для поддержки разнообразных стилей программирования.
Tags:
Hubs:
+43
Comments35

Articles