Przejście z C ++ do C.

83

Po kilku latach programowania w C ++ zaproponowano mi ostatnio pracę z kodowaniem w C, w polu embedded.

Pomijając pytanie, czy odrzucanie C ++ w polu embedded jest dobre czy złe, jest kilka funkcji / idiomów w C ++, których bardzo bym brakował. Żeby wymienić tylko kilka:

  • Ogólne, bezpieczne dla typów struktury danych (przy użyciu szablonów).
  • RAII. Zwłaszcza w funkcjach z wieloma punktami powrotu, np. Nie trzeba pamiętać o zwolnieniu muteksu w każdym punkcie powrotu.
  • Destruktory w ogóle. Oznacza to, że raz napiszesz d'tor dla MyClass, a następnie jeśli instancja MyClass jest członkiem MyOtherClass, MyOtherClass nie musi jawnie deinicjalizować instancji MyClass - jej d'tor jest wywoływany automatycznie.
  • Przestrzenie nazw.

Jakie są Twoje doświadczenia związane z przejściem z C ++ do C?
Jakie substytuty C znalazłeś dla swoich ulubionych funkcji / idiomów C ++? Czy odkryłeś jakieś funkcje C, które chciałbyś mieć C ++?

george
źródło
12
Prawdopodobnie powinno to być wiki społeczności, jeśli pytasz tylko o doświadczenia, a nie o radę.
Peter Alexander,
6
Możesz być zainteresowany Prog.SE .
11
@Peter: Pytania nie mogą być już zadawane CW przez OP, a wymagało to więcej powtórzeń niż miał, kiedy było to jeszcze możliwe. Jeśli uważasz, że pytanie powinno zostać utworzone na wiki społeczności z innego powodu niż umożliwienie większej liczbie użytkowników edytowania postów należących do społeczności, to naprawdę chcesz zamknąć pytanie.
4
Czy to pytanie nie pasowałoby bardziej do programmers.se? Ponieważ jest to zdecydowanie „prawdziwe” pytanie, mówię, że otwieramy je ponownie i głosujemy za jego przeniesieniem. A to niemożliwe. DOBRZE.
Lasse V. Karlsen
21
Przeniesienie nie nastąpi, dopóki prog SE nie wyjdzie z wersji beta, aw każdym razie myślę, że takie podejście do QA jest fraking braindead. Dzieli społeczność, denerwuje użytkowników, dubluje pytania i odpowiedzi. Tworzy chaos niezorganizowanych informacji, które wcześniej były dostępne i dostępne w jednej witrynie „programisty”. Ponadto to takie pytania jak to, mając ogromne widoki i niesamowite głosy za, wkurzają mnie między piątką walczących bliżej a całą społecznością.
Stefano Borini

Odpowiedzi:

68

Pracując nad projektem osadzonym, próbowałem raz pracować w języku C i po prostu nie mogłem tego znieść. Było tak rozwlekłe, że trudno było cokolwiek przeczytać. Podobały mi się również napisane przeze mnie kontenery zoptymalizowane pod kątem osadzania, które musiały zmienić się w znacznie mniej bezpieczne i trudniejsze do naprawienia #definebloki.

Kod, który w C ++ wyglądał następująco:

if(uart[0]->Send(pktQueue.Top(), sizeof(Packet)))
    pktQueue.Dequeue(1);

zamienia się w:

if(UART_uchar_SendBlock(uart[0], Queue_Packet_Top(pktQueue), sizeof(Packet)))
    Queue_Packet_Dequeue(pktQueue, 1);

co wielu ludzi prawdopodobnie powie, że jest w porządku, ale staje się śmieszne, jeśli musisz zrobić więcej niż kilka wywołań „metod” w jednej linii. Dwa wiersze C ++ zamieniłyby się w pięć C (ze względu na 80-znakowe ograniczenia długości wiersza). Oba generowałyby ten sam kod, więc nie obchodziło się to z procesorem docelowym!

Pewnego razu (w 1995) próbowałem napisać dużo C dla wieloprocesorowego programu do przetwarzania danych. Rodzaj, w którym każdy procesor ma własną pamięć i program. Kompilator dostarczony przez dostawcę był kompilatorem C (czymś w rodzaju pochodnej HighC), ich biblioteki były zamkniętymi źródłami, więc nie mogłem używać GCC do budowania, a ich API zostały zaprojektowane z myślą, że twoje programy będą przede wszystkim inicjalizacją / procesem / terminate, więc komunikacja między procesorami była w najlepszym przypadku elementarna.

