Programowanie obiektowe w C

W tym miejscu zadajemy pytania na temat języka C, dzielimy się swoją wiedzą, udzielamy wsparcia, rozwiązujemy problemy programistyczne.
Awatar użytkownika
xor
User
User
Posty: 139
Rejestracja: poniedziałek 05 wrz 2016, 21:44

Programowanie obiektowe w C

Postautor: xor » czwartek 07 cze 2018, 11:28

Cześć!
Niedawno natknąłem się na ciekawy materiał pokazujący zastosowanie zasad programowania obiektowego w języku C. Materiał jest częścią dokumentacji frameworka QP/C i dosyć ściśle z nim związany, ale niezależnie od tego sam w sobie wydaje mi się na tyle wartościowy że uznałem iż warto go omówić. Część technik tam omówionych, myślę że warto stosować w praktyce, a mi osobiście materiał pozwolił zrozumieć idee stojące u fundamentów programowania obiektowego.
Z oryginałem można się zapoznać tutaj: https://state-machine.com/doc/AN_OOP_in_C.pdf. Nie będę wchodził zbytnio w szczegóły tylko bardzo skrótowo opiszę podane zasady i przejdę do praktycznego ćwiczenia.

U podstaw programowania obiektowego leżą trzy zasady: hermetyzacja, dziedziczenie, polimorfizm.

1.Enkapsulacja to zamknięcie składowych klasy (klasa to, powiedzmy w tym wypadku, niewielki, jednoplikowy, moduł programowy) w jakiejś zamkniętej całości. Na gruncie języka C polega to na zgrupowaniu atrybutów klasy w strukturze. Instancje klasy powstają poprzez powołanie zmiennej o tymże typie strukturalnym. Wywołanie metody danej klasy następuje przez wywołanie funkcji z przekazaniem wskaźnika do tej zmiennej.
2.Dziedziczenie to tworzenie nowej klasy (subklasy) na podstawie klasy już istniejącej (klasy bazowej). Utworzona subklasa wykorzystuje metody i atrybuty klasy bazowej ale ją rozszerza przez wprowadzenie swoich własnych atrybutów i metod. Na gruncie języka C polega to na włączeniu struktury klasy bazowej do struktury subklasy. Struktura klasy bazowej musi być włączona na początku struktury subklasy dzięki temu zmienna będąca instancją subklasy może poprzez rzutowanie typu być wykorzystana w wywołaniu metod klasy bazowej jak i metod subklasy.
3.Polimorfizm to możliwość odmiennego zachowania danej metody przy wywołaniu dla klasy bazowej a innego dla subklasy. Pozwala to, z jednej strony na uniwersalne (generyczne) zaprogramowanie wywołania metody, z drugiej strony na uzyskanie odmiennego efektu dla różnych klas. Jest kilka odmian polimorfizmu, w przytoczonym materiale przedstawione są metody wirtualne, czyli ustalane na etapie wykonania (late binding). Polimorfizm w tym przypadku realizowany jest przez włączenie do struktury klasy wskaźnika na statyczną funkcję realizującą daną metodę.

To tyle jeśli chodzi o teorię. Ażeby teoria lepiej wchodziła do głowy warto ją przećwiczyć praktycznie. Na potrzeby treningu, jednym okiem patrząc na przykłady podane w przytoczonym materiale, wymyśliłem sobie bazową klasę o nazwie "Bufor" z dwiema klasami dziedziczącymi "Lifo" i "Fifo". Jak widać po nazwach klasy będą miały za zadanie umożliwić przechowanie jakiejś puli danych a następnie dostęp do tych danych. Klasa Lifo będzie "oddawać" zapisane dane w kolejności od ostatnio zapisanego elementu, klasa Fifo będzie oddawać od pierwszego zapisanego elementu. Klasa Bufor będzie realizować większość metod ale nie umożliwi dostępu do danych.
Nagłówek klasy Bufor niech będzie następujący:

Kod: Zaznacz cały

#ifndef BUFOR_H_
#define BUFOR_H_

#define MAXSIZE   100   //maksymalna wielkość bufora

