Skocz do zawartości

Tworzenie obiektów w języku C++ / Arduino - jak to prawidłowo robić?


hubert27

Pomocna odpowiedź

hubert27, w pytaniu pomieszałeś dwa pojęcia i dlatego odpowiedź na Twoje pytanie wydaje się trudna. Pytasz o konstruktor, a chodzi Ci o alokację pamięci.

Są to dwa różne pojęcia, chociaż często używane razem.

Konstruktor jest zwykłą funkcją, a jego zadaniem jest przypisanie początkowych wartości do składowych obiektu - gdyby nie on, obiekt mógłby mieć wszystkie składowe zerowe, albo co gorsza wypełnione dość losowo. Oczywiście takie porządki mogłaby wykonywać każda inna funkcja, ale C++ nieco ułatwia pamiętanie o jej wywołaniu poprzez automatyczne wywołanie konstruktora.

Alokacja pamięci do bardzo skomplikowany temat, więc mocno upraszczając: w Arduino mamy tylko jeden typ pamięci - SRAM (static random access memory). Jest jej bardzo niewiele, np. Atmega 328 ma raptem 2k.

Myśląc o alokacji pamięci, najlepiej na chwilę zapomnieć o klasach i obiektowości - tutaj nie ma różnicy między "zwykłymi" zmiennymi, np. typu int, czy skomplikowanymi obiektami.

Każda zmienna musi być gdzieś przechowywana, mamy więc trzy możliwości:

1. zmienne globalne - ich adres jest ustalany podczas kompilacji programu, linkowanie jest etapem, który przypisuje adresy funkcjom oraz zmiennym. Adres zmiennej jest stały, a początkowa wartość jest przypisywana przed funkcją main(). Jeśli zmienna jest obiektem, to również konstruktor jest wywoływany przed main()

Przykład:

int x = 5;       // wartosc 5 jest przypisywana do x przed wywolaniem main(), adres jest znany na etapie kompilacji
Klasa obiekt;    // konstruktor jest wywolywany przed main(), adres ustala linker

int main()
{
}

2. zmienne lokalne - pamięć jest alokowana na stosie. Gdy program wchodzi do danego zakresu,np. funkcji, przypisywana jest początkowa wartość ew. wywoływany konstruktor. Gdy program wychodzi z zakresu, pamięć jest automatycznie zwalniana.

Przykład:

void funkcja(void)
{
   int x = 5;  // pamiec jest przydzielana gdy program wchodzi do funkcji. Kazde wywolanie moze przydzielac inny adres
   Klasa obj;  // konstruktor jest wywolywany za kazdym razem

   // tutaj x == 5, obj ma stan ustalony przez konstruktor

}// wychodzac z funkcji pamiec jest zwalniana. Wywolywany jest destruktor

3. dynamiczna alokacja pamięci - tutaj mamy do czynienia z new/delete lub malloc/free. Pamięć jest alokowana z obszaru nazywanego stertą. Przed użyciem zmiennej trzeba "ręcznie" przydzielić pamięć wywołując new lub malloc, a na koniec pamiętać o jej zwolnieniu. Operator new alokuje pamięci i wywołuje konstruktor, malloc tylko przydziela pamięć.

Przykład:

int* x = new int(5);                // przydzielenie pamieci dla zmiennej x
Klasa* obj = new Klasa();           // przydzielenie pamieci i wywolanie konstruktora 
void* memory_for_obj = malloc(sizeof(Klasa)); // samo przydzielenie pamięci
Klasa* obj2 = new (memory_for_obj) Klasa(); // wywołanie konstruktora

free(memory_for_obj);           // zwalnianie pamieci
delete obj;
delete x;

Dynamiczne przydzielanie pamięci jest bardzo elastyczne, ale też "drogie". Poza wspomnianą fragmentacją pamięci, każda zmienna wymaga wskaźnika (który jest np. zmienną globalną lub lokalną na stosie), do tego dochodzi zużycie pamięci przez same funkcje przydzielające pamięć (muszą jakoś pamiętać który obszar został już wykorzystany).

Typowym błędem jest tzw. wyciek pamięci, czyli brak jej zwalniania.

Dlatego w przypadku małych mikrokontrolerów dynamiczne przydzielanie pamięci jest mało popularne - mając tylko 2k lepiej jej nie marnować.

  • Lubię! 1
  • Pomogłeś! 1
Link do komentarza
Share on other sites

Bardzo ładnie wyjaśnione, ale ja jeszcze się przyczepię do jednego szczegółu (to dla postronnych, bo oczywiście wiem, że Elvis doskonale o tym wie):

Dynamiczne przydzielanie pamięci jest bardzo elastyczne, ale też "drogie". Poza wspomnianą fragmentacją pamięci, każda zmienna wymaga wskaźnika (który jest np. zmienną globalną lub lokalną na stosie), do tego dochodzi zużycie pamięci przez same funkcje przydzielające pamięć (muszą jakoś pamiętać który obszar został już wykorzystany).