Dostałem około miesiąca, zanim się poddałem, znalazłem kopię cfront i włamałem ją do plików makefile, abym mógł używać C ++. Cfront nawet nie obsługiwał szablonów, ale kod C ++ był dużo, dużo jaśniejszy.

Ogólne, bezpieczne dla typów struktury danych (przy użyciu szablonów).

Najbliższą rzeczą, jaką C ma do szablonów, jest zadeklarowanie pliku nagłówkowego z dużą ilością kodu, który wygląda następująco:

TYPE * Queue_##TYPE##_Top(Queue_##TYPE##* const this)
{ /* ... */ }

następnie pociągnij go za pomocą czegoś takiego:

#define TYPE Packet
#include "Queue.h"
#undef TYPE

Zauważ, że to nie zadziała dla typów złożonych (np. Bez kolejek unsigned char), chyba że zrobisz typedefpierwszy.

Aha, i pamiętaj, jeśli ten kod nie jest nigdzie używany, to nawet nie wiesz, czy jest poprawny składniowo.

EDYCJA: Jeszcze jedno: musisz ręcznie zarządzać tworzeniem kodu. Jeśli Twój kod „szablonu” nie obejmuje wszystkich funkcji wbudowanych, musisz mieć pewną kontrolę, aby upewnić się, że instancje są tworzone tylko raz, aby linker nie wypluł stosu błędów „wielu wystąpień Foo” .

Aby to zrobić, musisz umieścić elementy niewymienione w sekcji „implementacja” w pliku nagłówkowym:

#ifdef implementation_##TYPE

/* Non-inlines, "static members", global definitions, etc. go here. */

#endif

Następnie w jednym miejscu, w całym kodzie na wariant szablonu , musisz:

#define TYPE Packet
#define implementation_Packet
#include "Queue.h"
#undef TYPE

Ponadto ta sekcja implementacji musi znajdować się poza standardową #ifndef/ #define/ #endiflitanią, ponieważ możesz dołączyć plik nagłówkowy szablonu do innego pliku nagłówkowego, ale musisz później utworzyć instancję w .cpliku.

Tak, szybko robi się brzydko. Dlatego większość programistów C nawet nie próbuje.

RAII.

Zwłaszcza w funkcjach z wieloma punktami powrotu, np. Nie trzeba pamiętać o zwolnieniu muteksu w każdym punkcie powrotu.

Cóż, zapomnij o swoim ładnym kodzie i przyzwyczaj się do tego, że wszystkie punkty powrotu (z wyjątkiem końca funkcji) gotos:

TYPE * Queue_##TYPE##_Top(Queue_##TYPE##* const this)
{
    TYPE * result;
    Mutex_Lock(this->lock);
    if(this->head == this->tail)
    {
        result = 0;
        goto Queue_##TYPE##_Top_exit:;
    }

    /* Figure out `result` for real, then fall through to... */

Queue_##TYPE##_Top_exit:
    Mutex_Lock(this->lock);
    return result;
}

Destruktory w ogóle.

Oznacza to, że raz napiszesz d'tor dla MyClass, a następnie jeśli instancja MyClass jest członkiem MyOtherClass, MyOtherClass nie musi jawnie deinicjalizować instancji MyClass - jej d'tor jest wywoływany automatycznie.

Konstrukcja obiektu musi być jawnie obsługiwana w ten sam sposób.

Przestrzenie nazw.

W rzeczywistości jest to proste do naprawienia: po prostu umieść przedrostek na każdym symbolu. Jest to główna przyczyna wzdęcia źródła, o którym mówiłem wcześniej (ponieważ klasy są niejawnymi przestrzeniami nazw). Ludzie z C żyją tak, cóż, od zawsze i prawdopodobnie nie zobaczą, o co chodzi.

YMMV

