Jak zaimplementować klasę w C? [Zamknięte]

139

Zakładając, że muszę używać C (bez C ++ lub kompilatorów obiektowych) i nie mam dynamicznej alokacji pamięci, jakich technik mogę użyć do zaimplementowania klasy lub dobrego przybliżenia klasy? Czy zawsze warto wyodrębnić „klasę” do osobnego pliku? Załóżmy, że możemy wstępnie przydzielić pamięć, przyjmując stałą liczbę wystąpień lub nawet definiując odniesienie do każdego obiektu jako stałą przed kompilacją. Zapraszam do zakładania, którą koncepcję OOP będę musiał wdrożyć (będzie się różnić) i zasugeruj najlepszą metodę dla każdego.

Ograniczenia:

  • Muszę używać C, a nie OOP, ponieważ piszę kod dla systemu osadzonego, a kompilator i wcześniej istniejący kod jest w C.
  • Nie ma dynamicznej alokacji pamięci, ponieważ nie mamy wystarczającej ilości pamięci, aby rozsądnie założyć, że jej nie zabraknie, jeśli zaczniemy ją dynamicznie alokować.
  • Kompilatory, z którymi pracujemy, nie mają problemów ze wskaźnikami funkcji
Ben Gartner
źródło
26
Obowiązkowe pytanie: czy musisz pisać kod zorientowany obiektowo? Jeśli zrobisz to z jakiegokolwiek powodu, to w porządku, ale będziesz toczył dość żmudną bitwę. Prawdopodobnie najlepiej będzie, jeśli unikniesz pisania kodu zorientowanego obiektowo w C. Jest to z pewnością możliwe - zobacz doskonałą odpowiedź - ale nie jest to do końca „łatwe”, a jeśli pracujesz na systemie osadzonym z ograniczoną pamięcią, może być niewykonalne. Mogę się jednak mylić - nie próbuję cię z tego wypierać, tylko przedstawiam kontrapunkty, których być może nie przedstawiono.
Chris Lutz,
1
Ściśle mówiąc, nie musimy. Jednak złożoność systemu sprawiła, że ​​kod jest nie do utrzymania. Mam wrażenie, że najlepszym sposobem na zmniejszenie złożoności jest wdrożenie pewnych koncepcji OOP. Dziękuję wszystkim, którzy odpowiedzieli w ciągu 3 minut. Jesteście szaleni i szybcy!
Ben Gartner,
8
To tylko moja skromna opinia, ale OOP nie sprawia, że ​​kod można natychmiast konserwować. Może to ułatwić zarządzanie, ale niekoniecznie łatwiejsze w utrzymaniu. Możesz mieć „przestrzenie nazw” w języku C (Apache Portable Runtime przedrostuje wszystkie symbole globalne, apr_a GLib dodaje do nich przedrostek, g_aby utworzyć przestrzeń nazw) i inne czynniki organizujące bez OOP. Jeśli i tak zamierzasz zrestrukturyzować aplikację, rozważę poświęcenie czasu na wymyślenie łatwiejszej do utrzymania struktury proceduralnej.
Chris Lutz,
było to omawiane bez końca - czy spojrzałeś na którąś z poprzednich odpowiedzi?
Larry Watanabe
To źródło, które znajdowało się w mojej usuniętej odpowiedzi, również może być pomocne: planetpdf.com/codecuts/pdfs/ooc.pdf Opisuje kompletne podejście do robienia OO w C.
Ruben Steins

Odpowiedzi:

86

To zależy od dokładnego zestawu funkcji „zorientowanych obiektowo”, jaki chcesz mieć. Jeśli potrzebujesz rzeczy takich jak przeciążanie i / lub metody wirtualne, prawdopodobnie będziesz musiał uwzględnić wskaźniki funkcji w strukturach:

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

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

Umożliwiłoby to zaimplementowanie klasy poprzez „dziedziczenie” klasy bazowej i zaimplementowanie odpowiedniej funkcji:

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;
}

Oczywiście wymaga to również zaimplementowania konstruktora, który zapewni prawidłową konfigurację wskaźnika funkcji. Zwykle dynamicznie przydzielałbyś pamięć dla instancji, ale możesz też na to pozwolić wywołującemu:

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

Jeśli potrzebujesz kilku różnych konstruktorów, będziesz musiał "udekorować" nazwy funkcji, nie możesz mieć więcej niż jednej rectangle_new()funkcji:

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