Tak naprawdę, to głównym "kosztem" nie jest konieczność trzymania wskaźnika, czy pamiętania co do czego jest używane (są na to sposoby), tylko dwa inne problemy: po pierwsze, jeśli program alokuje pamięć dynamicznie podczas swojego działania i zależnie od tego co akurat robi, to może się zdarzyć, że w którymś momencie spróbuje zaalokować pamięć, a tu się okaże, że ta się skończyła. Trzeba zatem albo pisać dodatkowy kod, który jakoś sobie radzi w takiej sytuacji (o czym najczęściej się zapomina), albo po prostu program się wysypie.

Drugi problem pojawia się kiedy często alokujemy i zwalniamy bloki pamięci różnych wielkości. Wyobraź sobie, że masz 10 jednostek wolnej pamięci:

XXXXXXXXXX

Alokujesz jakiś obiekt czy inną tablicę, która zabiera 4 jednostki:

AAAAXXXXXX

A potem kolejną, która zabiera 2 jednostki:

AAAABBXXXX

Teraz zwalniasz pierszą tablicę i chcesz zaalokować trzecią, która zajmie 5 jednostek. Niby masz wolne 8 jednostek pamięci, ale niestety wygląda to tak:

XXXXBBXXXX

Zatem nie ma miejsca na 5-jednostkową tablicę i twoja próba alokacji się nie powiedzie.

To się nazywa "fragmentacja pamięci" i są też dużo bardziej skomplikowane scenariusze. Istnieją też dość sprytne algorytmy radzenia sobie z nimi, ale zazwyczaj wymagają one mechanizmów "odśmiecania pamięci", które mają swoją cenę (istnieją nawet biblioteki dla C++, które to robią).

W przypadku używania stosu takich problemów nie ma, bo zarówno alokacja jak i zwalnianie pamięci zawsze mają tam miejsce na końcu stosu, więc zawsze jest jeden spójny obszar wolnej pamięci.

W przypadku używania zmiennych globalnych też problemu nie ma, bo tam nie ma zwalniania pamięci, co najwyżej możesz czasem próbować sztuczek z używaniem tej samej pamięci do dwóch różnych rzeczy, jeśli nie jest potrzebna w tym samym czasie.

Link do komentarza
Share on other sites

Nie mam najmniejszej ochoty robić za ciebie ciągle całej roboty, bo jesteś "ciekawy" ale nawet ci się komentarza na YouTube nie chce sprawdzić.

Dzięki, że odwaliłeś za mnie całą robotę i skopiowałeś mi komentarz z youtube tylko nie rozumiem tego "ciągle" w tym kontekście.

Czy skopiowanie fragmentu kodu to taka wielka robota? Myślę, że to nie jest problem a tym bardziej żadna tajemnica. Nie lepiej ułatwiać sobie życie wzajemnie niż utrudniać?

Elvisowi się chciało poświęcić chwile i napisać wartościowy post dla początkujących co zmotywowało również Ciebie do poświęcenia chwilki na zilustrowanie pojęcia fragmentacji pamięci - dziękujemy!

Nie wiem co było faktyczną motywacją Twojego "poświęcenia" chęć pomocy czy zabłyśnięcie wiedzą - nie ma znaczenia. Ma znaczenie to, że dzięki Elvisowi wspólnie wyjaśniliście jeden z niuansów programowania i to ma wartość.

BTW te "bebechy" rozjaśniły by w głowie niejednemu.

BTW2 osobiście uważam, że lepiej jest oświecać niż zabłyskać ale to moje prywatne zdanie wynikające być może z poglądów politycznych. Pozdrawiam.

Link do komentarza
Share on other sites

Nie lepiej ułatwiać sobie życie wzajemnie niż utrudniać?

Oczywiście, ale oczekiwałbym wzajemności, a przynajmniej minimum zaangażowania, a nie podejścia konsumenckiego "to teraz ja się położę, a wy mnie uczcie". Pokaż, że to twoje "zainteresowanie" nie jest tylko powierzchowne, poszukaj odpowiedzi sam i tak, jak najbardziej opisz ją na forum dla innych.

Link do komentarza
Share on other sites

Zarejestruj się lub zaloguj, aby ukryć tę reklamę.
Zarejestruj się lub zaloguj, aby ukryć tę reklamę.

jlcpcb.jpg

jlcpcb.jpg

Produkcja i montaż PCB - wybierz sprawdzone PCBWay!
   • Darmowe płytki dla studentów i projektów non-profit
   • Tylko 5$ za 10 prototypów PCB w 24 godziny
   • Usługa projektowania PCB na zlecenie
   • Montaż PCB od 30$ + bezpłatna dostawa i szablony
   • Darmowe narzędzie do podglądu plików Gerber
