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

ООП в С, неявно передают себя как параметр

Я работал над примером для изучения ООП в C. В настоящее время я придумал этот код, который работает, однако я заинтересован в том, чтобы методы неявно передавали себя как параметр.

#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>
//#include "Stopwatch.h"


typedef struct stopwatch_s
{
    unsigned int milliseconds;
    unsigned int seconds;
    unsigned int minutes;
    unsigned int hours;

    bool is_enabled;

    void ( *tick )      ( struct stopwatch_s* );
    void ( *start )     ( struct stopwatch_s* );
    void ( *stop )      ( struct stopwatch_s* );
    void ( *reset )     ( struct stopwatch_s* );
} stopwatch_t;

static void tick (stopwatch_t* _self)
{
    stopwatch_t * self = _self;
    if (self->is_enabled)
    {
        self->milliseconds++;
        if (self->milliseconds >= 1000)
        {
            self->milliseconds = 0;
            self->seconds++;

            if (self->seconds >= 60)
            {
                self->seconds = 0;
                self->minutes++;

                if (self->minutes >= 60)
                {
                    self->minutes = 0;
                    self->hours++;
                }
            }
        }
    }
}

static void start (stopwatch_t* _self)
{
    stopwatch_t * self = _self;
    self->is_enabled = true;
}

static void stop (stopwatch_t* _self)
{
    stopwatch_t * self = _self;
    self->is_enabled = false;
}

static void reset (stopwatch_t* _self)
{
    stopwatch_t * self = _self;
    self->is_enabled = false;
    self->milliseconds = 0;
    self->seconds = 0;
    self->minutes = 0;
    self->hours = 0;
}

void * new_stopwatch()
{
    stopwatch_t * newInstance = (stopwatch_t *)calloc(1, sizeof(stopwatch_t));
    newInstance->is_enabled = false;
    newInstance->milliseconds = 0;
    newInstance->seconds = 0;
    newInstance->minutes = 0;
    newInstance->hours = 0;
    newInstance->tick = &tick;
    newInstance->start = &start;
    newInstance->stop = &stop;
    newInstance->reset = &reset;

    return newInstance;
}

void main()
{
    struct stopwatch_s * Stopwatch = new_stopwatch();
    printf ("Initial: %d\n", Stopwatch->milliseconds);
    Stopwatch->start (Stopwatch);
    Stopwatch->tick (Stopwatch);
    Stopwatch->tick (Stopwatch);
    Stopwatch->tick (Stopwatch);
    printf ("Started: %d\n", Stopwatch->milliseconds);
    Stopwatch->stop (Stopwatch);
    Stopwatch->tick (Stopwatch);
    printf ("Stopped: %d\n", Stopwatch->milliseconds);
    Stopwatch->reset (Stopwatch);
    printf ("Reset: %d\n", Stopwatch->milliseconds);    
}

Я пробовал читать и после объектно-ориентированное программирование с использованием ANSI-C, но не могу обернуть голову тем, как структурировать мои "объект", поэтому вместо

Stopwatch->tick(Stopwatch);

Я могу написать

Stopwatch->tick();
4b9b3361

Ответ 1

Я не могу оборачивать голову тем, как структурировать свой "объект", поэтому вместо

Stopwatch->tick(Stopwatch);

Я могу написать Stopwatch->tick();

Это невозможно в стандартном C. Вам необходимо передать приемник как явный формальный аргумент для ваших C-функций (в отличие от С++, который имеет this как неявный формальный).

Однако:

  • вы, как правило, хотите упаковать все функции метода в один единственный struct с несколькими членами функции (и каждый экземпляр должен начинаться с указателя на struct). Читайте о vtable -s.

  • у вас может быть макрос (или, возможно, встроенная функция), чтобы не давать Stopwatch дважды; вы все равно напишите TICK(Stopwatch) not Stopwatch->tick();; statement-expr расширение GCC может быть полезным.

Посмотрите на GTK и его систему Gobject в качестве примера милой объектной системы для C.

Кстати, вы можете решить, что у вас есть селекторы методов первого класса (возможно, целые числа или указатели на какой-то общий тип селектора) и код variadic send dispatching (так что вы бы закодировали send(StopWatch,TICK_SEL) вместо вашего мечтательного Stopwatch->tick()) или макроса. Вы можете найти libffi. Старый Xview может быть вдохновляющим.