Oto podstawowy przykład pokazujący użycie:

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;
}

Mam nadzieję, że to przynajmniej daje kilka pomysłów. Aby uzyskać udaną i bogatą strukturę obiektową w języku C, zapoznaj się z biblioteką GObject glib .

Zwróć również uwagę, że powyżej nie jest modelowana żadna wyraźna „klasa”, każdy obiekt ma własne wskaźniki do metod, które są nieco bardziej elastyczne niż typowe dla C ++. Poza tym kosztuje pamięć. Możesz od tego uciec, upychając wskaźniki do metod w classstrukturze i wymyślić sposób, w jaki każda instancja obiektu będzie odwoływała się do klasy.

rozwijać
źródło
Nie musiałem próbować pisać zorientowanego obiektowo języka C, czy zazwyczaj najlepiej jest tworzyć funkcje, które przyjmują const ShapeClass *lub const void *jako argumenty? Wydawałoby się, że ten ostatni może być trochę lepszy w dziedziczeniu, ale widzę argumenty w obie strony ...
Chris Lutz
1
@Chris: Tak, to trudne pytanie. : | GTK + (który używa GObject) używa odpowiedniej klasy, czyli RectangleClass *. Oznacza to, że często musisz wykonywać rzuty, ale zapewniają one przydatne makra, które pomagają w tym, więc zawsze możesz rzutować BASECLASS * p na SUBCLASS *, używając tylko SUBCLASS (p).
zrelaksuj się
1
Mój kompilator nie działa w drugiej linii kodu: float (*computeArea)(const ShapeClass *shape);mówi, że ShapeClassjest to nieznany typ.
DanielSank
@DanielSank, co jest spowodowane brakiem deklaracji forward wymaganej przez 'strukturę typedef' (nie pokazano w podanym przykładzie). Ponieważ structsame odwołania wymagają deklaracji przed jej zdefiniowaniem. Jest to wyjaśnione na przykładzie tutaj w odpowiedzi na Lundin . Modyfikacja przykładu w celu uwzględnienia deklaracji forward powinna rozwiązać problem; typedef struct ShapeClass ShapeClass; struct ShapeClass { float (*computeArea)(const ShapeClass *shape); };
S. Whittaker
Co się dzieje, gdy Rectangle ma funkcję, której nie mają wszystkie Shapes. Na przykład get_corners (). Okrąg nie byłby w stanie tego zrealizować, ale prostokąt mógłby. Jak uzyskać dostęp do funkcji, która nie jest częścią klasy nadrzędnej, z której odziedziczyłeś?
Otus,
24

Raz też musiałem to zrobić jako zadanie domowe. Postępowałem zgodnie z tym podejściem:

  1. Zdefiniuj członków danych w struct.
  2. Zdefiniuj elementy składowe funkcji, które jako pierwszy argument przyjmą wskaźnik do Twojej struktury.
  3. Zrób to w jednym nagłówku i jednym c. Nagłówek definicji struktury i deklaracji funkcji, c dla implementacji.

Prostym przykładem może być:

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

void push(Queue* q, int element);
void pop(Queue* q);
// etc.
/// 
erelender
źródło
To właśnie robiłem w przeszłości, ale z dodaniem fałszywego zakresu przez umieszczanie prototypów funkcji w pliku .c lub .h, jeśli było to potrzebne (jak wspomniałem w mojej odpowiedzi).
Taylor Leese,
Podoba mi się to, deklaracja struktury przydziela całą pamięć. Z jakiegoś powodu zapomniałem, że to zadziała.
Ben Gartner,
Myślę, że potrzebujesz typedef struct Queue Queue;tam.
Craig McQueen,
3
Lub po prostu typedef struct {/ * Members * /} Queue;
Brooks Moses,
#Craig: Dzięki za przypomnienie, zaktualizowane.
erelender,
12

Jeśli potrzebujesz tylko jednej klasy, użyj tablicy structs jako danych „obiektów” i przekaż wskaźniki do nich do funkcji „składowych”. Możesz użyć typedef struct _whatever Whateverprzed deklaracją, struct _whateveraby ukryć implementację przed kodem klienta. Nie ma różnicy między takim „obiektem” a standardowym FILEobiektem biblioteki C.