struct BuforVtbl;
typedef struct {
   struct BuforVtbl const *vptr;
   char *buf;      //wskaźnik na obszar pamięci bufora
   int size;      //wielkość bufora
   int saved_num;   //ilość zapisanych elementów
   int last;       //indeks do ostatnio zapisanego elementu
} Bufor;

struct BuforVtbl {
   int (*fetch)(Bufor * const me, char *elem);
};

void Bufor_ctor(Bufor * const me, int size);      //zainicjowanie bufora o wiekości size<=MAXSIZE
int Bufor_stash(Bufor * const me, char element);   //zapisanie znaku w buforze
int Bufor_fetch(Bufor * const me, char *elem);      //zdjęcie znaku z bufora

#endif /* BUFOR_H_ */


typedef {} Bufor; to typ klasy, struct BuforVtbl jest typem pomocniczym na potrzeby polimorfizmu. Metoda Bufor_ctor() to konstruktor klasy, który w języku C trzeba wywołać jawnie przed pierwszym użyciem klasy (w C++ konstruktor jest wywoływany niejawnie). Metody Bufor_stash() i Bufor_fetch() to oczywiście metody klasy, odpowiednio, zapisująca dane w buforze i odczytująca dane z bufora. W przypadku klasy Bufor metoda Bufor_fetch jest "pure virtual" czyli metodą abstrakcyjną, której nie można wywołać i której implementacja musi być dostarczona przez klasę dziedziczącą. A oto definicje metod tej klasy:

Kod: Zaznacz cały

#include <assert.h>
#include "bufor.h"

static int Bufor_fetch_(Bufor * const me, char *elem);

static struct BuforVtbl const vtbl = {
      Bufor_fetch_
};

void Bufor_ctor(Bufor * const me, int size)   //zainicjowanie bufora o wiekości size<=MAXSIZE
{
   static char buffer[MAXSIZE];

   me->vptr = &vtbl;

   me->buf = buffer;
   me->size = size > MAXSIZE ? MAXSIZE : size;
   me->saved_num = 0;
   me->last = -1;
}

int Bufor_stash(Bufor * const me, char element)   //wepchnięcie danej do bufora
{
   if(me->saved_num < me->size)
   {
      int tmp = ++me->last;
      tmp %= me->size;   //zawijanie indeksu dla bufora kołowego
      me->saved_num++;
      me->buf[tmp] = element;
      return 0;
   }
   else
   {
      return -1;
   }

}

//wywołanie funkcji wirtualnej
int Bufor_fetch(Bufor * const me, char *elem)
{
   return (*me->vptr->fetch)(me,elem);
}


static int Bufor_fetch_(Bufor * const me, char *elem)
{
   assert(0);/* purely-virtual function should never be called */
   return -1;
}


Ażeby nie grzęznąć w alokacjach dynamicznych, na potrzeby bufora zarezerwowałem statyczną tablicę o jakiejś tam wielkości, z której konkretnie wykorzystywana jest tylko część podana w parametrze konstruktora klasy. Jak widać konstruktor oprócz inicjacji zwykłych atrybutów klasy, dokonuje też inicjacji zpolimorfizowanej metody _fetch. Odbywa się to przez przypisanie do vtbl wskaźnika na statyczną funkcję implementującą metodę Bufor_fetch_() (uwaga na podkreślnik w nazwie). Funkcja-metoda o widoczności zewnętrznej Bufor_fetch() (bez podkreślnika) jest tylko funkcją pośrednią, klejem łączącym interfejs z implementacją. Można równie dobrze zrealizować ją jako makro

Kod: Zaznacz cały

#define Bufor_fetch(me_,elem_) ((*(me_)->vptr->fetch)((me_),(elem_)))


Nagłówek klasy Lifo:

Kod: Zaznacz cały

#ifndef LIFO_H_
#define LIFO_H_

#include "bufor.h"

typedef struct {
   Bufor super;   //dziedziczenie klasy
} Lifo;

void Lifo_ctor(Lifo * const me, int size);   //zainicjowanie bufora o wiekości size<=MAXSIZE

#endif /* LIFO_H_ */



