Как реализовать класс на C? [закрыто]

141

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

Ограничения:

  • Я должен использовать C, а не ООП, потому что я пишу код для встроенной системы, а компилятор и уже существующая база кода находятся на C.
  • Не существует динамического распределения памяти, потому что у нас недостаточно памяти, чтобы разумно предположить, что у нас не закончится, если мы начнем динамическое распределение.
  • У компиляторов, с которыми мы работаем, нет проблем с указателями на функции
Бен Гартнер
источник
26
Обязательный вопрос: нужно ли писать объектно-ориентированный код? Если вы это сделаете по какой-либо причине, это нормально, но вам придется вести довольно тяжелую битву. Вероятно, это к лучшему, если вы избегаете попыток писать объектно-ориентированный код на C. Это, безусловно, возможно - см. Отличный ответ от unwind, - но это не совсем «легко», и если вы работаете во встроенной системе с ограниченной памятью, это может быть невыполнимо. Хотя я могу ошибаться - я не пытаюсь отговорить вас от этого, просто предлагаю некоторые контрапункты, которые, возможно, не были представлены.
Крис Лутц,
1
Строго говоря, нам и не нужно. Однако сложность системы сделала код неподдерживаемым. Я считаю, что лучший способ уменьшить сложность - это реализовать некоторые концепции ООП. Спасибо всем, кто ответил в течение 3 минут. Ребята, вы безумные и быстрые!
Бен Гартнер,
8
Это всего лишь мое скромное мнение, но ООП не делает код легко обслуживаемым. Это может упростить управление, но не обязательно более удобное в обслуживании. У вас могут быть «пространства имен» в C (Apache Portable Runtime префикс всех глобальных символов с помощью apr_префикса GLib g_для создания пространства имен) и другие организационные факторы без ООП. Если вы все равно собираетесь реструктурировать приложение, я бы подумал о том, чтобы потратить некоторое время, пытаясь придумать более удобную в обслуживании процедурную структуру.
Крис Лутц,
это обсуждалось бесконечно раньше - вы смотрели какой-либо из предыдущих ответов?
Ларри Ватанабе,
Этот источник, который был в моем удаленном ответе, также может быть полезным : planetpdf.com/codecuts/pdfs/ooc.pdf Он описывает полный подход к выполнению объектно-
Рубен

Ответы:

86

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

typedef struct {
  float (*computeArea)(const ShapeClass *shape);
} ShapeClass;

float shape_computeArea(const ShapeClass *shape)
{
  return shape->computeArea(shape);
}

Это позволит вам реализовать класс, «наследуя» базовый класс и реализуя подходящую функцию:

typedef struct {
  ShapeClass shape;
  float width, height;
} RectangleClass;

static float rectangle_computeArea(const ShapeClass *shape)
{
  const RectangleClass *rect = (const RectangleClass *) shape;
  return rect->width * rect->height;
}

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

void rectangle_new(RectangleClass *rect)
{
  rect->width = rect->height = 0.f;
  rect->shape.computeArea = rectangle_computeArea;
}

Если вам нужно несколько разных конструкторов, вам придется «украсить» имена функций, у вас не может быть более одной rectangle_new()функции:

void rectangle_new_with_lengths(RectangleClass *rect, float width, float height)
{
  rectangle_new(rect);
  rect->width = width;
  rect->height = height;
}

Вот базовый пример использования:

int main(void)
{
  RectangleClass r1;

  rectangle_new_with_lengths(&r1, 4.f, 5.f);
  printf("rectangle r1's area is %f units square\n", shape_computeArea(&r1));
  return 0;
}

Я надеюсь, что это даст вам хотя бы некоторые идеи. Для успешной и богатой объектно-ориентированной структуры на C загляните в библиотеку GObject glib .