Mike DeSimone
źródło
60
Oczywiście nienawidzisz C, jeśli próbujesz wymusić na nim C ++. Wątpię, by C ++ wyglądał świetnie, gdybyś próbował narzucić do niego funkcje z $ more_expressive_language. Nie krytyka twojego posta, tylko spostrzeżenie :-)
Odnośnie techniki goto-zamiast-RAII: czy nie jest to koszmar konserwacji? tj. za każdym razem, gdy dodajesz ścieżkę kodu, która wymaga oczyszczenia, lub nawet po prostu zmieniasz kolejność rzeczy wewnątrz funkcji, musisz pamiętać, aby przejść do etykiet goto na końcu i również je zmienić. Chciałbym zobaczyć technikę, która w jakiś sposób rejestruje kod czyszczenia tuż obok tego, co należy wyczyścić.
george
2
@george: Nienawidzę tego mówić, ale większość osadzonego kodu C, jaki widziałem, jest dość kiepska jak na standardy C. Na przykład pracuję teraz z at91lib Atmela i wymaga to napisania pliku „board.h”, który większość kodu pobiera jako zależność. (Na płycie demonstracyjnej ten nagłówek ma długość 792 wierszy). Ponadto funkcja „LowLevelInit ()”, którą musisz dostosować do swojej tablicy, jest prawie w całości rejestrowana, z liniami takimi jakAT91C_BASE_PMC->PMC_MOR = (0x37 << 16) | BOARD_OSCOUNT | AT91C_CKGR_MOSCRCEN | AT91C_CKGR_MOSCXTEN | AT91C_CKGR_MOSCSEL;
Mike DeSimone
1
Aha, i nic nie mówi ci, że BOARD_OSCOUNT(jaka jest wartość limitu czasu oczekiwania na przełączenie zegara; wyczyść, co?) Jest w rzeczywistości #definein board.h. W tej samej funkcji znajduje się również dużo kodu pętli spinowej kopiuj i wklej, który powinien zostać przekształcony w dwuwierszowy #define(a kiedy to zrobiłem, zapisał kilka bajtów kodu i utworzył bardziej czytelną funkcją dzięki wyraźniejszym zestawom rejestrów i pętlom spinowym). Jednym z głównych powodów używania C jest to, że pozwala on na mikrozarządzanie i optymalizację wszystkiego, ale większość kodu, który widziałem, nie przeszkadza.
Mike DeSimone
5
Zgadzam się z @Mads. Nie ma powodu, aby przechodzić przez to wszystko, aby uzyskać funkcje, których tak naprawdę nie potrzebujesz. Uważam, że podoba mi się styl podobny do biblioteki GTK. Zdefiniuj swoje „klasy” jako struktury, a następnie utwórz spójne metody, takie jak my_class_new (), a następnie przekaż je do „metod”: my_class_do_this (my_class_instance)
maksymalnie
17

Przeszedłem z C ++ na C z innego powodu (jakaś reakcja alergiczna;) i jest tylko kilka rzeczy, za którymi tęsknię i kilka rzeczy, które zyskałem. Jeśli będziesz trzymać się C99, jeśli możesz, istnieją konstrukcje, które w szczególności pozwalają ci programować całkiem przyjemnie i bezpiecznie

  • wyznaczone inicjatory (ewentualnie w połączeniu z makrami) sprawiają, że inicjalizacja prostych klas jest tak bezbolesna jak konstruktory
  • złożone literały dla zmiennych tymczasowych
  • for-scope zmienna może pomóc w zarządzaniu zasobami związanymi z zakresem , w szczególności w celu zapewnienia unlockmuteksów lub freetablic, nawet przy wstępnych zwrotach funkcji
  • __VA_ARGS__ makra mogą być używane do posiadania domyślnych argumentów funkcji i do rozwijania kodu
  • inline funkcje i makra, które dobrze się łączą, zastępując (w pewnym sensie) przeciążone funkcje
Jens Gustedt
źródło
2
@Mike: w przypadku której części w szczególności? Jeśli skorzystasz z linku, który podałem dla forlunet, wylądujesz na P99, gdzie możesz również rozejrzeć się po przykłady i opisy innych części.
Jens Gustedt
1
@Mike: Proszę bardzo.
george
@george: Dzięki! @Jens: Przykłady pozostałych czterech. Zostałem w tyle na moim C; ostatnio słyszałem, że dodali automatycznie przydzielane (tj. stos) tablice o rozmiarze runtime (np. void DoSomething(unsigned char* buf, size_t bufSize) { unsigned char temp[bufSize]; ... }) i inicjalizację struktury według nazw pól (np. struct Foo bar = { .field1 = 5, .field2 = 10 };), te ostatnie, które chciałbym zobaczyć w C ++, szczególnie w przypadku obiektów innych niż POD (np. UART uart[2] = { UART(0x378), UART(0x278) };) .
Mike DeSimone
@Mike: tak, istnieją tablice o zmiennej długości (VLA), ale mogą być nieco niebezpieczne w użyciu z powodu potencjalnego przepełnienia stosu. Drugie, które opisujesz, to dokładnie „wyznaczony inicjator”, więc przejdź do własnego przykładu ;-) Dla innych znajdziesz informacje na powyższym łączu P99, klikając „powiązane strony”.
Jens Gustedt
8