Zobacz również » Film z fabryki PCBWay

Nie lepiej ułatwiać sobie życie wzajemnie niż utrudniać?

Oczywiście, ale oczekiwałbym wzajemności, a przynajmniej minimum zaangażowania, a nie podejścia konsumenckiego "to teraz ja się położę, a wy mnie uczcie". Pokaż, że to twoje "zainteresowanie" nie jest tylko powierzchowne, poszukaj odpowiedzi sam i tak, jak najbardziej opisz ją na forum dla innych.

Już chyba znalazłem ale opiszę ją dopiero jak będę miał okazję zabłysnąć 😋 - nauczyłem się tego od Ciebie (to się chyba nazywa zgorszenie).

BTW pokazywałem jak byłem mały 😃

Link do komentarza
Share on other sites

Prawdopodobnie chodzi o to że możesz określić np. rozmiar tablicy na dane w trakcie działania programu. Możesz też zmieniać np, rozmiar bufora na dane (np. jeżeli program wykryje że przychodzi za dużo danych i nie nadąża to teoretycznie można coś takiego zrobić, w praktyce raczej nie ma to sensu na systemach wbudowanych).

Link do komentarza
Share on other sites

Cóż, bez komentarza. Próbowałem. Powodzenia w przyszłości.

Próbowałeś co? Nie zauważyłem żebyś próbował wyjaśnić w prosty jednoznaczny sposób czegokolwiek dopiero pod wpływem postu Elvisa - widzisz, jak chcesz to potrafisz 🙂 Moje "pytanie" nie dotyczyło czegoś nie wiadomo jak tajnego a jedynie definicji "klasy" bez klasy czyli bez używania dynamicznej alokacji pamięci i bynajmniej pisząc je publicznie nie odnosiło się jedynie do Ciebie. To jak, napisze ktoś po kolei jak to powinno być?

Link do komentarza
Share on other sites

Przeczytaj ten wątek jeszcze raz, bo nic nie zrozumiałeś.

I nadal nie rzozumiem. Może mam zwarcie na uzwojeniu ale nie widzę tu żadnej jednoznacznie oczywistej odpowiedzi na nurtujące mnie wątpliwości 🙁

Link do komentarza
Share on other sites

hubert27, pisząc elastyczności miałem w sumie kilka rzeczy na myśli.

Najważniejsza różnica to możliwość dostosowywania się do potrzeb programu - powiedzmy odbieramy dane, albo wczytujemy z pliku. W przypadku danych alokowanych statycznie musimy już na etapie kompilacji wiedzieć ile tych danych będzie (np. podać rozmiar tablicy). Wykorzystująca alokację dynamiczną, dopiero wykonując program ustalamy ilość potrzebnego miejsca - np. odbieramy najpierw wielkość pakietu, alokujemy bufor na dane, a później pozostaje już tylko odbierać.

Prosty przykład zalet alokacji dynamicznej to klasa obsługująca napisy. Statycznie musimy znać długość napisu przed kompilacją. Jednak co zrobić, jeśli nie wiemy jaki napis odczytamy? Używając malloc/new możemy pracować z napisami właściwie dowolnej długości.

Dynamicznie możemy tworzyć nieco ciekawsze struktury danych, jak chociażby drzewa, czy listy - statycznie musielibyśmy od razu znać rozmiar wszystkich elementów.

Jest jeszcze jeden powód pisania o elastyczności - czasem, szczególnie w systemach embedded możemy sami napisać funkcję alokującą i zwalniającą pamięć. Dzięki temu możemy dostosować się do potrzeb konkretnego projektu - np. zadbać o unikanie fragmentacji pamięci. Dobrym przykładem jest tutaj FreeRTOS, który dostarcza aż 5 implementacji dla funkcji zarządzania pamięcią - zaczynając od takiej która tylko przydziela, a nie zwalnia zasobów.

Link do komentarza
Share on other sites

Dołącz do dyskusji, napisz odpowiedź!

Jeśli masz już konto to zaloguj się teraz, aby opublikować wiadomość jako Ty. Możesz też napisać teraz i zarejestrować się później.
Uwaga: wgrywanie zdjęć i załączników dostępne jest po zalogowaniu!

Anonim
Dołącz do dyskusji! Kliknij i zacznij pisać...

×   Wklejony jako tekst z formatowaniem.   Przywróć formatowanie

  Dozwolonych jest tylko 75 emoji.

×   Twój link będzie automatycznie osadzony.   Wyświetlać jako link

×   Twoja poprzednia zawartość została przywrócona.   Wyczyść edytor

×   Nie możesz wkleić zdjęć bezpośrednio. Prześlij lub wstaw obrazy z adresu URL.

×
×
  • Utwórz nowe...

Ważne informacje

Ta strona używa ciasteczek (cookies), dzięki którym może działać lepiej. Więcej na ten temat znajdziesz w Polityce Prywatności.