Jeśli chcesz mieć więcej niż jedną klasę z dziedziczeniem i funkcjami wirtualnymi, często używa się wskaźników do funkcji jako elementów składowych struktury lub wspólnego wskaźnika do tabeli funkcji wirtualnych. Gobject biblioteka korzysta zarówno ten i podstęp typedef i jest powszechnie stosowane.

Jest też książka o technikach to dostępne online - Object Oriented Programming z ANSI C .

Pete Kirkham
źródło
1
Chłodny! Jakieś inne zalecenia dotyczące książek na temat OOP w języku C? A może inne nowoczesne techniki projektowania w C? (lub systemy wbudowane?)
Ben Gartner,
7

możesz rzucić okiem na GOBject. jest to biblioteka systemu operacyjnego, która daje pełny sposób na wykonanie obiektu.

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

Alex
źródło
1
Bardzo zainteresowany. Czy ktoś wie o licencji? Dla moich celów w pracy umieszczenie biblioteki open source w projekcie prawdopodobnie nie zadziała z prawnego punktu widzenia.
Ben Gartner,
GTK + i wszystkie biblioteki, które są częścią tego projektu (w tym GObject), są objęte licencją na licencji GNU LGPL, co oznacza, że ​​możesz łączyć się z nimi za pośrednictwem zastrzeżonego oprogramowania. Nie wiem jednak, czy będzie to wykonalne w przypadku pracy osadzonej.
Chris Lutz,
7

C Interfejsy i implementacje: techniki tworzenia oprogramowania wielokrotnego użytku , David R. Hanson

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

Ta książka świetnie radzi sobie z twoim pytaniem. Znajduje się w serii Addison Wesley Professional Computing.

Podstawowy paradygmat wygląda mniej więcej tak:

/* for data structure foo */

FOO *myfoo;
myfoo = foo_create(...);
foo_something(myfoo, ...);
myfoo = foo_append(myfoo, ...);
foo_delete(myfoo);
Mark Harrison
źródło
5

Podam prosty przykład tego, jak OOP powinno być zrobione w C. Zdaję sobie sprawę, że ten plik pochodzi z 2009 roku, ale mimo wszystko chciałbym to dodać.

/// 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;
}

Podstawowa koncepcja polega na umieszczeniu „klasy dziedziczonej” na szczycie struktury. W ten sposób dostęp do pierwszych 4 bajtów w strukturze uzyskuje również dostęp do pierwszych 4 bajtów w „klasie dziedziczonej” (zakładając nie szalone optymalizacje). Teraz, gdy wskaźnik struktury jest rzutowany na „dziedziczoną klasę”, „dziedziczona klasa” może uzyskać dostęp do „dziedziczonych wartości” w taki sam sposób, w jaki normalnie uzyskuje dostęp do swoich elementów członkowskich.

Ta i niektóre konwencje nazewnictwa dla konstruktorów, destruktorów, funkcji alokacji i deallocarionów (polecam init, clean, new, free) przyniosą Ci wiele korzyści.

Jeśli chodzi o funkcje wirtualne, użyj wskaźników funkcji w strukturze, prawdopodobnie z Class_func (...); opakowanie też. Jeśli chodzi o (proste) szablony, dodaj parametr size_t, aby określić rozmiar, wymagaj wskaźnika void * lub wymagaj typu 'class' z funkcjami, na których Ci zależy. (np. int GetUUID (Object * self); GetUUID (& p);)

yyny
źródło
Zastrzeżenie: cały kod napisany na smartfonie. W razie potrzeby dodaj kontrole błędów. Sprawdź błędy.
yyny
4

Użyć structdo symulacji członków danych z klasy. Jeśli chodzi o zakres metod, możesz symulować metody prywatne, umieszczając prototypy funkcji prywatnych w pliku .c, a funkcje publiczne w pliku .h.

Taylor Leese
źródło
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
źródło
3

W twoim przypadku dobrym przybliżeniem klasy może być ADT . Ale nadal nie będzie to samo.

Artem Barger
źródło
1
Czy ktoś może podać krótką różnicę między abstrakcyjnym typem danych a klasą? Zawsze uważałem te dwie koncepcje za ściśle powiązane.
Ben Gartner,
Są rzeczywiście blisko spokrewnieni. Klasę można postrzegać jako implementację ADT, ponieważ (podobno) można by ją zastąpić inną implementacją spełniającą ten sam interfejs. Myślę, że trudno jest jednak podać dokładną różnicę, ponieważ pojęcia nie są jasno zdefiniowane.
Jørgen Fogh
3