Nie istnieje nic takiego jak STL dla C.
Dostępne są biblioteki, które zapewniają podobną funkcjonalność, ale nie są już wbudowane.

Myślę, że byłby to jeden z moich największych problemów ... Wiedzieć, za pomocą którego narzędzia mógłbym rozwiązać problem, ale nie mam narzędzi dostępnych w języku, którego muszę używać.

MOnsDaR
źródło
to prawda. Czy ktoś mógłby wyjaśnić, których bibliotek klas kontenerów należy używać w języku C? A może odpowiedź brzmi „napisz sam”?
Sandeep
@Sandeep: Na początek ta odpowiedź jest właściwa tylko w przypadku kontenerów, których nie ma w standardowej bibliotece. Oprócz braku odpowiednika STL (najlepsza część C ++), biblioteka standardowa C jest znacznie lepsza. POSIX zawiera tsearch, lsearch, hsearch i bsearch oprócz qsort, który jest w libc. Glib to ostateczny „Boost” języka C, zobacz, że jest pełen gadżetów (w zestawie pojemniki). library.gnome.org/devel/glib/stable . Glib integruje się również z Gtk +, kombinacją, która przewyższa Boost i Qt. Istnieje również biblioteka libapr, popularna w przypadku programów xplatform, takich jak Subversion i Apache.
Matt Joiner
Nie mogę znaleźć żadnych bibliotek c, które mogłyby konkurować z STL, są trudniejsze w użyciu, trudniejsze w utrzymaniu, wydajność nie jest rywalem stl, jeśli chcą, aby biblioteki c były tak ogólne jak stl. ograniczenie c i powody, dla których nie możemy mieć czegoś takiego jak biblioteka stl w c, ponieważ c po prostu nie ma możliwości tworzenia czegoś takiego jak stl.
StereoMatching
8

Różnica między C i C ++ polega na przewidywalności zachowania kodu.

Łatwiej jest przewidzieć z dużą dokładnością, co zrobi twój kod w C, w C ++ może być nieco trudniej wymyślić dokładną prognozę.

Przewidywalność w C zapewnia lepszą kontrolę nad tym, co robi twój kod, ale oznacza to również, że musisz robić więcej rzeczy.

W C ++ możesz napisać mniej kodu, aby zrobić to samo, ale (przynajmniej dla mnie) czasami mam problemy ze zrozumieniem, jak kod obiektowy jest ułożony w pamięci i jakie jest oczekiwane zachowanie.

ds
źródło
4
Ilekroć martwię się o to, co naprawdę robi kod, dodaję -sflagę do, gccaby uzyskać zrzut zestawu, wyszukuję funkcję, której dotyczy problem, i zaczynam czytać. To świetny sposób na poznanie dziwactw dowolnego języka kompilowanego.
Mike DeSimone
2
To także strata czasu, ponieważ asemblacja wygenerowana w C ++ byłaby jak czytanie Perla. Brawo za szukanie w każdym razie.
Matt Joiner
7

W mojej pracy - nawiasem mówiąc, osadzonej - nieustannie przełączam się między C i C ++.

Kiedy jestem w C, tęsknię za C ++:

  • szablony (w tym między innymi kontenery STL). Używam ich do takich rzeczy, jak specjalne liczniki, pule buforów itp. (Zbudowałem własną bibliotekę szablonów klas i szablonów funkcji, których używam w różnych projektach osadzonych)

  • bardzo wydajna biblioteka standardowa

  • destruktory, które oczywiście umożliwiają RAII (muteksy, wyłączanie przerwań, śledzenie itp.)

  • specyfikatory dostępu, aby lepiej wymusić, kto może używać (nie widzieć) czego

