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: 124
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: 144
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: 592
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: 124
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


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ść