Также обратите внимание, что здесь не моделируется явный «класс», каждый объект имеет свои собственные указатели на методы, что немного более гибко, чем вы обычно найдете в C ++. Кроме того, это стоит памяти. Вы можете уйти от этого, вставив указатели методов в classструктуру и изобрести способ для каждого экземпляра объекта ссылаться на класс.

размотать
источник
Не пытаясь писать объектно-ориентированный язык C, обычно лучше делать функции, которые принимают const ShapeClass *или в const void *качестве аргументов? Казалось бы, последнее может быть немного лучше в отношении наследования, но я вижу аргументы в обоих направлениях ...
Крис Лутц
1
@Chris: Да, это сложный вопрос. : | GTK + (который использует GObject) использует правильный класс, то есть RectangleClass *. Это означает, что вам часто приходится выполнять приведение, но они предоставляют удобные макросы, которые помогают в этом, поэтому вы всегда можете преобразовать BASECLASS * p в SUBCLASS *, используя только SUBCLASS (p).
расслабьтесь
1
Мой компилятор не работает во второй строке кода: я float (*computeArea)(const ShapeClass *shape);говорю, что ShapeClassэто неизвестный тип.
DanielSank
@DanielSank, что связано с отсутствием предварительного объявления, требуемого структурой typedef (не показано в приведенном примере). Поскольку сама structссылка требует объявления перед определением. Это объясняется на примере здесь, в ответе Лундина . Изменение примера для включения форвардного объявления должно решить вашу проблему; typedef struct ShapeClass ShapeClass; struct ShapeClass { float (*computeArea)(const ShapeClass *shape); };
С. Уиттакер,
Что происходит, когда у Rectangle есть функция, которую не все Shapes выполняют. Например, get_corners (). Круг не реализует это, но прямоугольник может. Как получить доступ к функции, не являющейся частью родительского класса, от которого вы унаследовали?
Otus
24

Мне тоже однажды пришлось сделать это в качестве домашней работы. Я последовал такому подходу:

  1. Определите элементы данных в структуре.
  2. Определите свои функции-члены, которые принимают указатель на вашу структуру в качестве первого аргумента.
  3. Сделайте это в одном заголовке и в одном c. Заголовок для определения структуры и объявления функций, c для реализаций.

Вот простой пример:

/// Queue.h
struct Queue
{
    /// members
}
typedef struct Queue Queue;

void push(Queue* q, int element);
void pop(Queue* q);
// etc.
/// 
Erelender
источник
Это то, что я делал в прошлом, но с добавлением имитации области видимости, помещая прототипы функций в файл .c или .h по мере необходимости (как я упоминал в своем ответе).
Тейлор Лиз,
Мне нравится, что объявление структуры выделяет всю память. По какой-то причине я забыл, что это сработает.
Бен Гартнер,
Думаю, тебе там нужен typedef struct Queue Queue;.
Крейг МакКуин,
3
Или просто typedef struct {/ * members * /} Queue;
Brooks Moses,
#Craig: Спасибо за напоминание, обновлено.
erelender
12

Если вам нужен только один класс, используйте массив structs в качестве данных «объектов» и передайте указатели на них функциям-членам. Вы можете использовать typedef struct _whatever Whateverперед объявлением, struct _whateverчтобы скрыть реализацию от клиентского кода. Нет разницы между таким «объектом» и FILEобъектом стандартной библиотеки C.

Если вам нужно несколько классов с наследованием и виртуальными функциями, то обычно указатели на функции используются в качестве членов структуры или общий указатель на таблицу виртуальных функций. Библиотека GObject использует и этот прием, и трюк с определением типов , и широко используется.

Там также книга о методах для этого доступны в Интернете - объектно - ориентированное программирование с ANSI C .

Пит Киркхэм
источник
1
Круто! Есть ли другие рекомендации для книг по ООП на C? Или любые другие современные дизайнерские приемы на C? (или встроенные системы?)
Бен Гартнер,
7

