Przeczytaj
Alokacja pamięci
W języku C++ istnieje możliwość przydzielania (alokowanie) pamięci – możemy ją przydzielać w czasie wykonywania programu zmiennej lub tablicy. Proces ten jest znany pod nazwą dynamiczna alokacja pamięci.
Zapoznaj się z kodem:
Deklarujemy zmienną
wskaznikjako wskaźnikwskaźnik do zmiennej typuint.Używamy operatora
new, by zaalokować dynamicznie pamięć RAM dla pojedynczej zmiennej typuint.Przypisujemy adresadres nowo zaalokowanej pamięci do zmiennej wskaźnikowej
wskaznik.
W efekcie zmienna wskaznik przechowuje adres nowo zaalokowanej pamięci, gdzie może być przechowywana wartość typu int.
W poprzednim kroku zadeklarowaliśmy zmienną o nazwie wskaznik. Jak wyjaśniliśmy, wskazuje ona na fragment pamięci, w której przechowywana jest liczba całkowita. Jeżeli chcemy przypisać wartość liczbową do zmiennej wskaźnikowej, nie możemy wykorzystać do tego prostej operacji przypisania. Zrobimy to, wykorzystując następującą instrukcję:
Używamy operatora dereferencji
*przed zmiennąwskaznik, co oznacza, że chcemy odwołać się do wartości przechowywanej pod adresem wskazywanym przez zmiennąwskaznik.Przypisujemy wartość 24 do zmiennej, do której wskazuje wskaźnik
wskaznik.
W efekcie, wartość 24 jest przypisywana do zmiennej przechowywanej pod adresem wskazywanym przez wskaznik, czyli do dynamicznie zaalokowanej zmiennej typu int.
Podsumowując, te dwie linijki kodu alokują dynamicznie pamięć dla zmiennej typu int, a następnie przypisują wartość 24 do tej zmiennej poprzez wskaźnik.
Operator new w języku C++ jest używany do dynamicznej alokacji pamięci. Jego głównym zadaniem jest zaalokowanie odpowiedniej ilości pamięci na stosie i zwrócenie wskaźnika do tej pamięci. Pamięć zaalokowana przez operator new pozostaje zarezerwowana do momentu jej jawnego zwolnienia. Operator new nie tylko alokuje pamięć, ale także automatycznie wywołuje konstruktor obiektu, jeśli jest to obiekt klasy.
Więcej informacji na temat konstruktorów znajdziesz w e‑materiale Konstruktory i destruktory w języku C++Konstruktory i destruktory w języku C++.
Zarządzanie pamięcią
Specyfikacja języka C++ nie mówi o przydzielaniu pamięci na stosie czy stercie. Zamiast tego miejsce, w którym dany obiekt (dane zdefiniowane w programie) jest przechowywany w pamięci, zależy od kontekstu, według którego obiekt został zdefiniowany. W języku C++ każdy obiekt może mieć jeden z czterech poniższych cyklów istnienia:
Automatyczny - pamięć przydzielana jest obiektowi w momencie utworzenia go dla lokalnego zakresu, a zwalniana na koniec zakresu. Zwykle automatyczny cykl życia jest obsługiwany przez stos wywołań działający według zasady LIFOLIFO. Zakres rozumiemu jako blok programu (to, co znajduje się między nawiasami klamrowymi).
Dynamiczny - blok pamięci jest rezerwowany dla obiektu słowem kluczowym
new, a zwalniany operatoremdeletelubdelete[]. Zaalokowana pamięć pozostaje zarezerwowana do końca działania programu lub do momentu gdy jej nie zwolnimy. W większości programów obsługiwany jest przez stertę.Statyczny - miejsce w pamięci dla obiektu jest automatycznie rezerwowane przy uruchomieniu programu, a zwalniane gdy program zakończy działanie.
Wątkowy (od C++11) - rezerwuje blok pamięci dla obiektu, gdy wątek zostaje rozpoczęty, a dealokuje automatycznie pod koniec wątku.
Zwalnianie pamięci
W przeciwieństwie do języków takich jak Java lub Python , język C++ nie ma wbudowanego kolektora śmieci automatycznie zarządzającego pamięcią. Z tego powodu musimy samodzielnie zwolnić dynamicznie zarezerwowaną pamięć dla każdej zmiennej, która nie będzie już potrzebna.
Blok pamięci, który nie został zwolniony w trakcie uruchomiania, pozostaje zajęty przez cały okres działania programu. W niektórych przypadkach (gdy wyciek jest szczególnie duży) może doprowadzić to do obniżenia wydajności innych aplikacji lub systemu operacyjnego.
Operator delete zwalnia pamięć i wywołuje destruktor dla pojedynczego obiektu utworzonego za pomocą operatora new.
Więcej informacji na temat destruktorów znajdziesz w e‑materiale Konstruktory i destruktory w języku C++Konstruktory i destruktory w języku C++.
Operator delete[] zwalnia pamięć i wywołuje destruktory dla tablicy obiektów utworzonych za pomocą new[].
Przykład działania alokacji oraz zwalniania pamięci omawiamy we fragmencie dotyczącym tablic dynamicznych.
Zmienne dynamiczne i statyczne
Zmienna dynamiczna to pewien rodzaj zmiennej, której przestrzeń w pamięci komputera jest alokowana w trakcie działania programu (a nie w momencie kompilacji). Dostęp do tej przestrzeni pamięci jest możliwy poprzez wskaźnik. Alokacja pamięci dla zmiennej dynamicznej odbywa się za pomocą operatora new, a zwolnienie pamięci przy użyciu operatora delete. Dzięki zmiennym dynamicznym program może elastycznie zarządzać pamięcią, alokując i zwalniając ją w zależności od bieżących potrzeb.
Zmienna statyczna to taka zmienna, której przestrzeń w pamięci jest alokowana na etapie kompilacji programu i pozostaje zaalokowana przez cały czas jego działania. Zmienne statyczne mogą być deklarowane na poziomie globalnym, w przestrzeni nazw, wewnątrz klas jako składowe statyczne lub lokalnie wewnątrz funkcji jako zmienne lokalne statyczne. W przeciwieństwie do zmiennych dynamicznych dostęp do zmiennych statycznych odbywa się bezpośrednio przez ich nazwy, a nie poprzez wskaźniki.
Wskaźnik przechowuje adres w pamięci komputera, gdzie zapisana jest jakaś wartość lub obiekt.
Wskaźnik to zmienna, która zamiast przechowywać bezpośrednio daną wartość (jak liczba czy znak), przechowuje adres, pod którym ta wartość się znajduje w pamięci komputera. Dzięki temu za pomocą wskaźnika możemy wskazać na miejsce, gdzie coś jest zapisane, co jest szczególnie przydatne w przypadku pracy z zmiennymi dynamicznymi.
Alokacja i dealokacja pamięci dla zmiennych statycznych jest zarządzana automatycznie, co oznacza, że programista nie musi jawnie alokować lub zwalniać pamięci za pomocą operatorów new i delete. Zmienne statyczne zadeklarowane poza funkcjami (na poziomie globalnym lub jako składowe klas) są inicjalizowane do wartości zerowych (dla typów podstawowych) lub konstruktorów domyślnych (dla obiektów) jeśli nie są jawnie inicjalizowane.
Przykład zastosowania wskaźnika, zmiennej statycznej i zmiennej dynamicznej:
Wynik działania programu:
Znak * przez zmienną jest operatorem wskaźnika, który może być stosowany do deklaracji, dostępu i manipulacji wskaźnikami oraz do alokacji i zwalniania dynamicznej pamięci.
Druga linia wyniku, Wartość zmiennej dynamicznej: 20, jest rezultatem utworzenia zmiennej dynamicznej za pomocą operatora new, inicjalizacji jej wartością 20, a następnie wypisania tej wartości. W tym przypadku wskaźnik dynamicznaZmienna przechowuje adres zmiennej dynamicznej alokowanej w pamięci sterty, a używając tego wskaźnika z operatorem dereferencji (*dynamicznaZmienna), program wypisuje jej wartość. Następnie pamięć alokowana dynamicznie jest zwalniana za pomocą operatora delete, co jest kluczowe dla uniknięcia wycieków pamięci.
Pierwsza linia wyniku, czyli 10, jest wynikiem wypisania wartości zmiennej liczba, do której dostęp uzyskano za pomocą wskaźnika wskaznikNaLiczbe. Oznacza to, że korzystamy z samego adresu pamięci przechowywanego przez wskaźnik, aby uzyskać dostęp do danych zapisanych pod tym adresem. Wskaźnik służy jako pośrednik do odniesienia się do miejsca w pamięci, gdzie przechowywane są dane, zamiast przechowywać same dane bezpośrednio.
Wskaźnik ten został zainicjowany adresem zmiennej liczba, a następnie użyto go do odczytania wartości tej zmiennej (*wskaznikNaLiczbe), co pokazuje, jak wskaźniki mogą być używane do bezpośredniego dostępu i manipulacji wartościami zmiennych.
Pokazaliśmy w ten sposób dwa podstawowe zastosowania wskaźników w języku C++, czyli dostęp do zmiennych oraz zarządzanie pamięcią dynamiczną. Wskaźniki pozwalają na bezpośrednią manipulację pamięcią i adresami, co jest szczególnie ważne w kontekście zarządzania zasobami i optymalizacji. Niepoprawne użycie wskaźników może prowadzić do błędów takich jak wycieki pamięci, naruszenia dostępu do pamięci (z ang. segmentation faults) lub nieprzewidywalne zachowanie programu.
Sytuacje, gdy używamy wskaźników:
Chcemy zmieniać wartości zmiennych z innych funkcji (ponieważ przekazujemy do funkcji adres, a nie kopię wartości).
Zarządzamy pamięcią dynamicznie – kiedy chcemy sami decydować, kiedy coś zajmuje miejsce w pamięci i kiedy to miejsce zwalniamy.
Pracujemy z tablicami i innymi strukturami danych, gdzie wskaźniki pozwalają na efektywną manipulację danymi.
Rodzaje tablic
Tablice statyczne
Przypomnijmy sobie najważniejsze informacje na temat tablic statycznych (możesz również wrócić do e‑materiału Podstawowe struktury danych: tablicaPodstawowe struktury danych: tablica.
Tablice statyczne to jedna z podstawowych struktur danych w języku C++, która pozwala przechowywać zbiory elementów tego samego typu w ciągłym bloku pamięci. Są one nazywane statycznymi, ponieważ ich rozmiar musi być znany już w momencie kompilacji programu i nie może się zmieniać w trakcie jego działania.
Gdy deklarujesz tablicę statyczną, musisz określić jej rozmiar. Język C++ alokuje miejsce w pamięci na całą tablicę jako ciągły blok. Każdy element tablicy ma swój unikalny indeks, zaczynając od 0 dla pierwszego elementu, 1 dla drugiego i tak dalej. Umożliwia to prostu dostęp do każdego elementu tablicy.
W tym przykładzie zmienna mojaTablica jest pięcioelementową tablicą liczb całkowitych. Najpierw wypełniamy ją wartościami za pomocą pętli for, a następnie iterujemy przez tablicę, aby wypisać jej zawartość.
Wynik działania programu:
W kontekście zmiennych statycznych w tablicach statycznych, pamięć jest zarządzana na zasadach bardzo podobnych do tych dla pojedynczych zmiennych statycznych. Jest jednak kilka istotnych różnic.
Podobnie jak pojedyncze zmienne statyczne, pamięć dla tablic statycznych jest alokowana na etapie kompilacji programu. Rozmiar tablicy musi być znany w czasie kompilacji, co oznacza, że nie można go zmienić w czasie działania programu.
Tablica statyczna zajmuje ciągły obszar w pamięci, który jest wystarczająco duży, aby pomieścić wszystkie jej elementy. Pamięć ta jest zwykle alokowana na stosie (dla tablic zadeklarowanych lokalnie w funkcji) lub w sekcji danych programu (dla tablic globalnych lub statycznych w klasach).
Elementy tablicy statycznej są automatycznie inicjalizowane na wartości domyślne (np. 0 dla typów liczbowych, false dla bool, null dla wskaźników) jeśli nie są jawnie inicjalizowane w kodzie. Programista może jawnie zainicjować tablicę przy jej deklaracji, określając wartości dla niektórych lub wszystkich jej elementów.
Tablice statyczne zadeklarowane lokalnie w funkcji są dostępne tylko w obrębie tej funkcji, ale ich wartości są zachowywane między kolejnymi wywołaniami funkcji, w przeciwieństwie do zmiennych automatycznych. Dla tablic globalnych lub statycznych w klasach, pamięć jest alokowana na cały czas działania programu, a więc tablice te są dostępne przez cały czas jego wykonania.
Pamięć zajmowana przez tablice statyczne jest automatycznie zwalniana przez środowisko wykonawcze C++ po zakończeniu działania programu lub przy wyjściu z bloku, w którym tablica została zadeklarowana (dla tablic lokalnych).
Tablice dynamiczne
Wskaźnik może nie tylko przechowywać adres pojedynczej zmiennej, ale również pozwala deklarować tablice o rozmiarze określanym w trakcie działania programu.
W przypadku tablic dynamicznych należy pamiętać o dwóch istotncyh poleceniach, których używamy:
new– operator alokuje pamięć dla tablicy dynamicznej;delete– operator jest używany do zwolnienia pamięci zaalokowanej dla tablicy dynamicznej.
Operator new:
Wyjaśnienie:
typto typ danych, który ma być przechowywany w tablicy.nazwa_tablicyto nazwa wskaźnika, który będzie wskazywał na pierwszy element tablicy.rozmiarto liczba elementów w tablicy.
Operator new zwraca wskaźnik na pierwszy element nowo utworzonej tablicy.
Operator delete:
Wyjaśnienie:
nazwa_tablicyto nazwa wskaźnika, który wskazuje na pierwszy element tablicy.
Operator delete[] informuje kompilator, że chcemy zwolnić pamięć zaalokowaną dla całej tablicy, a nie tylko dla pojedynczego elementu.
Po użyciu operatora delete[], pamięć zaalokowana dla tablicy jest zwalniana, a wskaźnik staje się nieważny. Jednocześnie wszystkie elementy tablicy stają się niedostępne.
Operator new umożliwia alokację pamięci dla tablic obiektów dowolnego typu. Aby zadeklarować tablicę dynamiczną typu int, należy przy deklaracji umieścić rozmiar tablicy wewnątrz nawiasów kwadratowych. Przy deklaracji tablicy dynamicznej operator new zwraca adres pierwszego elementu tablicy.
Do wyrazów tablic dynamicznych odwołujemy się identycznie, jak w przypadku tablic statycznych.
Dynamicznie można deklarować nie tylko tablice jednowymiarowe, ale także tablice dwu i więcej wymiarowe. Wtedy wskaźnik do tablicy dwuwymiarowej będzie typu int**, a dla każdego elementu tablicy (typu int*) należy zadeklarować nową tablicę dynamiczną.
Przetestuj działanie programu:
Wynik działania programu:
Obiekty dynamiczne
Podobnie do tego, jak tworzymy zmienne dynamiczne, możemy tworzyć obiekty dynamiczne. Obiekty dynamiczne w języku C++ to instancje klas lub struktury, które są alokowane w czasie wykonywania programu na stercie zamiast na stosie.
Alokacja obiektów dynamicznych odbywa się za pomocą operatora new, który rezerwuje odpowiednią ilość pamięci na stercie dla obiektu danej klasy i zwraca wskaźnik do tego obszaru pamięci. Aby zwolnić zajmowaną pamięć, kiedy obiekt nie jest już potrzebny, używa się operatora delete. Dzięki temu mechanizmowi, programista ma kontrolę nad cyklem życia obiektów.
W tym przykładzie tworzymy dynamicznie obiekt klasy Samochod na stercie za pomocą operatora new. Obiekt jest dostępny przez wskaźnik mojSamochod, a jego metody są dostępne za pomocą operatora strzałki (->). Pola są prywatne. Po zakończeniu używania obiektu, zwalniamy pamięć za pomocą delete.
Dlaczego używamy obiektów dynamicznych? Obiekty dynamiczne mogą być tworzone i usuwane w dowolnym momencie działania programu, co daje możliwość kontroli nad zarządzaniem pamięcią.
W przypadku dużych obiektów lub zmiennej liczby obiektów, alokacja dynamiczna pozwala na efektywniejsze wykorzystanie zasobów.
Obiekty dynamiczne istnieją aż do momentu ich jawnego usunięcia, co oznacza, że mogą przetrwać dłużej niż zasięg bloku kodu, w którym zostały utworzone.
Jakie jest ryzyko?
Programista jest odpowiedzialny za zwolnienie pamięci zajmowanej przez obiekty dynamiczne, co może prowadzić do wycieków pamięci, jeśli zostanie to zaniedbane.
Alokacja i dealokacja na stercie są zazwyczaj kosztowniejsze pod względem wydajności niż operacje na stosie.
Podsumowując, obiekty dynamiczne w C++ oferują dużą elastyczność i kontrolę nad zarządzaniem pamięcią, ale wymagają od programistów staranności w zarządzaniu cyklem życia obiektów i pamięcią, aby unikać wycieków i innych problemów.
Pomimo że rozmiar dynamicznych tablic jest stały aż do momentu ich zwolnienia, istnieje prosta metoda na zarządzanie ich wielkością. Załóżmy, że mamy wskaźnik tab wskazujący na w pełni zapełnioną tablicę. W tej sytuacji możemy stworzyć nową tablicę nowa_tab. Będzie miała dwa razy większy rozmiar. Następnym krokiem jest przekopiowanie wszystkich elementów ze starej tablicy tab do nowej tablicy nowa_tab. Po skopiowaniu danych zwalniamy pamięć przydzieloną dla starej tablicy za pomocą delete[] tab. Na koniec wskaźnik tab ustawiamy na nową tablicę nowa_tab.
Dzięki temu procesowi rozszerzamy dostępną przestrzeń tablicy, zachowując dotychczasowe dane i umożliwiając dodawanie nowych elementów
W ten sposób działa między innymi kontener vector ze standardowej biblioteki szablonów STL. Poniżej znajduje się implementacja uproszczonej klasy wektora inspirowana klasą vector z biblioteki STL.
Wynik działania programu:
Pierwszy wiersz wyników wypisuje zawartość starej tablicy tab, ale po przekopiowaniu danych i przestawieniu wskaźnika tab na nową tablicę, jednak w rzeczywistości wypisuje pierwsze 10 elementów nowej tablicy nowa_tab. To pokazuje, że dane zostały pomyślnie przekopiowane z oryginalnej tablicy do nowej, większej tablicy.
Drugi wiersz wyników wypisuje zawartość nowej, większej tablicy nowa_tab po zmianie wskaźnika. Możemy zauważyć, że pierwsze 10 elementów zawiera przekopiowane wartości, a pozostałe 10 elementów zawiera wartości domyślne dla typu int w C++, czyli 0. Jest to spowodowane tym, że nowo alokowana pamięć dla typów prostych (do których należy int) nie jest inicjowana żadnymi konkretnymi wartościami, chyba że zostanie to zrobione jawnie. W tym przypadku, ponieważ dodatkowe miejsce nie zostało wypełnione żadnymi danymi, domyślnie przyjmuje wartość 0.
Słownik
alias pamięci w języku C++ odnosi się do sytuacji, w której jednemu identyfikatorowi przypisuje się inną nazwę dla istniejącej wartości w pamięci; zmienna referencyjna jest przykładem aliasu pamięci – jedna zmienna może być dostępna poprzez różne identyfikatory; jeśli utworzymy zmienną referencyjną, tworzymy alias pamięci dla istniejącej zmiennej, co umożliwia dostęp do tej zmiennej za pomocą dwóch różnych nazw
koncepcja dynamicznych struktur danych, w których ostatni element, który został dodany, jest także pierwszym elementem, który zostaje usunięty; ten sposób zachowania jest charakterystyczny dla takich struktur danych jak stos (stack) czy kolejka LIFO (queue)
tablica, której rozmiar nie jest znany w momencie kompilacji, lecz określany w trakcie działania programu; taka tablica umożliwia nie tylko dodawanie do niej kolejnych elementów, ale także ich usuwanie; tablice dynamiczne mogą być również całkowicie usunięte przez programistę w celu zwolnienia zarezerwowanej dla nich pamięci (podobnie jak wskaźniki umożliwiają np. efektywniejsze wykorzystanie pamięci)
specjalna zmienna przeznaczona do przechowywania zawartego w pamięci adresu innej zmiennej; sam wskaźnik również przechowywany jest pod określonym innym adresem – jego wykorzystanie w programach może gwarantować liczne korzyści, np. efektywniejsze wykorzystanie zasobów pamięci
zmienna referencyjna w języku C++ to specjalny rodzaj zmiennej, która przechowuje referencję (alias) do innego obiektu lub zmiennej; jest to alias dla istniejącej wartości w pamięci, który umożliwia bezpośredni dostęp do tej wartości za pomocą różnej nazwy; przykłady zmiennych referencyjnych obejmują referencje do zmiennych typu prostego oraz referencje do obiektów klasowych.
zmienna typu prymitywnego przechowuje w pamięci konkretne wartości; przykład:
wartości te nie są obiektami