Klasa Lifo nie wprowadza żadnych dodatkowych atrybutów i metod, ale dostarcza implementacji dla metody _fetch, dlatego potrzebuje własnego konstruktora.

Kod: Zaznacz cały

#include "lifo.h"

static int Lifo_fetch_(Bufor * const me, char *elem);

static struct BuforVtbl const vtbl = {
      Lifo_fetch_
};

void Lifo_ctor(Lifo * const me, int size)
{
   Bufor_ctor(&me->super, size);
   me->super.vptr = &vtbl;
}


//Wirtualna funkcja Bufor_fetch dla klasy Lifo zwraca elementy "od końca"
static int Lifo_fetch_(Bufor * const me, char *elem)
{
   Lifo * me_ = (Lifo*)me;

   if( me_->super.saved_num > 0 )
   {
      me_->super.saved_num--;
      *elem = me_->super.buf[me_->super.last--];

      return 0;
   }
   else
      return -1;
}


W konstruktorze klasy Lifo widać przejawy dziedziczenia: najpierw wywoływany jest konstruktor klasy bazowej, następnie wykonuje się uzupełniające operacje dla atrybutów i metod specyficznych dla klasy. W tym wypadku jest to reinicjacja wskaźnika dla zreimplementowanej metody _fetch().

No i w końcu kod dla klasy Fifo:

Kod: Zaznacz cały

#ifndef FIFO_H_
#define FIFO_H_

#include "bufor.h"

typedef struct {
   Bufor super;   //Dziedziczenie klasy Bufor
   int first;      //indeks komórki przed elementem kolejnym do zwrócenia
} Fifo;

void Fifo_ctor(Fifo * const me, int size);   //zainicjowanie bufora o wiekości size<=MAXSIZE

#endif /* FIFO_H_ */


Klasa wprowadza dodatkowy atrybut potrzebny metodzie _fetch() (tak naprawdę funkcję można by napisać bez dodatkowego atrybutu, ale przecież chodzi o ćwiczenie).

Kod: Zaznacz cały

#include "fifo.h"

static int Fifo_fetch_(Bufor * const me, char *elem);

static struct BuforVtbl const vtbl = {
      Fifo_fetch_
};

void Fifo_ctor(Fifo * const me, int size)
{
   Bufor_ctor(&me->super, size);
   me->super.vptr = &vtbl;
   me->first = -1;
}

//Wirtualna funkcja Bufor_fetch dla klasy Fifo zwraca elementy "od początku"
static int Fifo_fetch_(Bufor * const me, char *elem)
{
   Fifo * me_ = (Fifo*)me;

   if( me_->super.saved_num > 0 )
   {
      int tmp = ++me_->first;
      tmp %= me_->super.size;
      me_->super.saved_num--;
      *elem = me_->super.buf[tmp];

      return 0;
   }
   else
      return -1;
}


Tu nic nowego się nie dzieje więc nie ma co opisywać.

Na koniec prościutki program mający za zadanie powołać dwa bufory, jeden klasy Lifo, drugi klasy Fifo, zapisać do nich po 10 pierwszych liter alfabetu w kolejności rosnącej a następnie odczytać zapisane znaki. Dla bufora Lifo odczytane znaki będą pojawiać się w kolejności malejącej, dla bufora Fifo w kolejności rosnącej.

Kod: Zaznacz cały

#include <stdio.h>
#include <stdint.h>
#include "lifo.h"
#include "fifo.h"

int main()
{
   Lifo lifo_buffer;
   Fifo fifo_buffer;

   Lifo_ctor(&lifo_buffer, 10);
   Fifo_ctor(&fifo_buffer, 10);

   for(char i = 0, ch='A'; i<10; i++, ch++)
   {
      Bufor_stash((Bufor*)&lifo_buffer, ch);
      Bufor_stash((Bufor*)&fifo_buffer, ch);
   }

   for(char i = 0, ch; i<10; i++)
   {
      Bufor_fetch((Bufor*)&lifo_buffer, &ch);
      putchar(ch);
   }
   putchar('\n');

   for(char i = 0, ch; i<10; i++)
   {
      Bufor_fetch((Bufor*)&fifo_buffer, &ch);
      putchar(ch);
   }
   putchar('\n');

   return 0;
}