вы можете взглянуть на GOBject. это библиотека ОС, которая дает вам подробный способ создания объекта.

http://library.gnome.org/devel/gobject/stable/

Alex
источник
1
Очень заинтересован. Кто-нибудь знает о лицензировании? Для моих рабочих целей включение библиотеки с открытым исходным кодом в проект, вероятно, не сработает с юридической точки зрения.
Бен Гартнер,
GTK + и все библиотеки, которые являются частью этого проекта (включая GObject), находятся под лицензией GNU LGPL, что означает, что вы можете ссылаться на них из проприетарного программного обеспечения. Однако я не знаю, возможно ли это для встроенной работы.
Крис Лутц,
7

Интерфейсы и реализации C: методы создания программного обеспечения многократного использования , Дэвид Р. Хэнсон

http://www.informit.com/store/product.aspx?isbn=0201498413

Эта книга отлично справляется с вашим вопросом. Это из серии профессиональных вычислений Addison Wesley.

Основная парадигма выглядит примерно так:

/* for data structure foo */

FOO *myfoo;
myfoo = foo_create(...);
foo_something(myfoo, ...);
myfoo = foo_append(myfoo, ...);
foo_delete(myfoo);
Марк Харрисон
источник
5

Я приведу простой пример того, как ООП должно быть реализовано в C. Я понимаю, что это тезис 2009 года, но все равно хотел бы добавить это.

/// Object.h
typedef struct Object {
    uuid_t uuid;
} Object;

int Object_init(Object *self);
uuid_t Object_get_uuid(Object *self);
int Object_clean(Object *self);

/// Person.h
typedef struct Person {
    Object obj;
    char *name;
} Person;

int Person_init(Person *self, char *name);
int Person_greet(Person *self);
int Person_clean(Person *self);

/// Object.c
#include "object.h"

int Object_init(Object *self)
{
    self->uuid = uuid_new();

    return 0;
}
uuid_t Object_get_uuid(Object *self)
{ // Don't actually create getters in C...
    return self->uuid;
}
int Object_clean(Object *self)
{
    uuid_free(self->uuid);

    return 0;
}

/// Person.c
#include "person.h"

int Person_init(Person *self, char *name)
{
    Object_init(&self->obj); // Or just Object_init(&self);
    self->name = strdup(name);

    return 0;
}
int Person_greet(Person *self)
{
    printf("Hello, %s", self->name);

    return 0;
}
int Person_clean(Person *self)
{
    free(self->name);
    Object_clean(self);

    return 0;
}

/// main.c
int main(void)
{
    Person p;

    Person_init(&p, "John");
    Person_greet(&p);
    Object_get_uuid(&p); // Inherited function
    Person_clean(&p);

    return 0;
}

Основная концепция предполагает размещение «унаследованного класса» в верхней части структуры. Таким образом, доступ к первым 4 байтам в структуре также обращается к первым 4 байтам в «унаследованном классе» (допуская ненормальные оптимизации). Теперь, когда указатель структуры приводится к «унаследованному классу», «унаследованный класс» может получить доступ к «унаследованным значениям» так же, как он обычно обращается к его членам.

Это и некоторые соглашения об именах для конструкторов, деструкторов, функций выделения и освобождения (я рекомендую init, clean, new, free) помогут вам в долгом.

Что касается виртуальных функций, используйте указатели функций в структуре, возможно, с Class_func (...); обертка тоже. Что касается (простых) шаблонов, добавьте параметр size_t для определения размера, укажите указатель void * или укажите тип «класс» только с той функциональностью, которая вам нужна. (например, int GetUUID (Object * self); GetUUID (& p);)

yyny
источник
Отказ от ответственности: весь код написан на смартфоне. При необходимости добавьте проверки ошибок. Проверить на наличие ошибок.
yyny
4

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

Тейлор Лиз
источник
4
#include <stdio.h>
#include <math.h>
#include <string.h>
#include <uchar.h>

