Як ви реалізуєте клас на C? [зачинено]


139

Якщо припустити, що я повинен використовувати C (немає C ++ або об'єктно-орієнтованих компіляторів) і у мене немає динамічного розподілу пам’яті, які методи можна використовувати для реалізації класу чи хорошого наближення до класу? Чи завжди гарна ідея виділити "клас" в окремий файл? Припустимо, що ми можемо попередньо розподілити пам'ять, прийнявши фіксовану кількість екземплярів або навіть визначивши посилання на кожен об'єкт як константи до часу компіляції. Не соромтеся робити припущення щодо того, яку концепцію OOP мені потрібно буде впровадити (вона буде різною) та запропонувати найкращий метод для кожного.

Обмеження:

  • Мені потрібно використовувати C, а не OOP, оскільки я пишу код для вбудованої системи, а компілятор і попередня база коду знаходиться в C.
  • Не існує динамічного розподілу пам'яті, оскільки у нас недостатньо пам'яті, щоб обгрунтовано припустити, що ми не закінчимося, якщо почнемо динамічно розподіляти її.
  • Компілятори, з якими ми працюємо, не мають проблем з функціональними покажчиками

26
Обов’язкове запитання: чи потрібно писати об'єктно-орієнтований код? Якщо ви зробите це з якоїсь причини, це добре, але ви будете вести досить важку битву. Це, мабуть, найкраще, якщо ви не намагаєтеся написати об'єктно-орієнтований код на C. Це, безумовно, можливо - див. Відмінну відповідь розмотування - але це не зовсім "просто", і якщо ви працюєте над вбудованою системою з обмеженою пам'яттю, це може бути недоцільним. Я, можливо, помиляюся - я не намагаюся вас сперечатися з цим, просто презентуйте деякі контрпункти, які, можливо, не були представлені.
Кріс Лутц

1
Строго кажучи, нам цього не потрібно. Однак складність системи зробила код нездійсненним. Я відчуваю, що найкращий спосіб зменшити складність - це реалізація деяких концепцій ООП. Дякуємо всім, хто відповів протягом 3 хвилин. Ви, хлопці, божевільні та швидкі!
Бен Гартнер

8
Це лише моя скромна думка, але OOP не робить код миттєво доцільним. Це може полегшити управління, але не обов’язково більш ремонтувати. Ви можете мати "простори імен" у C (префікс Apache Portable Runtime префіксує всі символи глобальної групи за допомогою apr_та префікси GLIb з ними g_для створення простору імен) та інших організаційних факторів без OOP. Якщо ви все одно будете реструктурувати додаток, я б подумав витратити якийсь час, намагаючись придумати більш доцільну процедурну структуру.
Кріс Лутц

це обговорювалося нескінченно раніше - ви подивилися на будь-яку з попередніх відповідей?
Ларрі Ватанабе

Це джерело, яке було у видаленій моїй відповіді, також може допомогти: planetpdf.com/codecuts/pdfs/ooc.pdf У ньому описаний повний підхід до виконання ОО в C.
Рубен

Відповіді:


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структурі, і винайдіть спосіб для кожного екземпляра об'єкта для посилання на клас.


Не маючи спроби написати об'єктно-орієнтований С, як правило, найкраще робити функції, які беруть const ShapeClass *або const void *як аргументи? Здавалося б, останнє може бути трохи приємніше щодо спадкування, але я можу бачити аргументи обома способами ...
Кріс Лутц

1
@Chris: Так, це складне питання. : | GTK + (який використовує GObject) використовує належний клас, тобто RectangleClass *. Це означає, що вам часто доводиться робити касти, але вони надають зручні макроси, які допомагають у цьому, тому ви завжди можете передавати BASECLASS * p на SUBCLASS *, використовуючи лише SUBCLASS (p).
розмотувати

1
Мій компілятор не в другому рядку коду: float (*computeArea)(const ShapeClass *shape);кажучи, що ShapeClassце невідомий тип.
DanielSank

@DanielSank, що пов'язано з відсутністю прямого декларації, що вимагається "typedef structure" (не показано у наведеному прикладі). Оскільки саме structпосилання, воно потребує декларації, перш ніж воно буде визначене. Це пояснюється прикладом тут у відповіді Лундіна . Змінення прикладу для включення прямої декларації повинно вирішити вашу проблему; typedef struct ShapeClass ShapeClass; struct ShapeClass { float (*computeArea)(const ShapeClass *shape); };
С. Віттакер

Що відбувається, коли у прямокутника є функція, яку виконують не всі фігури. Наприклад, 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.
/// 

Це я вже робив у минулому, але з додаванням підробленої області, розміщуючи прототипи функцій у файлі .c або .h за потребою (як я вже згадував у своїй відповіді).
Тейлор Ліз

Мені це подобається, структурна декларація виділяє всю пам'ять. Я чомусь забув, це буде добре.
Бен Гартнер

Я думаю, що вам там потрібен typedef struct Queue Queue;.
Крейг МакКуін,

3
Або просто наберітьdef структуру {/ * members * /} Черга;
Брукс Мойсей

#Craig: Дякую за нагадування, оновлено.
erelender

12

Якщо ви хочете лише один клас, використовуйте масив structs як дані "об'єктів" і передайте їм покажчики на функції "член". Ви можете використовувати typedef struct _whatever Whateverперед оголошенням, struct _whateverщоб приховати реалізацію від клієнтського коду. Немає різниці між таким "об'єктом" та FILEоб'єктом стандартної бібліотеки С.

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

Там також книга про методи для цього доступні в Інтернеті - об'єктно - орієнтоване програмування з ANSI C .


1
Класно! Будь-які інші рекомендації щодо книг про OOP в C? Або будь-які інші сучасні методи дизайну в С? (чи вбудовані системи?)
Бен Гартнер

7

ви можете подивитися на GOBject. це бібліотека ОС, яка дає вам докладний спосіб зробити об’єкт.

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


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

Я наведу простий приклад того, як слід робити OOP у 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-х байтів у «спадковому класі» (Asuming non-crazy optimisations). Тепер, коли покажчик структури передається на "успадкований клас", "успадкований клас" може отримати доступ до "успадкованих значень" так само, як звичайно отримав би доступ до своїх членів.

Це та деякі правила іменування для конструкторів, деструкторів, функцій розподілу та трансалокаріону (рекомендую init, clean, new, free) дадуть вам довгий шлях.

Що стосується віртуальних функцій, використовуйте функціональні вказівники в структурі, можливо, з Class_func (...); обгортка теж. Що стосується (простих) шаблонів, додайте параметр size_t, щоб визначити розмір, вимагати недійсного * вказівника або вимагати типу 'class' з лише функціоналом, який вам цікавий. (наприклад, int GetUUID (Object * self); GetUUID (& p);)


Відмова від відповідальності: весь код написаний на смартфоні. Додайте перевірки помилок, де потрібно. Перевірте наявність помилок.
yyny

4

Використовуйте a 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;
};

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) звільняє виділену пам'ять (індекс масиву) в динамічній стратегії розподілу

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