Jak widać, odczyt znaków z obu buforów odbywa się w identyczny sposób, jednak dzięki polimofizmowi uzyskane efekty są inne.
Niestety polimorfizm w tym kształcie jest bardzo kłopotliwy i nieodporny na błędy. Proponuję "omyłkowo" wywołać konstruktor dla bufora lifo w poniższy sposób i zobaczyć co się stanie.

Kod: Zaznacz cały

Bufor_ctor(&lifo_buffer, 10);

Z kolei wywołanie czegoś takiego

Kod: Zaznacz cały

Fifo_ctor(&lifo_buffer, 10);

prawdopodobnie skończy się katastrofą.
Sam autor materiału pisze: jeśli chcesz używać polimorfizmu na większą skalę, prawdopodobnie powinieneś przerzucić się na C++.

To właściwie tyle w tym temacie. Trochę niefajnie, że wnętrzności struktury klasowej są wystawione na widok w pliku nagłówkowym. Dlatego w drugiej fazie ćwiczenia przerobiłem trochę kod wprowadzając tzw. opaque pointer. W tej postaci klasa ujawnia publiczności tylko to co jest niezbędne. Druga wersja w załączniku.

Have fun!
Nie masz wymaganych uprawnień, aby zobaczyć pliki załączone do tego posta.
Ostatnio zmieniony środa 13 cze 2018, 19:37 przez xor, łącznie zmieniany 1 raz.

Awatar użytkownika
mokrowski
User
User
Posty: 168
Rejestracja: czwartek 08 paź 2015, 20:50
Lokalizacja: Tam gdzie Centymetro

Re: Programowanie obiektowe w C

Postautor: mokrowski » czwartek 07 cze 2018, 11:50

Super :) Od siebie polecę jeszcze odnalezienie pliku ooc.pdf w sieci. To książka (już dość leciwa) o programowaniu obiektowym w C. Pokazuje jeszcze więcej sposobów implementacji konceptów które pokazałeś. Jak ktoś będzie uparty to i dla programowania funkcyjnego w C także coś znajdzie :)
Stosując mechanizmy C (włącznie z makrami), da się zaimplementować wszystkie właściwości obiektowe znane z innych języków oraz teorii OOP. Przecież "w czymś" te wszystkie Ruby, Python'y, JavaScript'y i Javy są napisane :) Problem jest tylko jeden. Kod nie będzie odzwierciedlał jawnie konceptów stojących za myśleniem obiektowym osoby implementującej. To jest bardzo poważna wada dyskwalifikująca język w niektórych zastosowaniach. Aparat analityczny jest w tej chwili najlepiej sprawdzony właśnie co do OOP (ang. Object Oriented Programming). Paradygmaty proceduralne, funkcyjne czy deklaratywne, nie są jeszcze tak silnie wspierane przez techniki analizy.
Zdarzało mi się stosować podejście obiektowe w C w kilku projektach. Z mojego doświadczenia, proste koncepty się sprawdzają (wszyscy w zespole je rozumieją). Te bardziej zaawansowane, wymagają już znajomości OOP (np. rozumienia polimorfizmu) co już nie jest takie częste u osoby znającej wyłącznie C.

Pozwolę sobie dopisać ew. uwagi w tym poście po dokładnym przeczytaniu artykułu. To takie uwagi na gorąco.

Nie mam uwag do samego wpisu. Jako refleksję co do zasadności, warto kontynuować implementację w kierunku "obiektu z metodami". Czyli struktury ze wskaźnikami na metody. Zakończenie na "generycznym void *" może pokazać czy i gdzie warto się zatrzymać :)
,,Myślenie nie jest łatwe, ale można się do niego przyzwyczaić" - Alan Alexander Milne: Kubuś Puchatek

Awatar użytkownika
dambo
Expert
Expert
Posty: 601
Rejestracja: czwartek 17 mar 2016, 17:12

Re: Programowanie obiektowe w C