/**
 * Define Shape class
 */
typedef struct Shape Shape;
struct Shape {
    /**
     * Variables header...
     */
    double width, height;

    /**
     * Functions header...
     */
    double (*area)(Shape *shape);
};

/**
 * Functions
 */
double calc(Shape *shape) {
        return shape->width * shape->height;
}

/**
 * Constructor
 */
Shape _Shape() {
    Shape s;

    s.width = 1;
    s.height = 1;

    s.area = calc;

    return s;
}

/********************************************/

int main() {
    Shape s1 = _Shape();
    s1.width = 5.35;
    s1.height = 12.5462;

    printf("Hello World\n\n");

    printf("User.width = %f\n", s1.width);
    printf("User.height = %f\n", s1.height);
    printf("User.area = %f\n\n", s1.area(&s1));

    printf("Made with \xe2\x99\xa5 \n");

    return 0;
};
Pierozi
источник
Избегайте имен вроде _Shape. Это было бы неопределенное поведение. Имя, начинающееся с подчеркивания, за которым следует заглавная буква, являются зарезервированными идентификаторами .
ViralTaco_
3

В вашем случае хорошим приближением класса может быть ADT . Но все равно все будет не так.

Артем Баргер
источник
1
Может ли кто-нибудь дать краткое различие между абстрактным типом данных и классом? Я всегда считал эти два понятия тесно связанными.
Бен Гартнер,
Они действительно тесно связаны. Класс можно рассматривать как реализацию ADT, поскольку (предположительно) он может быть заменен другой реализацией, удовлетворяющей тому же интерфейсу. Я думаю, что трудно дать точную разницу, поскольку концепции четко не определены.
Jørgen Fogh
3