Що стосується стилю, якщо у вас є інформація, яку ви можете додати до свого питання, вам слід відредагувати своє запитання, щоб включити цю інформацію.
Кріс Лутц

Ви, схоже, перейшли з малокалонного динамічно виділяючи з великої купи до ClassInit (), динамічно вибираючи пул фіксованого розміру, а не насправді робити щось про те, що станеться, коли ви запитаєте інший об’єкт і не маєте ресурсів надати його .
Піт Кіркем

Так, тягар управління пам'яттю перекладається на код, що викликає ClassInit (), щоб перевірити, чи повернута ручка є дійсною. По суті, ми створили власну цілу купу для класу. Не впевнений, що я бачу спосіб уникнути цього, якщо ми хочемо зробити будь-яке динамічне виділення, якщо тільки ми не реалізували купу загального призначення. Я вважаю за краще виділити спадковий ризик у купі до одного класу.
Бен Гартнер

3

Також дивіться цю відповідь і цю

Можливо. Це завжди здається гарною ідеєю, але згодом стає кошмаром технічного обслуговування. Ваш код засипається шматочками коду, що пов'язує все разом. У нового програміста виникне багато проблем з читанням та розумінням коду, якщо ви використовуєте функціональні покажчики, оскільки не буде очевидно, які функції викликаються.

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