Postautor: dambo » czwartek 07 cze 2018, 19:13

To od siebie w temacie mogę polecić książkę "Interfejsy i implementacje w języku C" oraz stream Gynvaela: "Gynvael's Livestream #67: Pseudoobiektowość w C" na yt.
Zapraszam na mojego pseudobloga z projektami itp: http://projektydmb.blogspot.com/

Awatar użytkownika
xor
User
User
Posty: 139
Rejestracja: poniedziałek 05 wrz 2016, 21:44

Re: Programowanie obiektowe w C

Postautor: xor » środa 13 cze 2018, 19:32

I jeszcze prezentacja, która właściwie nic nowego nie wnosi ale dosyć fajnie podsumowuje:
https://www.physik.uni-muenchen.de/lehr ... _c_ooc.pdf

Awatar użytkownika
j23
User
User
Posty: 384
Rejestracja: czwartek 08 paź 2015, 18:40

Re: Programowanie obiektowe w C

Postautor: j23 » sobota 17 lis 2018, 00:10

mokrowski pisze:(...)Problem jest tylko jeden. Kod nie będzie odzwierciedlał jawnie konceptów stojących za myśleniem obiektowym osoby implementującej.(...) Te bardziej zaawansowane, wymagają już znajomości OOP (np. rozumienia polimorfizmu) co już nie jest takie częste u osoby znającej wyłącznie C(...)
Jestem pewny, że Kolega Mokrowski o tym doskonale wie, ale pozwolę sobie dopisać, że dla aplikacji typu embedded jest coś takiego jak mruby - tzn. wg tego co piszą: "(...)mruby jest lekką implementacją języka Ruby, która może być połączona i osadzona w aplikacji.(...)".
Tutaj w linkach poniżej jest wyjaśnione co i jak z tym "mruby":
http://mruby.org/docs/
http://mruby.org/docs/articles/executing-ruby-code-with-mruby.html
http://mruby.org/docs/api/
Od siebie -na tyle ile mogę się wypowiedzieć- uważam, że Ruby jest całkiem zacnym językiem :) i nie można go ignorować, jeśli nie chce się "pozostawać w tyle". Chyba najlepiej oddają to słowa opisujące język na jego oficjalnej stronie (także dostępnej w języku polskim):
Ruby jest językiem starannie dobranej równowagi. Jego twórca, Yukihiro “Matz” Matsumoto, połączył części jego ulubionych języków (Perla, Smalltalka, Eiffel, Ady i Lispa) by uformować nowy język, który zbalansował programowanie funkcjonalne wraz z programowanie imperatywnym.

Matz często mówi, że chce uczynić ten język naturalnym – nie prostym – w sposób odzwierciedlający życie.

Bazując na tym dodaje:
Ruby jest prosty z wyglądu, ale bardzo skomplikowany w środku, tak jak ciało ludzkie.

Chciałbym też od siebie dodać, że Ruby to nie jest jakiś super nowy język, z tym że od momentu jego powstania (dokładnie pamiętam te czasy) język ten niesamowicie ewaluował i -IMHO- obecnie prezentuje się bardzo okazale. Najbardziej podoba mi się to stwierdzenie o Ruby, że jest to język starannie zbalansowany pod kątem wydajności, udokumentowania, prostoty instalacji i ogólnego używania (zgodności z różnymi IDE, np.Netbeans). Ruby może być stosowany jako taka "nakładka" na język C właśnie po to aby ułatwić podejście obiektowe, o którym wspomina Kolega Mokrowski.

73! J23

Awatar użytkownika
mokrowski
User
User
Posty: 168
Rejestracja: czwartek 08 paź 2015, 20:50
Lokalizacja: Tam gdzie Centymetro

Re: Programowanie obiektowe w C

Postautor: mokrowski » niedziela 18 lis 2018, 14:36