Моя стратегия:

  • Определите весь код для класса в отдельном файле
  • Определите все интерфейсы для класса в отдельном файле заголовка
  • Все функции-члены принимают "ClassHandle", который заменяет имя экземпляра (вместо o.foo () вызовите foo (oHandle)
  • Конструктор заменяется функцией void ClassInit (ClassHandle h, int x, int y, ...) ИЛИ ClassHandle ClassInit (int x, int y, ...) в зависимости от стратегии выделения памяти
  • Все переменные-члены хранятся как член статической структуры в файле класса, инкапсулируя его в файл, предотвращая доступ к нему внешних файлов.
  • Объекты хранятся в массиве статической структуры выше с предопределенными дескрипторами (видимыми в интерфейсе) или фиксированным пределом объектов, которые могут быть созданы.
  • Если это полезно, класс может содержать общедоступные функции, которые будут проходить через массив и вызывать функции всех созданных объектов (RunAll () вызывает каждый Run (oHandle)
  • Функция Deinit (ClassHandle h) освобождает выделенную память (индекс массива) в стратегии динамического распределения

Кто-нибудь видит какие-либо проблемы, дыры, потенциальные ловушки или скрытые преимущества / недостатки любого варианта этого подхода? Если я заново изобретаю метод дизайна (а я полагаю, что должен), можете ли вы указать мне на его название?

Бен Гартнер
источник
С точки зрения стиля, если у вас есть информация, которую можно добавить к вашему вопросу, вы должны отредактировать свой вопрос, чтобы включить эту информацию.
Крис Лутц,
Кажется, вы перешли от malloc, динамически выделяющего из большой кучи, к ClassInit (), динамически выбирая из пула фиксированного размера, вместо того, чтобы фактически делать что-либо о том, что произойдет, когда вы запросите другой объект и у вас нет ресурсов для его предоставления .
Пит Киркхэм,
Да, бремя управления памятью перекладывается на код, вызывающий ClassInit (), чтобы проверить правильность возвращенного дескриптора. По сути, мы создали нашу собственную выделенную кучу для класса. Не уверен, что вижу способ избежать этого, если мы хотим выполнять какое-либо динамическое распределение, если только мы не реализовали кучу общего назначения. Я бы предпочел изолировать риск, унаследованный в куче, до одного класса.
Бен Гартнер,
3

Также см. Этот ответ и этот

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

Скрытие данных с помощью функций get / set легко реализовать в C, но остановимся на этом. Я видел несколько попыток сделать это во встроенной среде, и в конце концов это всегда проблема обслуживания.

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

Герхард
источник
2

Мой подход заключался бы в том, чтобы переместить structвсе основные функции и все связанные с ними функции в отдельный исходный файл (ы), чтобы его можно было использовать «переносимо».

В зависимости от вашего компилятора, вы могли бы включать функции в struct, но это очень специфичное для компилятора расширение и не имеет ничего общего с последней версией стандарта, который я обычно использовал :)

Уоррен
источник
2
Указатели на функции хороши. Мы склонны использовать их для замены больших операторов switch поисковой таблицей.
Бен Гартнер,
2

Первый компилятор C ++ на самом деле был препроцессором, который переводил код C ++ в C.

Так что вполне возможно иметь классы на C. Вы можете попробовать откопать старый препроцессор C ++ и посмотреть, какие решения он создает.

Жаба
источник
Это было бы cfront; он столкнулся с проблемами при добавлении исключений в C ++ - обработка исключений не является тривиальной задачей.
Джонатан Леффлер,
2

GTK полностью построен на C и использует многие концепции ООП. Я прочитал исходный код GTK, он впечатляет и определенно легче читается. Основная концепция состоит в том, что каждый «класс» - это просто структура и связанные с ней статические функции. Все статические функции принимают структуру «экземпляр» в качестве параметра, делают все, что нужно, и при необходимости возвращают результаты. Например, у вас может быть функция «GetPosition (CircleStruct obj)». Функция просто копается в структуре, извлекает номера позиций, возможно, строит новый объект PositionStruct, вставляет x и y в новый PositionStruct и возвращает его. GTK даже реализует наследование таким образом, встраивая структуры внутрь структур. довольно умно.

ракеты
источник
1

Вам нужны виртуальные методы?

В противном случае вы просто определяете набор указателей на функции в самой структуре. Если вы назначите все указатели на функции стандартным функциям C, вы сможете вызывать функции из C в синтаксисе, очень похожем на то, как вы это делали бы в C ++.

Если вы хотите иметь виртуальные методы, все становится сложнее. В основном вам нужно будет реализовать свой собственный VTable для каждой структуры и назначить указатели функций для VTable в зависимости от того, какая функция вызывается. Затем вам понадобится набор указателей на функции в самой структуре, которые, в свою очередь, вызывают указатель функции в VTable. По сути, это то, что делает C ++.

TBH, хотя ... если вы хотите последнее, вам, вероятно, лучше просто найти компилятор C ++, который вы можете использовать, и перекомпилировать проект. Я никогда не понимал, что одержимость C ++ неприменима во встроенном. Я использовал его много раз, он работает быстро и не имеет проблем с памятью. Конечно, вам нужно быть немного более осторожным в том, что вы делаете, но на самом деле это не так сложно.

Гоз
источник
Я уже сказал это и скажу еще раз, но повторю еще раз: вам не нужны указатели на функции или возможность вызывать функции из структур в стиле C ++ для создания ООП в C, ООП в основном касается наследования функциональности и переменных. (контент) и то, и другое может быть достигнуто на C без указателей на функции или дублирования кода.
yyny
0

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

Кроме того, если вы можете использовать C, вы, вероятно, можете использовать C ++ и получать классы.

Бенджамин
источник
4
Я не буду отрицать, но, к вашему сведению, указатели на функции или возможность вызывать функции из структур (что, как я полагаю, является вашим намерением) не имеет ничего общего с ООП. ООП в основном касается наследования функциональности и переменных, которые могут быть реализованы на C без указателей на функции или дублирования.
yyny