Używam dziedziczenia w większych projektach, a wbudowane w C ++ wsparcie dla tego jest znacznie czystsze i ładniejsze niż "hack" C polegający na osadzaniu klasy bazowej jako pierwszego elementu członkowskiego (nie wspominając o automatycznym wywoływaniu konstruktorów, list inicjujących itp. ), ale te wymienione powyżej to te, których najbardziej mi brakuje.

Ponadto prawdopodobnie tylko około jednej trzeciej osadzonych projektów C ++, w których pracuję, używa wyjątków, więc przyzwyczaiłem się do życia bez nich, więc nie tęsknię za nimi zbytnio, kiedy wracam do C.

Z drugiej strony, kiedy wracam do projektu w C ze znaczną liczbą programistów, istnieją całe klasy problemów C ++, które zwykłem wyjaśniać ludziom, a które odchodzą. Głównie problemy wynikają ze złożoności C ++ i ludzi, którzy myślą, że wiedzą, co się dzieje, ale tak naprawdę są w części „C z klasami” krzywej pewności C ++ .

Mając wybór, wolałbym używać C ++ w projekcie, ale tylko wtedy, gdy zespół jest dość solidny w języku. Oczywiście zakładając, że nie jest to projekt 8K μC, w którym i tak efektywnie piszę "C".

Dan
źródło
2
Ta „Krzywa zaufania C ++” trochę mi przeszkadza. Sposób, w jaki jest napisany i komentarze, sugerują, że C ++ jest beznadziejny, przegrana sprawa lub cokolwiek innego. Czy coś mi brakuje?
Mike DeSimone
Zanurz się, zobaczymy się za kilka lat. Większość dobrych programistów wychodzi na drugą stronę z kwaśnym smakiem.
Matt Joiner
3

Kilka obserwacji

  • O ile nie planujesz użyć kompilatora C ++ do zbudowania swojego C (co jest możliwe, jeśli trzymasz się dobrze zdefiniowanego podzbioru C ++), wkrótce odkryjesz rzeczy, na które kompilator zezwala w C, które byłyby błędem kompilacji w C ++.
  • Koniec z tajemniczymi błędami w szablonach (yay!)
  • Brak (obsługiwane języki) programowania obiektowego
hhafez
źródło
C nie obsługuje szablonów nie oznacza, że ​​nie potrzebujemy "ogólnych paradygmatów", w C musisz użyć void * i makra, aby naśladować szablon, jeśli potrzebujesz "ogólnych paradygmatów". Void * nie jest bezpieczny, błędy makra również dość kiepski, nie lepszy niż szablon. Szablon jest znacznie łatwiejszy do odczytania i utrzymania niż makro, a także bezpieczny typ.
StereoMatching
2

Prawie te same powody, dla których używam C ++ lub mieszanki C / C ++ zamiast czystego C. Mogę żyć bez przestrzeni nazw, ale używam ich cały czas, jeśli pozwala na to standard kodu. Powodem jest to, że w C ++ możesz napisać znacznie bardziej zwarty kod. Jest to dla mnie bardzo przydatne, piszę serwery w C ++, które mają tendencję do awarii. W tym momencie bardzo pomaga, jeśli kod, na który patrzysz, jest krótki i spójny. Weźmy na przykład pod uwagę następujący kod:

uint32_t 
ScoreList::FindHighScore(
  uint32_t p_PlayerId)
{
  MutexLock lock(m_Lock); 

  uint32_t highScore = 0; 
  for(int i = 0; i < m_Players.Size(); i++)
  {
    Player& player = m_Players[i]; 
    if(player.m_Score > highScore)
      highScore = player.m_Score; 
  }

  return highScore; 
}

W C wygląda to tak:

uint32_t 
ScoreList_getHighScore(
  ScoreList* p_ScoreList)
{
  uint32_t highScore = 0; 

  Mutex_Lock(p_ScoreList->m_Lock); 

  for(int i = 0; i < Array_GetSize(p_ScoreList->m_Players); i++)
  {
    Player* player = p_ScoreList->m_Players[i]; 
    if(player->m_Score > highScore)
      highScore = player->m_Score; 
  }

  Mutex_UnLock(p_ScoreList->m_Lock);

  return highScore; 
}