Nie wiem czy o innych językach tak bym napisał. Cóż.. ja Ruby ... ignoruję. Powody?
1. Co do filozofii powtórka z Perl'a (wiem co mówię i wiem jakie konsekwencje ma ta filozofia... nie chcę tego z całych sił... ). Nigdy więcej takiej traumy.
2. Co do zasady działania, nie.. nie zgadzam się z narzutem jaki proponuje ten język ani w w fazie wykonania ani redakcji kodu.
3. W mojej ocenie nie oferuje mi nic (dziś) czego nie mam w językach które znam a także w przyszłości nie sądzę żeby mógł zaoferować.
4. Ocena rynku stała się jednoznaczna. W zasadzie ROR w Web. i długo nic (jasne, znam i inne projekty a piszę wyłącznie o postrzeganiu rynku). Ten sektor także w mojej ocenie ma silniejszych graczy. Może i "gorszych/mniej zaawansowanych" ale z większym zasięgiem.

Co nie znaczy oczywiście że odradzam język jeśli ktoś uczy się go jako 2 po C :-) Z pewnością będzie zadowolony bo ma zupełnie inne podejście. Nie należy jednak ukrywać że w embedded, jeśli chodzi o język "pomocniczy", raczej Python... (bo już pomysłów typu TCL nie będę wspominał). Byłbym zaskoczony gdyby jakikolwiek klient używał do automatyzacji testowania lub generowania kodu Ruby i zadawał bym sobie pytanie czy nie jest to objaw "silnej osobowości na stanowisku technicznym-kierowniczym" :)

Z podstawowych dla embedded, zerkał bym na Rust'a. Na razie dobre obietnice (w końcu da się pisać bezpiecznie). Na razie obietnice... :-)

Cóż... wyszło pesymistycznie...
,,Myślenie nie jest łatwe, ale można się do niego przyzwyczaić" - Alan Alexander Milne: Kubuś Puchatek

Awatar użytkownika
j23
User
User
Posty: 384
Rejestracja: czwartek 08 paź 2015, 18:40

Re: Programowanie obiektowe w C

Postautor: j23 » wtorek 20 lis 2018, 00:13

Dziękuję Kolego Mokrowski za szybką odpowiedź. Wcale nie wyszło pesymistycznie, tylko konkretnie i jednoznacznie i dokladnie tak jak jest, tzn. jak sytuacja wygląda. Podpisuję się pod tym co napisałeś obiema rękami. Faktycznie - mi osobiście na chwilę obecną język Ruby jest nie tyle sam w sobie pomocny jak to, że jego obecność jest wymagana do korzystania z takich technologii webowych jak SASS czy LESS (rozbudowane i bardziej modularne podjeście do CSS). Mimo, iż w CSS wykorzystuję własne pomysły (np. biblioteki graficzne napisane w Javascript/JQuery do generowania grafiki 2D) to chcę spróbować tej technologii SASS/LESS z prostej przyczyny, a mianowicie umiejętność korzystania z SASS/LESS jest to czasem wymagane przy projektach webowych.
Sprawa Ruby embedded: faktycznie nie jest tak pięknie jak na początku myślałem, tzn. owszem Ruby wspomaga język C w projektach embedded, ale ZAKRES tego supportu jest póki co dość znikomy. Cóż, mruby jest w fazie początkowego rozwoju, no a jeśli brać pod uwagę możliwość wykorzystania mruby pod Linux (bo inaczej wygląda mruby pod Windows i pod MacOS) to powiedziałbym, że mruby w Linuxie dopiero raczkuje.

Osobiście wolę pragmatyczne podejście do spraw związanych z programowaniem. Wszelkie obietnice (wynikająca z tego powodu często niezgodność dokumentacji ze stanem faktycznym - np.niektóre frameworki webowe, lub funkcje cms'ów) generalnie mnie niesamowicie wkurzają. IMHO niektóre dokumentacje piszą jacyś wyjątkowo niekompetentni, infantylni ludzie. ;)

Pozdrawiam! J23
Bo ona jest piękniejsza niż słońce
i wszelki gwiazdozbiór.
Porównana ze światłością-uzyska pierwszeństwo
(...)Mądrości zło nie przemoże
Mdr 7,29


Wróć do „Pisanie programów w C”

Kto jest online

Użytkownicy przeglądający to forum: Obecnie na forum nie ma żadnego zarejestrowanego użytkownika i 1 gość