Наконец, как и многие разработчики реалистичного слоя, вы можете использовать метапрограммирование и предоставить некоторый инструмент генерации кода C (например, moc в Qt). Вы даже можете рассмотреть возможность настройки компилятора GCC с MELT для таких целей. Или сделать переводчик (см. this) от вашего фантастического диалекта OOP до C (например, VALA do). Или предварительно обработайте свой код внешним препроцессором (ваш собственный, или m4 или GPP и т.д.).

Ответ 2

Примечание: уже есть ряд хороших ответов, которые объясняют, почему синтаксис "вызов метода" недоступен на C, однако они не объясняют, что делать, а просто указывают на ресурсы. Базовый OO в C на самом деле относительно прост, так что здесь быстро КАК.

Этот КАК разделяется на две части:

  • в первом разделе показано, как добиться инкапсуляции.
  • во втором разделе показано, как сложить последнее связывание сверху

Инкапсуляция

Часто, OO фактически используется для обозначения инкапсуляции. Идея инкапсуляции состоит в том, чтобы получить модульный дизайн с четко определенными интерфейсами над состоянием программы в надежде облегчить сохранение инвариантов.

В C это традиционно достигается с помощью непрозрачных указателей:

// stop_watch.h
typedef struct stop_swatch_ stop_watch;

stop_watch* stop_watch_create();
stop_watch* stop_watch_clone(stop_watch const* sw);
void stop_watch_dispose(stop_watch* sw);

void stop_watch_tick(stop_watch* sw);
void stop_watch_start(stop_watch* sw);
void stop_watch_stop(stop_watch* sw);
void stop_watch_reset(stop_watch* sw);

Этот заголовок - это единственное, что видит пользователь, и поэтому он не может назвать внутренние элементы struct stop_watch_. Конечно, это C, пользователь все равно может с ними столкнуться, но, по крайней мере, мы немного усложнили ее.

Примечание: .c остается в качестве упражнения для читателя; это простой скучный код C в конце концов.

Поздняя привязка

Late Binding решает во время выполнения, какую функцию вызывать; его можно, например, достичь с помощью методов virtual в С++, Java,...

Это можно сделать и на C, с относительной легкостью. Вы просто не выиграете от всего сахара.

// stop_watch.h
typedef struct stop_watch_functions_ stop_watch_functions;

typedef struct {
    stop_watch_functions const* functions;
} stop_watch;

struct stop_watch_functions_ {
    void (*clone)(stop_watch const*);
    void (*dispose)(stop_watch*);

    void (*tick)(stop_watch*);
    void (*start)(stop_watch*);
    void (*stop)(stop_watch*);
    void (*reset)(stop_watch*);
};

stop_watch* stop_watch_clone(stop_watch const* sw);
void stop_watch_dispose(stop_watch* sw);

void stop_watch_tick(stop_watch* sw);
void stop_watch_start(stop_watch* sw);
void stop_watch_stop(stop_watch* sw);
void stop_watch_reset(stop_watch* sw);

Хорошо, поэтому мы определяем:

  • v-таблица: stop_watch_functions
  • структура, удерживающая эту v-таблицу: stop_watch; он должен быть частью экземпляра конкретных стоп-часов.

Перейдем к реализации:

// stop_watch.c
stop_watch* stop_watch_clone(stop_watch const* sw) {
    return (*sw->functions->clone)(sw);
}

void stop_watch_dispose(stop_watch* sw) {
    return (*sw->functions->dispose)(sw);
}

void stop_watch_tick(stop_watch* sw) {
    return (*sw->functions->tick)(sw);
}

void stop_watch_start(stop_watch* sw) {
    return (*sw->functions->start)(sw);
}

void stop_watch_stop(stop_watch* sw)  {
    return (*sw->functions->stop)(sw);
}

void stop_watch_reset(stop_watch* sw) {
    return (*sw->functions->reset)(sw);
}

Довольно просто, правильно?

И, наконец, перейдем к конкретной реализации стоп-часов:

// my_stop_watch.h
#include "stop_watch.h"

typedef struct my_stop_watch_ my_stop_watch;

my_stop_watch* my_stop_watch_create();

stop_watch* my_stop_watch_upcast(my_stop_watch* msw);
my_stop_watch* my_stop_watch_downcast(stop_watch* sw);

Хорошо, заголовок скучный; все хорошие вещи, скрытые в конце концов:

// my_stop_watch.c
#include "my_stop_watch.h"

struct my_stop_watch_ {
    stop_watch base;

    unsigned int milliseconds;
    unsigned int seconds;
    unsigned int minutes;
    unsigned int hours;

    bool is_enabled;
};

static stop_watch* my_stop_watch_clone(stop_watch const* sw) {
    my_stop_watch* new = malloc(sizeof(my_stop_watch));
    memset(new, (my_stop_watch const*)sw, sizeof(my_stop_watch));
}