Moja strategia to:

  • Zdefiniuj cały kod dla klasy w osobnym pliku
  • Zdefiniuj wszystkie interfejsy dla klasy w oddzielnym pliku nagłówkowym
  • Wszystkie funkcje składowe przyjmują „ClassHandle”, który zastępuje nazwę instancji (zamiast o.foo (), wywołanie foo (oHandle)
  • Konstruktor jest zastępowany funkcją void ClassInit (ClassHandle h, int x, int y, ...) LUB ClassHandle ClassInit (int x, int y, ...) w zależności od strategii alokacji pamięci
  • Wszystkie zmienne składowe są przechowywane jako składowe struktury statycznej w pliku klasy, hermetyzując je w pliku, uniemożliwiając dostęp do niego zewnętrznym plikom
  • Obiekty są przechowywane w tablicy struktury statycznej powyżej, z predefiniowanymi uchwytami (widocznymi w interfejsie) lub ustalonym limitem obiektów, które można utworzyć
  • Jeśli jest to przydatne, klasa może zawierać funkcje publiczne, które będą przechodzić przez tablicę i wywoływać funkcje wszystkich instancji obiektów (RunAll () wywołuje każde polecenie Run (oHandle)
  • Funkcja Deinit (ClassHandle h) zwalnia przydzieloną pamięć (indeks tablicy) w strategii alokacji dynamicznej

Czy ktoś widzi jakieś problemy, dziury, potencjalne pułapki lub ukryte korzyści / wady któregokolwiek z wariantów tego podejścia? Jeśli wymyślam na nowo metodę projektowania (i zakładam, że tak musi być), czy możesz wskazać mi jej nazwę?

Ben Gartner
źródło
Ze względu na styl, jeśli masz informacje, które chcesz dodać do swojego pytania, zmodyfikuj swoje pytanie, tak aby zawierało te informacje.
Chris Lutz,
Wydaje się, że przeniosłeś się z malloc dynamicznie przydzielając z dużej sterty do ClassInit () dynamicznie wybierając z puli o stałym rozmiarze, zamiast robić cokolwiek z tym, co się stanie, gdy poprosisz o inny obiekt i nie masz zasobów, aby go dostarczyć .
Pete Kirkham
Tak, obciążenie związane z zarządzaniem pamięcią jest przenoszone na kod wywołujący metodę ClassInit () w celu sprawdzenia, czy zwrócony uchwyt jest prawidłowy. Zasadniczo stworzyliśmy własną dedykowaną stertę dla klasy. Nie jestem pewien, czy widzę sposób, aby tego uniknąć, jeśli chcemy przeprowadzić alokację dynamiczną, chyba że zaimplementowaliśmy stertę ogólnego przeznaczenia. Wolę odizolować ryzyko dziedziczone w stercie do jednej klasy.
Ben Gartner,
3

Zobacz także tę odpowiedź i

To jest możliwe. Zawsze wydaje się to dobrym pomysłem, ale potem staje się koszmarem konserwacji. Twój kod jest zaśmiecony kawałkami kodu wiążącymi wszystko razem. Nowy programista będzie miał wiele problemów z czytaniem i zrozumieniem kodu, jeśli użyjesz wskaźników do funkcji, ponieważ nie będzie oczywiste, jakie funkcje się nazywają.

Ukrywanie danych za pomocą funkcji get / set jest łatwe do zaimplementowania w C, ale na tym koniec. Widziałem wiele prób tego w środowisku wbudowanym i ostatecznie zawsze jest to problem z konserwacją.

Ponieważ wszyscy jesteście gotowi, macie problemy z konserwacją, chciałbym omijać.

Gerhard
źródło
2

Moje podejście polegałoby na przeniesieniu funkcji structi wszystkich związanych z nią głównie funkcji do oddzielnych plików źródłowych, aby można było ich używać „przenośnie”.

W zależności od kompilatora, to może być w stanie obejmować funkcje Into the struct, ale to bardzo kompilator specyficzne rozszerzenie, a nie ma nic wspólnego z ostatnią wersją standardową I rutynowo stosowanego :)

królikarnia
źródło
2
Wskaźniki funkcji są dobre. Zwykle używamy ich do zastępowania dużych instrukcji przełączania tabelą przeglądową.
Ben Gartner,
2

Pierwszy kompilator C ++ był w rzeczywistości preprocesorem, który przetłumaczył kod C ++ na C.

Tak więc bardzo możliwe jest posiadanie klas w C. Możesz spróbować wykopać stary preprocesor C ++ i zobaczyć, jakie rozwiązania tworzy.

Ropucha
źródło
To byłoby cfront; napotkał problemy, gdy wyjątki zostały dodane do C ++ - obsługa wyjątków nie jest trywialna.
Jonathan Leffler,
2

GTK jest zbudowany w całości na C i wykorzystuje wiele koncepcji OOP. Przeczytałem kod źródłowy GTK i jest on imponujący i zdecydowanie łatwiejszy do odczytania. Podstawowa koncepcja jest taka, że ​​każda „klasa” jest po prostu strukturą i związanymi z nią funkcjami statycznymi. Wszystkie funkcje statyczne akceptują strukturę „instancji” jako parametr, robią wszystko, co trzeba, i zwracają wyniki, jeśli to konieczne. Na przykład możesz mieć funkcję „GetPosition (CircleStruct obj)”. Funkcja po prostu przeszukałaby strukturę, wyodrębniłaby numery pozycji, prawdopodobnie utworzyłaby nowy obiekt PositionStruct, przykleiłaby x i y do nowego PositionStruct i zwróciłaby go. GTK nawet implementuje dziedziczenie w ten sposób, osadzając struktury wewnątrz struktur. całkiem sprytny.

rakiety są szybkie
źródło
1

Chcesz wirtualnych metod?

Jeśli nie, po prostu zdefiniujesz zestaw wskaźników funkcji w samej strukturze. Jeśli przypiszesz wszystkie wskaźniki funkcji do standardowych funkcji C, będziesz mógł wywoływać funkcje z C w bardzo podobnej składni, jak w C ++.

Jeśli chcesz mieć metody wirtualne, sprawa staje się bardziej skomplikowana. Zasadniczo będziesz musiał zaimplementować własną tabelę VTable do każdej struktury i przypisać wskaźniki funkcji do tabeli VTable w zależności od wywoływanej funkcji. Potrzebny byłby wtedy zestaw wskaźników funkcji w samej strukturze, które z kolei wywołują wskaźnik funkcji w tabeli VTable. To jest zasadniczo to, co robi C ++.

Jednak TBH ... jeśli chcesz tego drugiego, prawdopodobnie lepiej będzie po prostu znaleźć kompilator C ++, którego możesz użyć i ponownie skompilować projekt. Nigdy nie rozumiałem obsesji na punkcie tego, że C ++ nie nadaje się do użytku we wbudowanym. Używałem go wiele razy i działa szybko i nie ma problemów z pamięcią. Oczywiście, musisz być bardziej ostrożny w tym, co robisz, ale to naprawdę nie jest takie skomplikowane.

Goz
źródło
Już to powiedziałem i powiem to jeszcze raz, ale powiem to jeszcze raz: Nie potrzebujesz wskaźników funkcji ani możliwości wywoływania funkcji ze struktur w stylu C ++ do tworzenia OOP w C, OOP polega głównie na dziedziczeniu funkcjonalności i zmiennych (zawartość) z których oba można osiągnąć w C bez wskaźników funkcji lub powielania kodu.
yyny
0

C nie jest językiem OOP, jak słusznie zauważyłeś, więc nie ma wbudowanego sposobu na napisanie prawdziwej klasy. Najlepiej jest spojrzeć na struktury i wskaźniki do funkcji , które pozwolą ci zbudować przybliżenie klasy. Jednakże, ponieważ C jest językiem proceduralnym, możesz rozważyć napisanie bardziej podobnego do C kodu (tj. Bez próby używania klas).

Ponadto, jeśli możesz użyć C, prawdopodobnie możesz użyć C ++ i uzyskać klasy.

Benzoes
źródło
4
Nie będę głosował przeciw, ale FYI, wskaźniki funkcji lub możliwość wywoływania funkcji ze struktur (co, jak przypuszczam, jest twoim zamiarem) nie ma nic wspólnego z OOP. OOP dotyczy głównie dziedziczenia funkcjonalności i zmiennych, które można osiągnąć w C bez wskaźników funkcji i duplikatów.
yyny