Оскільки у всіх готових є проблеми з технічним обслуговуванням, я б вирішив зрозуміти.


2

Мій підхід полягав би в тому, щоб перенести structвсі і в першу чергу пов’язані функції в окремий вихідний файл (и), щоб він міг "переноситися".

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


2
Функціональні вказівники - все добре. Ми схильні використовувати їх для заміни великих операторів комутації таблицею пошуку.
Бен Гартнер

2

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

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


Це було б cfront; він зіткнувся з проблемами, коли до C ++ були додані винятки - обробка винятків не тривіальна.
Джонатан Леффлер

2

GTK повністю побудований на C, і він використовує багато концепцій OOP. Я прочитав вихідний код GTK, і він досить вражаючий і, безумовно, легше читати. Основна концепція полягає в тому, що кожен "клас" - це просто структура і пов'язані з ним статичні функції. Усі статичні функції приймають структуру "екземпляр" як параметр, роблять все, що потрібно, і повертають результати, якщо необхідно. Наприклад, у вас може бути функція "GetPosition (CircleStruct obj)". Функція просто перекопає структуру, витягне номери позицій, ймовірно, побудує новий об'єкт PositionStruct, вставить x і y у новий PositionStruct і поверне його. GTK навіть реалізує спадкування таким чином, вбудовуючи конструкції всередині конструкцій. досить розумний.


1

Хочете віртуальних методів?

Якщо ні, то ви просто визначите набір покажчиків функцій у самій структурі. Якщо ви призначите всі покажчики функцій стандартним функціям C, ви зможете викликати функції з C у дуже подібному синтаксисі до того, як ви працювали в C ++.

Якщо ви хочете мати віртуальні методи, це ускладнюється. В основному вам потрібно буде впровадити свій власний VTable до кожної структури та призначити функціональні покажчики VTable залежно від того, яка функція викликається. Тоді вам знадобиться набір покажчиків функцій у самій структурі, які, в свою чергу, викликають покажчик функції у VTable. Це, по суті, те, що робить C ++.

TBH, хоча ... якщо ви хочете останнього, то вам, мабуть, краще просто знайти компілятор C ++, який ви можете використовувати та перекомпілювати проект. Я ніколи не розумів, що одержимість C ++ не використовується вбудованим. Я використовував його багато разів, і він працює швидко і не має проблем із пам'яттю. Впевнені, що ви повинні бути трохи уважнішими до того, що робите, але це насправді не так вже й складно.


Я вже говорив це і знову скажу, але скажу ще раз: Вам не потрібні функціональні покажчики або можливість викликати функції зі стилів C ++ для створення стилю O + в C, OOP здебільшого стосується успадкування функціональності та змінних (вміст), що може бути досягнуто в C без покажчиків функцій або дублювання коду.
yyny

0

C не є мовою OOP, як правильно зазначають, тому немає вбудованого способу написання справжнього класу. Найкраще ставитись до того, щоб подивитися на структури та функціональні вказівники , це дозволить вам побудувати наближення класу. Однак, оскільки C є процедурним, ви можете розглянути можливість написання більше подібного до С коду (тобто, не намагаючись використовувати класи).

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


4
Я не заперечую, але FYI, покажчики функцій або можливість викликати функції з структур (на що я думаю, ваш намір) не має нічого спільного з OOP. OOP здебільшого стосується успадкування функціональних можливостей та змінних, що може бути досягнуто в C без покажчиків функцій чи дублювання.
yyny
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.