2009-09-10 4 views
106

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

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

  • я должен использовать C, а не ООП , потому что я пишу код для встроенной системы, и компилятор и предсуществующей базы коды на C.
  • Там не является динамическим распределением памяти , потому что у нас недостаточно памяти , чтобы разумно предположить, что мы не закончим , если мы начнем динамически выделять .
  • Составители мы работаем не имеют никаких проблем с указателями на функции
+21

Обязательный вопрос: вам нужно написать объектно-ориентированный код? Если вы по какой-то причине, это прекрасно, но вы будете сражаться с довольно тяжелой битвой. Вероятно, это лучше всего, если вы не пытаетесь написать объектно-ориентированный код на C. Это, безусловно, возможно - см. Отличный ответ от unwind - но это не совсем «легко», и если вы работаете с встроенной системой с ограниченной памятью, может оказаться невозможным. Возможно, я ошибаюсь, но я не пытаюсь вас отговорить, просто представляю некоторые контрапункты, которые, возможно, не были представлены. –

+1

Строго говоря, нам это не нужно. Однако сложность системы сделала код недостижимым. Я чувствую, что лучший способ уменьшить сложность - реализовать некоторые концепции ООП. Спасибо всем, кто отвечает в течение 3 минут. Вы, ребята, сумасшедшие и быстрые! –

+8

Это просто мое скромное мнение, но ООП не делает код мгновенно поддерживаемым. Это может упростить управление, но не обязательно более удобным для обслуживания. Вы можете иметь «пространства имен» в C (Apache Portable Runtime префикс всех символов глобальных символов с помощью apr_', а GLib префикс их «g_» для создания пространства имен) и других организационных факторов без ООП. Если вы все равно собираетесь реструктурировать приложение, я бы подумал о том, чтобы потратить некоторое время, пытаясь придумать более удобную процедурную структуру. –

ответ

64

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

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 и придумать способ для каждого экземпляра объекта ссылаться на класс.

+0

Не нужно было пытаться писать объектно-ориентированный C, как правило, лучше всего выполнять функции, которые принимают 'const ShapeClass *' или 'const void *' в качестве аргументов? Казалось бы, последнее может быть немного приятнее в наследовании, но я могу видеть аргументы в обоих направлениях ... –

+1

@ Крис: Да, это сложный вопрос. : | GTK + (который использует GObject) использует соответствующий класс, т. Е. RectangleClass *. Это означает, что вам часто приходится делать броски, но они предоставляют удобные макросы, помогите с этим, поэтому вы всегда можете использовать BASECLASS * p для SUBCLASS *, используя только SUBCLASS (p). – unwind

+1

Мой компилятор не работает во второй строке кода: 'float (* computeArea) (const ShapeClass * shape);' говоря, что 'ShapeClass' является неизвестным типом. – DanielSank

2

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

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

+2

Функциональные указатели - все хорошо. Мы обычно используем их для замены операторов с большим количеством операторов с помощью таблицы поиска. –

0

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

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

+2

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

3

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

+1

Может ли кто-нибудь дать краткий разброс между абстрактным типом данных и классом? Я всегда из двух понятий тесно связан. –

+0

Они действительно тесно связаны. Класс можно рассматривать как реализацию ADT, поскольку (предположительно) его можно заменить другой реализацией, удовлетворяющей одному и тому же интерфейсу. Я думаю, что трудно дать точную разницу, хотя понятия четко не определены. –

1

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

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

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

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

+0

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

4

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

21

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

  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. 
/// 
+0

Это то, что я сделал в прошлом, но с добавлением области фальсификации, поместив прототипы функций в файлы .c или .h по мере необходимости (как я уже упоминал в своем ответе). –

+0

Мне это нравится, объявление структуры выделяет всю память. По какой-то причине я забыл, что это будет хорошо работать. –

+0

Я думаю, вам нужно 'typedef struct Queue Queue;' там. –

11

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

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

Существует также книга по технике для этого онлайн - Object Oriented Programming with ANSI C.

+1

Прохладный! Любые другие рекомендации для книг по ООП в C? Или любые другие современные методы проектирования в C? (или встроенные системы?) –

2

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

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

+0

Это будет 'cfront'; он столкнулся с проблемами, когда исключения были добавлены в C++ - обработка исключений не является тривиальной. –

7

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

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

+1

Очень интересно. Кто-нибудь знает о лицензировании? Для моих целей на работе отключение библиотеки с открытым исходным кодом в проект, вероятно, не будет работать с юридической точки зрения. –

+0

GTK +, и все библиотеки, входящие в этот проект (включая GObject), лицензируются под GNU LGPL, что означает, что вы можете ссылаться на них с проприетарного программного обеспечения. Однако я не знаю, будет ли это целесообразным для встроенных работ. –

3

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

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

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

+0

Что касается стиля, если у вас есть информация для добавления к вашему вопросу, вы должны отредактировать свой вопрос, чтобы включить эту информацию. –

+0

Кажется, что вы переместились из malloc, динамически выделяя из большой кучи ClassInit() динамически, выбрав из пула фиксированного размера, а не фактически что-то делать с тем, что произойдет, когда вы попросите другого объекта и не будете иметь ресурсов для предоставления один. –

+0

Да, бремя управления памятью смещается на код, вызывающий ClassInit(), чтобы проверить, что возвращаемый дескриптор действителен. По сути, мы создали собственную выделенную кучу для класса. Не уверен, что я вижу способ избежать этого, если мы хотим сделать любое динамическое распределение, если только мы не реализуем кучу общего назначения. Я предпочел бы изолировать риск наследования в куче к одному классу. –

3

Также см this answer и this one

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

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

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

6

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

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

Эта книга делает отличную работу покрытия на ваш вопрос. Это в серии Addison Wesley Professional Computing.

Основная парадигма что-то вроде этого:

/* for data structure foo */ 

FOO *myfoo; 
myfoo = foo_create(...); 
foo_something(myfoo, ...); 
myfoo = foo_append(myfoo, ...); 
foo_delete(myfoo); 
1

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

3
#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; 
}; 
4

Я приведу простой пример того, как ООП следует делать на 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);)

+0

Отказ от ответственности: весь код написан на смартфоне. Добавьте errorchecks, если необходимо. Проверьте наличие ошибок. – YoYoYonnY

Смежные вопросы