static void my_stop_watch_dispose(stop_watch* sw) {
    free(sw);
}

static void my_stop_watch_tick(stop_watch* sw) {
    my_stop_watch* msw = (my_stop_watch*)sw;
    /* do something */
}

static void my_stop_watch_start(stop_watch* sw) {
    my_stop_watch* msw = (my_stop_watch*)sw;
    /* do something */
}

static void my_stop_watch_stop(stop_watch* sw) {
    my_stop_watch* msw = (my_stop_watch*)sw;
    /* do something */
}

static void my_stop_watch_reset(stop_watch* sw) {
    my_stop_watch* msw = (my_stop_watch*)sw;
    /* do something */
}

static stop_watch_functions const my_stop_watch_table = {
    &my_stop_watch_clone,
    &my_stop_watch_dispose,

    &my_stop_watch_tick,
    &my_stop_watch_start,
    &my_stop_watch_stop,
    &my_stop_watch_reset
};

my_stop_watch* my_stop_watch_create() {
    my_stop_watch* msw = malloc(sizeof(my_stop_watch*));

    msw->base = &my_stop_watch_table;

    /* do something */

    return msw;
}

stop_watch* my_stop_watch_upcast(my_stop_watch* msw) {
    return &msw->base;
}

my_stop_watch* my_stop_watch_downcast(stop_watch* sw) {
    if (sw->functions != &my_stop_watch_table) {
        return NULL;
    }

    return (my_stop_watch*)((char*)sw - offsetof(my_stop_watch, base));
}

Здесь я использовал стратегию большинства реализаций С++ (с виртуальной таблицей); есть и другие доступные стратегии, но это широко применимо.

Ответ 3

Это не означает языковой особенности C, и это, вероятно, было одним из мотивов придумывания С++, поэтому с помощью C это невозможно.

В C много библиотек, и в основном все, что я знаю, использует тот же подход, что и вы, используя структуры для хранения состояний (в вашем случае, stopwatch_t), но просто пропустите указатели на функции в структурах, где это возможно; это не потому, что программистам C не нравится ООП, а потому, что они менее резервируются, например,

 stoplib_tick(Stopwatch);

вместо

 Stopwatch->tick(Stopwatch);

Кроме того, это плохая идея нести вокруг кучи указателей функций в каждом экземпляре вашей структуры, которые всегда одинаковы; это просто пустая трата пространства и возможная причина ошибки. Если вы хотите, вы можете создать одну структуру, содержащую все указатели функций для вашего типа, и вызвать их из этой таблицы. В принципе, то, что VTables в С++, внизу.

Следовательно, ни один программист C не сделает этого; если ваш указатель функции на самом деле не является чем-то, что может измениться, вы просто не выполняете операции, которые вы можете выполнять над структурой в этой структуре.

Я не знаю эту книгу, о которой вы говорите, но если она проповедует это, мне это не нравится.

Серьезно, если вы хотите C с ориентацией объектов, перейдите на С++; помимо кодирования для нескольких ядер, очень мало вы не можете сделать с С++, что вы можете сделать с C, и это было действительно изобретено полвека назад, чтобы привести объектную ориентацию к программистам на C. Не будьте 1960-ыми.

EDIT начал читать PDF-код, на который вы ссылаетесь, - серьезно, кто будет использовать ANSI-C в настоящее время? Особенно, если вы хотите комфортно работать с structs и т.д., вы не должны использовать ничего старше C99 (учитывая, что это уже довольно старый...), и, следовательно, эта книга безнадежно устарела, если вы не придете с системой огромного значения и наследия ( "привет, я работаю над системами управления ядерным оружием с 1980-х годов, и мне нужно исправить это и это" ), я бы сказал, что я не могу придумать случай, когда он будет следует следовать этим примерам; очевидно, "я научился делать ООП на С с нуля" не должен основываться на вещах, которые устарели более десятилетия.

EDIT: вы прокомментируете

Как я пришел из встроенной среды, придерживающейся ANSI-C, все будет работать.

Я неохотно соглашаюсь. На некоторых платформах поддержка C99 отсутствует, но: большинство компиляторов поддерживают подавляющее большинство функций C99. ANSI-C (правильнее называть C89, потому что C99 также является стандартом ANSI) действительно больше 25 лет, и, не зная, ваш код может даже не соответствовать C89. Код в книге уверен, что ад - не действительный ANSI-C, независимо от того, что автор утверждает. Например, ANSI-C не имеет комментариев //; это всего лишь небольшая ошибка, и я бы предположил, что все компиляторы, если они не настроены на педантичный режим, не будут жаловаться, но все же это не хорошее зрелище.