Nie ma wielkiej różnicy. Jeszcze jedna linia kodu, ale to się sumuje. Zwykle starasz się, aby był czysty i szczupły, ale czasami musisz zrobić coś bardziej złożonego. W takich sytuacjach cenisz sobie liczbę linii. Jeszcze jedna linia to jeszcze jedna rzecz, na którą należy zwrócić uwagę, próbując dowiedzieć się, dlaczego sieć rozgłoszeniowa nagle przestaje dostarczać wiadomości.

W każdym razie stwierdzam, że C ++ pozwala mi robić bardziej złożone rzeczy w bezpieczny sposób.

Anty-bohater
źródło
W C nie możesz tego zrobić "for (int i = 0".
Victor
6
Victor, jest jednak poprawny dla c99 (lub kompilując C z kompilatorem C ++ w przypadku niektórych problemów z pisaniem).
Roman A. Taycher
Uważam, że nie mogę ufać bezpieczeństwu. Więc twój mutex jest ograniczony. Teraz nie wiesz, dlaczego jego odblokowanie powinno „wędrować” przez wyjątek. Nie wiesz nawet, kiedy zostanie odblokowany, każda część twojego kodu może zdecydować, że ma dość i rzucić. Te dodatkowe ukryte „zabezpieczenia” mogą maskować błędy.
Matt Joiner
Matt, wiemy, dlaczego się odblokował.W normalnym przypadku, gdy program dotrze do końca zakresu, mutex odblokuje się, nie musimy go odblokowywać ręcznie robionymi kodami, jest to koszmar konserwacyjny. wyjątek, mutex zostanie odblokowany i możemy go złapać i przeczytać komunikat o błędzie. Komunikat o błędzie jest wystarczająco dobry, czy nie, będzie zależał od tego, jak grasz z wyjątkiem.
StereoMatching
0

Myślę, że głównym problemem, dlaczego c ++ jest trudniejsze do zaakceptowania w środowisku osadzonym, jest brak inżynierów, którzy rozumieją, jak prawidłowo używać c ++.

Tak, to samo rozumowanie można zastosować również do C, ale na szczęście w C nie ma tylu pułapek, które mogą strzelić sobie w stopę. C ++ z drugiej strony, musisz wiedzieć, kiedy nie używać niektórych funkcji w C ++.

Podsumowując, lubię c ++. Używam tego w warstwie usług O / S, sterowniku, kodzie zarządzania itp. Ale jeśli Twój zespół nie ma w tym wystarczającego doświadczenia, będzie to trudne wyzwanie.

Miałem doświadczenie z oboma. Kiedy reszta zespołu nie była na to gotowa, była to totalna katastrofa. Z drugiej strony było to dobre doświadczenie.

KOkon
źródło
0

tak! poznałem oba te języki i stwierdziłem, że C ++ jest bardziej przyjaznym językiem. Ułatwia dzięki większej liczbie funkcji. Lepiej jest powiedzieć, że C ++ jest nadzbiorem języka C, ponieważ zapewnia dodatkowe funkcje, takie jak polimorfizm, interitance, przeciążanie operatorów i funkcji, typy danych zdefiniowane przez użytkownika, które nie są tak naprawdę obsługiwane w C. Tysiąc linii kodu jest zredukowanych do kilku wierszy z pomoc programowania obiektowego to główny powód przejścia z C do C ++.

kaynat liaqat
źródło
C ++ naprawdę nie jest nadzbiorem języka C; bardzo łatwo jest napisać kod w C, którego kompilacja nie powiedzie się na kompilatorze C ++. Z drugiej strony Objective-C był (jest?) Ścisłym nadzbiorem C.
ad absurdum,
@exnihilo Konwencją nadzbiór tutaj jest zdefiniowanie, że C ++ ma więcej funkcji. Poprawia również składnię i semantykę oraz zmniejsza szanse na błędy. Istnieje kod, który nie kompiluje się w C ++, ale może w C, jak const int a; Spowoduje to błąd w C ++, ponieważ konieczne jest zainicjowanie stałej w momencie deklaracji, podczas gdy w C koncepcja zostanie skompilowana. Zatem superseria nie jest sztywną i szybką regułą, jak w matematyce (A⊂B), a raczej przybliżeniem tego, że C ++ zapewnia dodatkowe funkcje, takie jak koncepcja obiektowa.
kaynat liaqat