Jaki byłby zestaw sprytnych hacków preprocesora (kompatybilnych z ANSI C89 / ISO C90), które umożliwiają jakąś brzydką (ale użyteczną) orientację obiektową w C?
Znam kilka różnych języków zorientowanych obiektowo, więc nie odpowiadaj na pytania typu „Naucz się C ++!”. Przeczytałem " Programowanie obiektowe z ANSI C " (uwaga: format PDF ) i kilka innych ciekawych rozwiązań, ale najbardziej interesuje mnie twoje :-)!
Zobacz także Czy potrafisz pisać kod zorientowany obiektowo w C?
Odpowiedzi:
C Object System (COS) brzmi obiecująco (nadal jest w wersji alfa). Stara się ograniczyć do minimum dostępne koncepcje ze względu na prostotę i elastyczność: jednolite programowanie obiektowe, w tym klasy otwarte, metaklasy, metaklasy właściwości, rodzaje generyczne, multimetody, delegowanie, własność, wyjątki, kontrakty i zamknięcia. Istnieje szkic dokumentu (PDF), który to opisuje.
Wyjątkiem w C jest implementacja C89 TRY-CATCH-FINALLY znaleziona w innych językach OO. Zawiera zestaw testowy i kilka przykładów.
Zarówno Laurent Deniau, który pracuje dużo na OOP w C .
źródło
Odradzałbym używanie preprocesora (ab) do prób nadawania składni C bardziej podobnej do innego, bardziej obiektowego języka. Na najbardziej podstawowym poziomie po prostu używasz zwykłych struktur jako obiektów i przekazujesz je za pomocą wskaźników:
Aby uzyskać takie rzeczy, jak dziedziczenie i polimorfizm, musisz trochę bardziej popracować. Możesz zrobić ręczne dziedziczenie, mając pierwszy element struktury jako instancję nadklasy, a następnie możesz swobodnie rzutować wskaźniki na klasy bazowe i pochodne:
Aby uzyskać polimorfizm (tj. Funkcje wirtualne), użyj wskaźników funkcji i opcjonalnie tabel wskaźników funkcji, znanych również jako tabele wirtualne lub vtables:
I tak robi się polimorfizm w C. Nie jest ładny, ale spełnia swoje zadanie. Występują pewne przyklejone problemy dotyczące rzutowania wskaźnika między klasami podstawowymi i pochodnymi, które są bezpieczne, o ile klasa bazowa jest pierwszym składnikiem klasy pochodnej. Dziedziczenie wielokrotne jest znacznie trudniejsze - w takim przypadku, aby dokonać wielkości liter między klasami bazowymi innymi niż pierwsza, musisz ręcznie dostosować wskaźniki w oparciu o odpowiednie przesunięcia, co jest naprawdę trudne i podatne na błędy.
Inną (trudną) rzeczą, którą możesz zrobić, jest zmiana typu dynamicznego obiektu w czasie wykonywania! Po prostu przypisujesz mu nowy wskaźnik vtable. Możesz nawet selektywnie zmieniać niektóre funkcje wirtualne, zachowując inne, tworząc nowe typy hybrydowe. Po prostu uważaj, aby utworzyć nową tabelę vtable zamiast modyfikować globalną tabelę vtable, w przeciwnym razie przypadkowo wpłyniesz na wszystkie obiekty danego typu.
źródło
struct derived {struct base super;};
jest oczywistym do odgadnięcia, jak to działa, ponieważ według kolejności bajtów jest poprawne.Kiedyś pracowałem z biblioteką C, która została zaimplementowana w sposób, który wydał mi się dość elegancki. Napisali w C sposób definiowania obiektów, a następnie dziedziczenia po nich, dzięki czemu były tak samo rozszerzalne jak obiekt w C ++. Podstawowy pomysł był taki:
Dziedziczenie jest trudne do opisania, ale zasadniczo wyglądało to tak:
Następnie w innym pliku:
Wtedy mógłbyś mieć furgonetkę utworzoną w pamięci i używaną przez kod, który wiedział tylko o pojazdach:
Działało pięknie, a pliki .h dokładnie definiowały, co powinieneś być w stanie zrobić z każdym obiektem.
źródło
Pulpit GNOME dla Linuksa jest napisany w zorientowanym obiektowo C i ma model obiektowy o nazwie „ GObject ”, który obsługuje właściwości, dziedziczenie, polimorfizm, a także inne dodatki, takie jak referencje, obsługa zdarzeń (zwanych „sygnałami”), środowisko wykonawcze pisanie, prywatne dane itp.
Zawiera hacki preprocesora do wykonywania rzeczy, takich jak rzutowanie typów w hierarchii klas, itp. Oto przykładowa klasa, którą napisałem dla GNOME (rzeczy takie jak gchar są typedefami):
Źródło klasy
Nagłówek klasy
Wewnątrz struktury GObject znajduje się liczba całkowita GType, która jest używana jako magiczna liczba dla systemu dynamicznego pisania GLib (możesz rzutować całą strukturę na „GType”, aby znaleźć jej typ).
źródło
Robiłem tego typu rzeczy w C, zanim wiedziałem, co to jest OOP.
Poniżej znajduje się przykład, który implementuje bufor danych, który rośnie na żądanie, biorąc pod uwagę minimalny rozmiar, przyrost i maksymalny rozmiar. Ta konkretna implementacja była oparta na "elemencie", co oznacza, że została zaprojektowana tak, aby umożliwić zbiór podobny do listy dowolnego typu C, a nie tylko bufor bajtowy o zmiennej długości.
Chodzi o to, że obiekt jest tworzony za pomocą xxx_crt () i usuwany za pomocą xxx_dlt (). Każda z metod „składowych” wymaga do działania określonego typu wskaźnika.
Zaimplementowałem w ten sposób listę połączoną, bufor cykliczny i wiele innych rzeczy.
Muszę przyznać, że nigdy nie zastanawiałem się, jak wdrożyć dziedziczenie przy takim podejściu. Wyobrażam sobie, że połączenie tego, co oferuje Kieveli, może być dobrą drogą.
dtb.c:
dtb.h
PS: vint był po prostu typem int - użyłem go, aby przypomnieć mi, że jego długość była zmienna w zależności od platformy (w celu przeniesienia).
źródło
Nieco nie na temat, ale oryginalny kompilator C ++, Cfront , skompilował C ++ do C, a następnie do assemblera.
Tutaj zachowane .
źródło
Jeśli myślisz o metodach wywoływanych na obiektach jako o metodach statycznych, które przekazują niejawny '
this
' do funkcji, może to ułatwić myślenie OO w C.Na przykład:
staje się:
Czy jakoś tak.
źródło
string->length(s);
ffmpeg (zestaw narzędzi do przetwarzania wideo) jest napisany w prostym C (i języku asemblera), ale przy użyciu stylu obiektowego. Jest pełen struktur ze wskaźnikami funkcji. Istnieje zestaw funkcji fabrycznych, które inicjalizują struktury za pomocą odpowiednich wskaźników „metod”.
źródło
Jeśli naprawdę myśli catefully, nawet standardowe użycie biblioteki C OOP - rozważyć
FILE *
jako przykład:fopen()
inicjujeFILE *
obiekt i użyć go użyć metod członkówfscanf()
,fprintf()
,fread()
,fwrite()
i innych, a ostatecznie sfinalizować jąfclose()
.Możesz także pójść drogą pseudo-celu-C, która również nie jest trudna:
Używać:
Oto, co może wynikać z takiego kodu Objective-C, jak ten, jeśli używany jest dość stary translator Objective-C-to-C:
źródło
__attribute__((constructor))
wvoid __meta_Foo_init(void) __attribute__((constructor))
?popen(3)
zwraca również aFILE *
dla innego przykładu.Myślę, że to, co opublikował Adam Rosenfield, to właściwy sposób robienia OOP w C. Chciałbym dodać, że to, co pokazuje, to implementacja obiektu. Innymi słowy, rzeczywista implementacja zostanie umieszczona w pliku
.c
pliku, a interfejs w nagłówku.h
pliku . Na przykład, używając powyższego przykładu małpy:Interfejs wyglądałby następująco:
W
.h
pliku interfejsu widać, że definiujesz tylko prototypy. Następnie możesz skompilować część dotyczącą implementacji ".c
plik” do biblioteki statycznej lub dynamicznej. Tworzy to hermetyzację, a także możesz dowolnie zmieniać implementację. Użytkownik twojego obiektu nie musi prawie nic wiedzieć o jego implementacji. To również kładzie nacisk na ogólny projekt obiektu.Osobiście uważam, że oop jest sposobem na konceptualizację struktury kodu i możliwości ponownego wykorzystania i nie ma tak naprawdę nic wspólnego z innymi elementami dodanymi do c ++, takimi jak przeciążanie lub szablony. Tak, są to bardzo przydatne, przydatne funkcje, ale nie są reprezentatywne dla tego, czym naprawdę jest programowanie obiektowe.
źródło
typedef struct Monkey {} Monkey;
Jaki jest cel wpisywania jej po utworzeniu?struct _monkey
to po prostu prototyp. Rzeczywista definicja typu jest zdefiniowana w pliku implementacji (plik .c). Tworzy to efekt hermetyzacji i umożliwia programiście interfejsu API ponowne zdefiniowanie struktury małpy w przyszłości bez modyfikowania interfejsu API. Użytkownicy interfejsu API muszą zajmować się tylko rzeczywistymi metodami. Projektant API dba o implementację, w tym sposób ułożenia obiektu / struktury. Zatem szczegóły obiektu / struktury są ukryte przed użytkownikiem (typ nieprzezroczysty).int getCount(ObjectType obj)
itp., Jeśli zdecydujesz się zdefiniować strukturę w pliku implementacji.Moja rada: nie komplikuj. Jednym z największych problemów, jakie mam, jest konserwacja starszego oprogramowania (czasami starszego niż 10 lat). Jeśli kod nie jest prosty, może być trudny. Tak, można napisać bardzo przydatne OOP z polimorfizmem w C, ale może być trudne do odczytania.
Wolę proste obiekty, które zawierają pewne dobrze zdefiniowane funkcje. Świetnym tego przykładem jest GLIB2 , na przykład tablica mieszająca:
Klucze to:
źródło
Gdybym miał pisać OOP w CI, prawdopodobnie poszedłbym z pseudo-Pimplem projektem . Zamiast przekazywać wskaźniki do struktur, w końcu przekazujesz wskaźniki do wskaźników do struktur. To sprawia, że treść jest nieprzejrzysta i ułatwia polimorfizm i dziedziczenie.
Prawdziwy problem z OOP w C polega na tym, co się dzieje, gdy zmienne wychodzą z zakresu. Nie ma destruktorów generowanych przez kompilator, co może powodować problemy. Makra mogą pomóc, ale zawsze będzie brzydko na nie patrzeć.
źródło
if
instrukcji i zwalniając je na końcu. Na przykładif ( (obj = new_myObject()) ) { /* code using myObject */ free_myObject(obj); }
Innym sposobem programowania w stylu obiektowym w C jest użycie generatora kodu, który przekształca język specyficzny dla domeny na C. Tak jak w przypadku TypeScript i JavaScript w celu przeniesienia OOP do js.
źródło
Wynik:
Oto pokaz tego, czym jest programowanie obiektowe w C.
To jest prawdziwe, czyste C, bez makr preprocesora. Mamy dziedziczenie, polimorfizm i enkapsulację danych (w tym dane prywatne dla klas lub obiektów). Nie ma szans na chroniony odpowiednik kwalifikatora, co oznacza, że prywatne dane są również prywatne w łańcuchu dziedziczenia. Ale to nie jest niedogodność, ponieważ uważam, że nie jest to konieczne.
CPolygon
nie jest tworzony, ponieważ używamy go tylko do manipulowania obiektami w dół łańcucha dziedziczenia, które mają wspólne aspekty, ale różne ich implementacje (polimorfizm).źródło
@Adam Rosenfield ma bardzo dobre wyjaśnienie, jak osiągnąć OOP w C
Poza tym polecam lekturę
1) pjsip
Bardzo dobra biblioteka C dla VoIP. Możesz dowiedzieć się, jak osiąga OOP, korzystając ze struktur i tabel wskaźników funkcji
2) Środowisko wykonawcze iOS
Dowiedz się, w jaki sposób środowisko wykonawcze iOS napędza Cel C. Osiąga OOP za pomocą wskaźnika isa, meta klasy
źródło
Dla mnie orientacja obiektowa w C powinna mieć następujące cechy:
Hermetyzacja i ukrywanie danych (można osiągnąć za pomocą struktur / nieprzezroczystych wskaźników)
Dziedziczenie i obsługa polimorfizmu (pojedyncze dziedziczenie można osiągnąć za pomocą struktur - upewnij się, że abstrakcyjna podstawa nie jest instancyjna)
Funkcje konstruktora i destruktora (niełatwe do osiągnięcia)
Sprawdzanie typów (przynajmniej dla typów zdefiniowanych przez użytkownika, ponieważ C nie wymusza żadnego)
Liczenie referencji (lub coś do zaimplementowania RAII )
Ograniczona obsługa wyjątków (setjmp i longjmp)
Ponadto powinien opierać się na specyfikacjach ANSI / ISO i nie powinien polegać na funkcjonalności specyficznej dla kompilatora.
źródło
Spójrz na http://ldeniau.web.cern.ch/ldeniau/html/oopc/oopc.html . Jeśli nic innego, przeczytanie dokumentacji jest pouczającym doświadczeniem.
źródło
Trochę spóźniłem się na imprezę, ale lubię unikać obu makro ekstremów - zbyt wiele lub zbyt wiele zaciemnia kod, ale kilka oczywistych makr może ułatwić tworzenie i czytanie kodu OOP:
Myślę, że ma to dobrą równowagę, a błędy, które generuje (przynajmniej przy domyślnych opcjach gcc 6.3) dla niektórych bardziej prawdopodobnych błędów, są pomocne, a nie mylące. Chodzi o to, aby poprawić produktywność programistów, nie?
źródło
Jeśli potrzebujesz napisać mały kod, spróbuj tego: https://github.com/fulminati/class-framework
źródło
Pracuję również nad tym w oparciu o rozwiązanie makro. To chyba tylko dla odważnych ;-) Ale już jest całkiem fajnie, a na dodatek pracuję już nad kilkoma projektami. Działa tak, że najpierw definiujesz oddzielny plik nagłówkowy dla każdej klasy. Lubię to:
Aby zaimplementować klasę, tworzysz dla niej plik nagłówkowy i plik C, w którym implementujesz metody:
W nagłówku, który utworzyłeś dla klasy, dołączasz inne potrzebne nagłówki i definiujesz typy itp. Związane z klasą. Zarówno w nagłówku klasy, jak iw pliku C dołączasz plik specyfikacji klasy (zobacz pierwszy przykład kodu) i X-makro. Te X-makra ( 1 , 2 , 3 itd.) Rozszerzą kod do rzeczywistych struktur klas i innych deklaracji.
Aby odziedziczyć klasę
#define SUPER supername
i dodaćsupername__define \
jako pierwszy wiersz w definicji klasy. Obie muszą tam być. Istnieje również obsługa JSON, sygnały, klasy abstrakcyjne itp.Aby stworzyć obiekt, po prostu użyj
W_NEW(classname, .x=1, .y=2,...)
. Inicjalizacja jest oparta na inicjalizacji struktury wprowadzonej w C11. Działa ładnie i wszystko, czego nie ma na liście, jest ustawione na zero.Aby wywołać metodę, użyj
W_CALL(o,method)(1,2,3)
. Wygląda jak wywołanie funkcji wyższego rzędu, ale jest to tylko makro. Rozszerza się,((o)->klass->method(o,1,2,3))
co jest naprawdę fajnym hackem.Zobacz dokumentację i sam kod .
Ponieważ framework potrzebuje trochę standardowego kodu, napisałem skrypt Perla (wobject), który wykonuje to zadanie. Jeśli tego używasz, możesz po prostu pisać
i utworzy plik specyfikacji klasy, nagłówek klasy i plik C, który zawiera miejsce, w
Point_impl.c
którym zaimplementujesz klasę. Oszczędza sporo pracy, jeśli masz wiele prostych klas, ale nadal wszystko jest w C. wobject jest bardzo prostym skanerem opartym na wyrażeniach regularnych, który można łatwo dostosować do określonych potrzeb lub przepisać od nowa.źródło
Projekt Dynace typu open source właśnie to robi. Jest na https://github.com/blakemcbride/Dynace
źródło
Możesz wypróbować COOP , przyjazny dla programistów framework do OOP w C, zawiera klasy, wyjątki, polimorfizm i zarządzanie pamięcią (ważne dla kodu osadzonego). Jest to stosunkowo lekka składnia, zobacz samouczek na Wiki .
źródło