Итак: сделайте себе одолжение и не полагайтесь на книгу, которая выборочно использует жесткие языковые статусы и пытается использовать все, что поддерживает ваш компилятор.

Кроме того: чем больше я читал эту книгу, тем менее хорошим упражнением в (современном) ООП, похоже, является (стр. 3, на странице PDF):

Общий указатель void * используется повсюду. С одной стороны, это делает невозможно понять, что представляет собой набор, но, с другой стороны, это позволяет нам передают практически все add() и другие функции.

Да, потому что безопасность типов - это не концепция, жизненно важная для успеха C, и потому что хорошая идея передать void*, а затем, в первой строке метода, применить ее к вашему желаемому типу указателя. Aargh! Если вам нужны ужасные ошибки, как вы их получите.

Посмотрите на такие вещи, как CPython: Python - это язык OO, но интерпретатор/компилятор написан на C. Python делает C-OOP с структурой PyObject, которая в качестве основной функции имеет ссылку на тип, поэтому, чтобы избежать делая эти слепые слепки. Вы не сможете передать const char[], где вы ожидаете указателя на один из ваших объектов, а вся полиморфность заключается в том, что вы можете использовать дочерние типы вашего типа, но не полностью разрозненные типы, с функция. Книга действительно не делает ООП никакой пользы. Читайте что-то еще. Я почти уверен, что есть книги по дизайну CPython, и я лично думаю, что они не могут быть хуже.

Ответ 4

Почему мне никогда не нравилась эта книга, она пытается превратить C в С++. Каждый должен сначала понять, что программирование на C++ не обязательно совпадает с объектно-ориентированным программированием. ООП - это способ разработки программ, и он совершенно не связан с синтаксисом языка. С++ просто упрощает и красивее, это все. Но только потому, что у С++ есть функция, которая делает код более красивым в некоторой ситуации, это не обязательно означает, что эта функция вообще связана с ООП (например, перегрузка оператора).

Поэтому не пытайтесь превратить C в С++. Примите, что C имеет другой синтаксис, который может быть не таким красивым. C на самом деле имеет множество функциональных возможностей, которые позволяют реализовать проект ООП. Истинная инкапсуляция с частными/общедоступными переменными или функциями на 100% достижима в C.

Так как C не является С++, вам не нужны функции-члены внутри структуры. Единственный указатель на функцию, который вы хотите, есть специальные случаи, такие как функции обратного вызова и тому подобное. Поэтому вместо Stopwatch->tick(&Stopwatch) лучше не использовать указатели на функции, а напрямую вызвать функцию-член: sw_tick(&Stopwatch). Где sw - некоторый уникальный префикс для модуля секундомера.

Это позволяет вам реализовать Секундомер как объект неполного типа (также называемый "непрозрачным типом" ), который является самым ядром ООП в C. Неполный тип позволяет скрыть содержимое структуры до вызывающего.

Затем перепишите весь "класс" секундомера (назовем его классом или ADT или что-то еще) следующим образом:

stopwatch.h

typedef struct stopwatch_t stopwatch_t; // incomplete type

stopwatch_t* sw_new (void);             // "constructor"

void sw_delete (stopwatch_t* sw);       // "destructor"

void sw_tick (const stopwatch_t* sw);   // public member function
// any number of public functions here
// mind const correctness!

stopwatch.c

struct stopwatch_t        // implementation
{
  // true private variables:

  unsigned int milliseconds;
  unsigned int seconds;
  unsigned int minutes;
  unsigned int hours;
  bool is_enabled;
};

stopwatch_t* sw_new (void)
{
  // same as what you already have
}

// the module is responsible for cleaning up its own mess, NOT THE CALLER
void sw_delete (stopwatch_t* sw)
{
  free(sw);
}

// any number of public member functions:
void sw_tick (const stopwatch_t* sw)
{
  // here sw is the "self"/"this" pointer
}

// any number of private member functions:
static void sw_do_stuff (stopwatch_t* sw)
{
}

Вызывающий может только объявлять указатели на объекты, но никогда не является их экземпляром. Это не так много, так много библиотек C и С++. Указатель на неполный тип несколько похож на указатель на абстрактный базовый класс в С++. Вы также не можете объявлять экземпляры этих файлов.

Если вам нужно смешать переменные private и public member, вы должны ввести команду struct в h файл, где переменные public-члена объявлены как простые члены структуры, а переменные private-члена объявлены через неполный тип.