ООП в С, неявно передают себя как параметр
Я работал над примером для изучения ООП в 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();
Ответы
Ответ 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-члена объявлены через неполный тип.