Program może przechowywać informacje w głównej pamięci komputera na dwa główne sposoby. Pierwsza wykorzystuje zmienne globalne i lokalne, w tym tablice, struktury i klasy. W przypadku globalnych i statycznych zmiennych lokalnych miejsce przechowywania informacji jest ustalone na czas wykonywania programu. W przypadku zmiennych lokalnych pamięć jest alokowana na stosie. Chociaż Borland C++ radzi sobie z tymi zmiennymi bardzo wydajnie, używanie ich wymaga od programisty wcześniejszej znajomości ilości pamięci, która będzie wymagana podczas wykonywania programu.

Drugim sposobem przechowywania informacji jest użycie systemu dynamicznej alokacji pamięci Borland C++. W tej metodzie pamięć do przechowywania informacji jest przydzielana z wolnego obszaru pamięci zgodnie z potrzebami i zwracana z powrotem, tj. zwolniony, gdy nie jest już potrzebny. Wolny obszar pamięci leży między obszarem pamięci, w którym znajduje się program, a stosem. Obszar ten nazywany jest stertą i jest używany do żądań dynamicznej alokacji pamięci.

Zaletą używania pamięci dynamicznej jest to, że ta sama pamięć może być używana do przechowywania różnych informacji podczas wykonywania programu. Ponieważ pamięć jest przydzielana do określonego celu i zwalniana po zakończeniu jej używania, możliwe jest użycie tej samej pamięci w innym momencie do innych celów w innej części programu. Kolejną zaletą dynamicznej alokacji pamięci jest możliwość tworzenia z nią połączonych list, drzew binarnych i innych dynamicznych struktur danych.

Rdzeniem dynamicznej alokacji pamięci w C są funkcje malloc() i free(), które są częścią standardowej biblioteki. Za każdym razem, gdy żądanie alokacji pamięci jest wykonywane przez funkcję malloc(), alokowana jest część dostępnej wolnej pamięci. Za każdym razem, gdy ta pamięć jest zwalniana za pomocą funkcji free(), ta pamięć jest zwracana z powrotem do systemu.

Język C++ definiuje dwa operatory dynamicznej alokacji pamięci, new i delete.

Standard ANSI C definiuje tylko cztery funkcje dynamicznej alokacji pamięci: calloc(), malloc(), free() i realloc(). Borland C++ zawiera jednak kilka innych funkcji dynamicznej alokacji pamięci. Podczas kompilowania kodu dla nowoczesnego 32-bitowego modelu pamięci pamięć jest płaska i zwykle używane są tylko cztery standardowe funkcje alokacji pamięci.

Standard ANSI C określa, że ​​informacje nagłówka potrzebne do dynamicznej alokacji pamięci są zawarte w pliku stdlib.h. Jednak Borland C++ umożliwia korzystanie z plików nagłówkowych stdlib.h lub alloc.h. Używamy tutaj pliku nagłówkowego stdlib.h, ponieważ zapewnia on przenośność. Niektóre inne funkcje dynamicznej alokacji pamięci wymagają plików nagłówkowych alloc.h, malloc.h lub dos.h. Zwróć szczególną uwagę na to, jaki plik nagłówkowy jest potrzebny do użycia każdej funkcji.

Odkryliśmy możliwości dynamicznej alokacji pamięci. Co to znaczy? Oznacza to, że przy dynamicznej alokacji pamięci, pamięć jest rezerwowana nie na etapie kompilacji, ale na etapie wykonywania programu. A to daje nam możliwość wydajniejszej alokacji pamięci, głównie na tablice. Z zaznaczanie dynamiczne pamięci, nie musimy z góry ustawiać wielkości tablicy, zwłaszcza, że ​​nie zawsze wiadomo jaki rozmiar ma mieć tablica. Następnie spójrzmy, jak możesz alokować pamięć.

Przydział pamięci w C (funkcja malloc)

Funkcja malloc() jest zdefiniowana w plik nagłówkowy stdlib.h , służy do inicjalizacji wskaźników z wymaganą ilością pamięci. Pamięć jest przydzielana z sektora pamięć o dostępie swobodnym dostępne dla wszystkich programów działających na tym komputerze. Argumentem jest liczba bajtów pamięci do zaalokowania, funkcja zwraca wskaźnik do zaalokowanego bloku w pamięci. Funkcja malloc() działa jak każda inna funkcja, nic nowego.

Dlatego różne rodzaje dane mają różne wymagania dotyczące pamięci, musimy jakoś nauczyć się, jak uzyskać rozmiar danych w bajtach inny rodzaj. Na przykład potrzebujemy kawałka pamięci dla tablicy wartości typu int - jest to jeden rozmiar pamięci, a jeśli potrzebujemy przydzielić pamięć dla tablicy o tym samym rozmiarze, ale już typu char - to jest inny rozmiar. Dlatego musisz jakoś obliczyć rozmiar pamięci. Można to zrobić za pomocą operacji sizeof(), która pobiera wyrażenie i zwraca jego rozmiar. Na przykład sizeof(int) zwróci liczbę bajtów potrzebnych do przechowania wartości int. Rozważ przykład:

#włączać int *ptrVar = malloc(sizeof(int));

W tym przykładzie w linia 3 wskaźnikowi ptrVar przypisywany jest adres fragmentu pamięci, którego rozmiar odpowiada typowi danych int . Automatycznie ten obszar pamięci staje się niedostępny dla innych programów. A to oznacza, że ​​gdy przydzielona pamięć stanie się niepotrzebna, należy ją jawnie zwolnić. Jeśli pamięć nie zostanie jawnie zwolniona, to na końcu programu pamięć nie zostanie zwolniona na czas system operacyjny, nazywa się to przeciekiem pamięci. Możesz również określić rozmiar przydzielonej pamięci, która musi zostać przydzielona, ​​przekazując pusty wskaźnik, oto przykład:

int *ptrVar = malloc(sizeof(*ptrVar));

Co tu się dzieje? Operacja sizeof(*ptrVar) oszacuje rozmiar obszaru pamięci wskazywanego przez wskaźnik. Ponieważ ptrVar jest wskaźnikiem do kawałka pamięci typu int , sizeof() zwróci rozmiar liczby całkowitej. Oznacza to, że w rzeczywistości zgodnie z pierwszą częścią definicji wskaźnika obliczany jest rozmiar drugiej części. Dlaczego więc tego potrzebujemy? Może to być konieczne, jeśli nagle trzeba zmienić definicję wskaźnika, na przykład int na pływak, a następnie nie musimy zmieniać typu danych w dwóch częściach definicji wskaźnika. Wystarczy, że zmienimy pierwszą część:

Float *ptrVar = malloc(sizeof(*ptrVar));

Jak widać, w takim zapisie jest jeden bardzo forte, nie powinniśmy wywoływać funkcji malloc() przy użyciu sizeof(float) . Zamiast tego przekazaliśmy wskaźnik do typu float do malloc() , w którym to przypadku rozmiar przydzielonej pamięci zostanie automatycznie określony przez siebie!

Jest to szczególnie przydatne, jeśli musisz alokować pamięć daleko od definicji wskaźnika:

Liczba zmiennoprzecinkowa *ptrVar; /* . . . sto linijek kodu */ . . . ptrZm = malloc(rozmiar(*ptrZm));

Gdybyś użył konstrukcji alokacji pamięci z operacją sizeof(), musiałbyś znaleźć definicję wskaźnika w kodzie, spojrzeć na jego typ danych i tylko wtedy będziesz w stanie poprawnie przydzielić pamięć.

Dealokacja przydzielonej pamięci

Zwalnianie pamięci odbywa się za pomocą funkcji free(). Oto przykład:

Wolny(ptrVar);

Po zwolnieniu pamięci, dobrą praktyką jest zresetowanie wskaźnika do zera, czyli ustawienie *ptrVar = 0 . Jeśli przypiszesz 0 do wskaźnika, wskaźnik staje się pusty, innymi słowy, nie wskazuje już nigdzie. Zawsze po zwolnieniu pamięci przypisz wskaźnikowi 0, w przeciwnym razie nawet po zwolnieniu pamięci wskaźnik nadal na nią wskazuje, co oznacza, że ​​możesz przypadkowo uszkodzić inne programy, które mogą korzystać z tej pamięci, ale nawet nic o tym nie wiesz będziesz wiedział i pomyślisz, że program działa poprawnie.

PS: Każdy, kto lubi edycję wideo, może być zainteresowany tym edytorem wideo Windows 7. Edytor wideo nazywa się Movavi, może ktoś już go zna lub nawet z nim pracował. Dzięki temu programowi w języku rosyjskim możesz łatwo dodać wideo z kamery, poprawić jakość i nakładkę piękne filmy efekty.

Pamięć statyczna przydzielany jest jeszcze przed uruchomieniem programu, na etapie kompilacji i montażu. Zmienne statyczne mają stały adres, który jest znany przed uruchomieniem programu i nie zmienia się w trakcie jego działania. Zmienne statyczne są tworzone i inicjowane przed wejściem do główna funkcja w którym rozpoczyna się wykonywanie programu.

Istnieją dwa rodzaje zmiennych statycznych:

  • zmienne globalne są zdefiniowane zmienne funkcje zewnętrzne, w opisie którego brakuje słowa statyczny. Zwykle opisy zmienne globalne zawierające słowo extern są przenoszone do plików nagłówkowych (plików h). Słowo extern oznacza, że ​​zmienna jest zadeklarowana, ale nie jest tworzona w tym momencie w programie. Definicje zmienne globalne, tj. opisy bez słowa extern umieszczane są w plikach implementacyjnych (c-files lub cpp-files). Przykład: zmienna globalna maxind jest deklarowana dwukrotnie:
    • w pliku h z linią

      zewn. wewn. maxind;

      ta deklaracja zgłasza obecność takiej zmiennej, ale nie tworzy tej zmiennej!
    • w pliku cpp z linią

      int maxind = 1000;

      ten opis tworzy zmiennej maxind i przypisuje jej początkową wartość 1000 . Należy pamiętać, że standard językowy nie wymaga obowiązkowego przypisywania wartości początkowych do zmiennych globalnych, ale mimo to zawsze lepiej to zrobić, w przeciwnym razie zmienna będzie zawierać nieprzewidywalną wartość (śmieci, jak mówią programiści). Dobrym stylem jest inicjowanie wszystkich zmiennych globalnych, gdy są one zdefiniowane.
    Zmienne globalne są tak nazwane, ponieważ są dostępne w dowolnym miejscu programu we wszystkich jego plikach. Dlatego nazwy zmiennych globalnych muszą być wystarczająco długie, aby uniknąć przypadkowych nazw dwóch różnych zmiennych. Na przykład nazwy x lub n dla zmiennej globalnej nie są odpowiednie;
  • zmienne statyczne to zmienne, których opis zawiera słowo static . Z reguły deklarowane są zmienne statyczne funkcje zewnętrzne. Takie zmienne statyczne są pod każdym względem podobne do zmiennych globalnych, z jednym wyjątkiem: zakres zmiennej statycznej jest ograniczony do pojedynczego pliku, w którym jest zdefiniowana - a ponadto może być używana tylko po jej zadeklarowaniu, tj. poniżej w tekście. Z tego powodu deklaracje zmiennych statycznych umieszczane są zwykle na początku pliku. W przeciwieństwie do zmiennych globalnych, zmienne statyczne nigdy nie są opisane w plikach h (modyfikatory zewnętrzne i statyczne są ze sobą w konflikcie). Wskazówka: użyj zmiennych statycznych, jeśli chcesz, aby były dostępne tylko dla funkcji opisanych w środku ten sam plik. Jeśli to możliwe, nie używaj zmiennych globalnych w takich sytuacjach, pozwoli to uniknąć konfliktów nazw przy realizacji dużych projektów składających się z setek plików.
    • Zmienną statyczną można również zadeklarować wewnątrz funkcji, chociaż zwykle nikt tego nie robi. Zmienna nie znajduje się na stosie, ale w pamięci statycznej, tj. nie może być używany w rekurencji, a jego wartość jest zachowywana między różnymi danymi wejściowymi funkcji. Zakres takiej zmiennej jest ograniczony do treści funkcji, w której jest zdefiniowana. W przeciwnym razie jest podobny do zmiennej statycznej lub globalnej. Zauważ, że słowo kluczowe statyczny w C jest używany do dwóch różnych celów:
      • jako wskazanie typu pamięci: zmienna znajduje się w pamięci statycznej, a nie na stosie;
      • jako sposób na ograniczenie zakresu zmiennej w pojedynczym pliku (w przypadku deklaracji zmiennej poza funkcją).
  • Słowo static może również pojawić się w nagłówku funkcji. Służy jednak tylko do ograniczenia zakresu nazwy funkcji do pojedynczego pliku. Przykład:

    statyczny int gcd(int x, int y); // Prototyp funkcji. . . static int gcd(int x, int y) ( // Implementacja . . )

    Wskazówka: użyj modyfikator statyczny w nagłówku funkcji, jeśli wiadomo, że funkcja zostanie wywołana tylko w obrębie jednego pliku. Słowo static musi być obecne zarówno w opisie prototypu funkcji, jak iw nagłówku funkcji podczas jej implementacji.

Stos lub pamięć lokalna

Zmienne lokalne lub stosowe są zmiennymi zadeklarowanymi wewnątrz funkcji. Pamięć dla takich zmiennych jest alokowana w stosie sprzętowym, patrz rozdział 2.3.2. Pamięć jest przydzielana po wejściu do funkcji lub bloku i jest zwalniana po wyjściu z funkcji lub bloku. W tym przypadku przechwytywanie i uwalnianie pamięci następuje niemal natychmiast, ponieważ. komputer modyfikuje tylko rejestr zawierający adres wierzchołka stosu.

Zmienne lokalne mogą być używane w rekursji, ponieważ po ponownym wprowadzeniu funkcji na stosie tworzony jest nowy zestaw zmiennych lokalnych, a poprzedni zestaw nie jest niszczony. Z tego samego powodu zmienne lokalne są bezpieczne podczas używania wątków w programowaniu równoległym (patrz sekcja 2.6.2). Programiści nazywają tę właściwość funkcji ponowne wejście, z angielskiego. re-enterable - możliwość ponownego wejścia. To bardzo ważna cecha z punktu widzenia niezawodności i bezpieczeństwa programu! Program pracujący ze zmiennymi statycznymi nie ma tej właściwości, więc aby chronić zmienne statyczne, musisz użyć mechanizmy synchronizacji(patrz 2.6.2), a logika programu staje się znacznie bardziej skomplikowana. Zawsze powinieneś unikać używania zmiennych globalnych i statycznych, jeśli możesz sobie poradzić z lokalnymi.

Wady zmiennych lokalnych są rozszerzeniem ich zalet. Zmienne lokalne są tworzone, gdy funkcja wchodzi i znika, gdy wychodzi, więc nie mogą być używane jako dane współdzielone przez wiele funkcji. Ponadto rozmiar stosu sprzętowego nie jest nieskończony, stos może się przepełnić w jednym delikatnym momencie (na przykład podczas głębokiej rekurencji), co doprowadzi do katastrofalnego zakończenia programu. Dlatego zmienne lokalne nie powinny być duże. W szczególności duże tablice nie mogą być używane jako zmienne lokalne.

Pamięć dynamiczna lub sterta

Oprócz pamięci statycznej i stosowej istnieje również prawie nieograniczony zasób pamięci, który nazywa się dynamiczny, lub sterta(sterta ). Program może przechwytywać obszary pamięci dynamicznej o pożądanej wielkości. Po użyciu poprzednio przechwycona sekcja pamięci dynamicznej powinna zostać zwolniona.

Miejsce przeznaczone na pamięć dynamiczną pamięć wirtualna proces między pamięcią statyczną a stosem. (Mechanizm pamięci wirtualnej został omówiony w sekcji 2.6.) Zazwyczaj stos znajduje się pod wyższymi adresami pamięci wirtualnej i rośnie w kierunku malejących adresów (zob. sekcja 2.3). Program i dane stałe są umieszczone pod niższymi adresami, zmienne statyczne znajdują się powyżej. Przestrzeń nad zmiennymi statycznymi i pod stosem zajmuje pamięć dynamiczna:

adres zamieszkania zawartość pamięci

kod i dane programu,

chronione przed zmianami

...

zmienne statyczne

programy

pamięć dynamiczna

max. adres (2 32 -4)

stos

Dynamiczna struktura pamięci jest automatycznie utrzymywana przez system wykonawczy C lub C++. Sterta składa się z przechwyconych i wolnych segmentów, z których każdy jest poprzedzony deskryptorem segmentu. Podczas wykonywania żądania przejęcia pamięci system wykonujący szuka wolnego segmentu o wystarczającej wielkości i chwyta w nim segment o wymaganej długości. Zwolniony segment pamięci oznaczany jest jako wolny; w razie potrzeby łączy się kilka kolejnych wolnych segmentów.

Język C używa standardowych funkcji malloc i free do pozyskiwania i zwalniania pamięci dynamicznej; ich prototypy są opisane w standardowym pliku nagłówkowym "stdlib.h". (Nazwa malloc jest skrótem od przydziel pamięć- "przechwytywanie pamięci".) Prototypy tych funkcji wyglądają tak:

void *malloc(rozmiar_t n); // Pobranie kawałka pamięci // n bajtów w rozmiarze void free(void *p); // Zwolnij obszar // pamięci o adresie p

Tutaj n to rozmiar przechwyconego obszaru w bajtach, size_t to nazwa jednego z typów liczb całkowitych, które definiują największy rozmiar przechwycony obszar. Typ size_t jest określony w standardowym pliku nagłówkowym „stdlib.h” za pomocą instrukcji typedef (patrz na stronie 117). Zapewnia to, że tekst programu w C jest niezależny od używanej architektury. W architekturze 32-bitowej size_t jest definiowana jako liczba całkowita bez znaku:

typedef unsigned int size_t;

Funkcja malloc zwraca adres przechwyconego obszaru pamięci lub zero w przypadku awarii (gdy nie ma wystarczająco dużego wolnego obszaru). Darmowa funkcja zwalnia kawałek pamięci pod podanym adresem. Do ustawienia adresu służy wskaźnik. typ ogólny próżnia* . Po wywołaniu funkcji malloc należy ją rzutować na wskaźnik do konkretnego typu za pomocą operacji rzutowania, patrz Rozdział 3.4.11. Na przykład poniższy przykład przechwytuje stertę 4000 bajtów i przypisuje jej adres do wskaźnika do tablicy 1000 liczb całkowitych:

int*a; // Wskaźnik do tablicy liczb całkowitych. . . a = (int *) malloc(1000 * sizeof(int));

Wyrażenie w argumencie funkcji malloc to 4000, ponieważ rozmiar liczby całkowitej sizeof(int) wynosi cztery bajty. Aby przekonwertować wskaźnik, użyj operacji rzutowania (int *) ze wskaźnika typu ogólnego na wskaźnik na liczbę całkowitą.

Przykład: drukowanie pierwszych n liczb pierwszych

Spójrzmy na przykład wykorzystujący dynamiczne przechwytywanie pamięci. Musisz wpisać liczbę całkowitą n i wypisać pierwsze n liczb pierwszych. (Liczba pierwsza to liczba, która nie ma nietrywialnych dzielników.) Stosujemy następujący algorytm: kolejno sprawdzamy wszystkie liczby nieparzyste, zaczynając od trzech (rozważamy dwie osobno). Następną liczbę dzielimy przez wszystkie liczby pierwsze znalezione w poprzednich krokach algorytmu i nieprzekraczające pierwiastka kwadratowego sprawdzanej liczby. Jeśli nie jest podzielna przez żadną z tych liczb pierwszych, to sama jest liczbą pierwszą; jest wypisywany i dodawany do tablicy znalezionych liczb pierwszych.

Ponieważ wymagana liczba liczb pierwszych n nie jest znana przed uruchomieniem programu, nie jest możliwe utworzenie tablicy do przechowywania ich w pamięci statycznej. Wyjściem jest zdobycie miejsca na tablicę w pamięci dynamicznej już po wpisaniu liczby n . Oto pełny tekst programu:

#włączać #włączać #włączać int main() ( int n; // Wymagana liczba liczb pierwszych int k; // Aktualna liczba znalezionych liczb pierwszych int *a; // Wskaźnik do tablicy znalezionych liczb pierwszych int p; // Następna liczba do sprawdzenia int r; // Część całkowita pierwiastek kwadratowy z p int i; // Indeks dzielnika liczb pierwszych bool prime; // Pierwsza liczba pierwsza printf("Wprowadź liczbę liczb pierwszych: "); scanf("%d", &n); if (n<= 0) // Некорректное значение =>powrót 1; // zakończ z kodem błędu // Przechwyć pamięć dla tablicy liczb pierwszych a = (int *) malloc(n * sizeof(int)); a = 2; k = 1; // Dodaj 2 do tablicy printf("%d", a); // i wypisz p = 3; podczas (k< n) { // Проверяем число p на простоту r = (int)(// Целая часть корня sqrt((double) p) + 0.001); i = 0; prime = true; while (i < k && a[i] <= r) { if (p % a[i] == 0) { // p делится на a[i] prime = false; // =>p nie jest pierwszym, przerwa; // wyjdź z pętli ) ++i; // Do następnego dzielnika liczby pierwszej ) if (prim) ( // Jeśli znajdziesz liczbę pierwszą, a[k] = p; // następnie dodaj ją do tablicy ++k; // Zwiększ liczbę liczb pierwszych printf( "%d", p ); // Wydrukuj liczbę pierwszą, jeśli (k % 5 == 0) ( // Przełącz na Nowa linia printf("\n"); // po każdych pięciu liczbach )) p += 2; // Do następnej liczby nieparzystej ) if (k % 5 != 0) ( printf("\n"); // Przetłumacz łańcuch ) // Zwolnij pamięć dynamiczną free(a); zwróć 0; )

Przykład działania tego programu:

Wpisz liczbę prostych: 50 2 3 5 7 11 13 13 19 29 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 101 103 107 109 113 127 131 137 139 149 151 157 167 173 179 181 191 193 197 197 197 197 197 197 197 197 197 197 197 197 197 197 199 211 223 227 229

C++ nowe i usuń operatory

Język C++ używa operatorów new i delete do pozyskiwania i zwalniania pamięci dynamicznej. Są one częścią języka C++, w przeciwieństwie do funkcji malloc i free, które są częścią standardowej biblioteki C.

Niech T będzie jakimś typem C lub C++, p będzie wskaźnikiem do obiektu typu T . Następnie do przechwycenia pamięci o rozmiarze jednego elementu typu T stosuje się nowy operator:

T*p; p = nowy T;

Na przykład, aby przechwycić osiem bajtów pod prawdziwy numer fragment jest używany typu double

podwójne *p; p = nowy podwójny;

Używając new , w przeciwieństwie do malloc , nie musisz rzutować wskaźnika z void* na żądany typ: operator new zwraca wskaźnik do typu zapisanego po słowie new . Porównaj dwa równoważne fragmenty w C i C++.

    przechowuje globalne zmienne i stałe;

    rozmiar jest określany w czasie kompilacji.

    stos

    przechowuje zmienne lokalne, argumenty funkcji i wartości pośrednie obliczeń;

    rozmiar jest określany podczas uruchamiania programu (zwykle przydzielane są 4 MB).

    Sterta

    pamięć alokowana dynamicznie;

    System operacyjny przydziela pamięć w porcjach (w razie potrzeby).

Pamięć alokowaną dynamicznie należy wykorzystać, jeśli nie wiemy z góry (w momencie pisania programu), ile pamięci będziemy potrzebować (np. wielkość tablicy zależy od tego, co użytkownik wprowadzi podczas działania programu) oraz podczas pracy z dużą ilością danych.

Pamięć dynamiczna, zwana także „stertą”, jest jawnie przydzielana na żądanie programu z zasobów systemu operacyjnego i jest kontrolowana przez wskaźnik. Nie jest automatycznie inicjowany i musi zostać jawnie zwolniony. W przeciwieństwie do pamięci statycznej i automatycznej, pamięć dynamiczna jest praktycznie nieograniczona (ograniczona jedynie wielkością pamięci RAM) i może się zmieniać podczas działania programu.

Praca z pamięcią dynamiczną w s

Do pracy z pamięcią dynamiczną w języku C wykorzystywane są następujące funkcje: malloc, calloc, bezpłatny, realloc. Rozważmy je bardziej szczegółowo.

    Przydział (przechwytywanie pamięci): void *malloc(size_t size);

Jak parametr wejściowy funkcja przyjmuje rozmiar pamięci do przydzielenia. Wartość zwracana jest wskaźnikiem do przydzielonego na stercie fragmentu pamięci. Jeśli system operacyjny nie był w stanie przydzielić pamięci (na przykład nie było wystarczającej ilości pamięci), malloc zwraca 0.

    Po zakończeniu pracy z dynamicznie przydzielaną pamięcią musisz ją zwolnić. W tym celu jest używany darmowa funkcja, który zwraca pamięć pod kontrolą systemu operacyjnego: void free(void *ptr);

Jeśli pamięć dynamiczna nie zostanie zwolniona przed zakończeniem programu, zostanie ona zwolniona automatycznie po zakończeniu programu. Jest jednak oznaką dobrego stylu programowania, aby wyraźnie zwolnić niepotrzebną pamięć.

Przykład:// przydział pamięci na 1000 elementów int

int * p = (int *) malloc(1000*rozmiar(int));

jeśli (p==NULL) out<< "\n память не выделена";

wolny(p); // powrót pamięci do sterty

2. Alokacja (przechwytywanie pamięci): void *calloc(size_t nmemb, size_t size);

Funkcja działa podobnie jak malloc, ale różni się składnią (zamiast wielkości przydzielonej pamięci należy określić liczbę elementów i rozmiar jednego elementu) oraz tym, że przydzielona pamięć zostanie zresetowana do zera. Na przykład po wykonaniu int * p = (int *) calloc(1000, sizeof(int)) p wskaże początek tablicy int składającej się z 1000 elementów zainicjowanych na zero.

3. Zmiana rozmiaru pamięci: void *realloc(void *ptr, size_t size);

Funkcja zmienia rozmiar przydzielonej pamięci (wskazane przez ptr, pochodzące z połączenia malloc, calloc lub realloc). Jeśli rozmiar określony w parametrze rozmiar większa niż przydzielona pod wskaźnikiem ptr, następnie sprawdzane jest, czy możliwe jest przydzielenie brakujących komórek pamięci w wierszu z już zaalokowanymi. Jeśli nie ma wystarczającej ilości miejsca, alokowana jest nowa część pamięci z rozmiarem rozmiar i dane wskaźnika ptr są kopiowane na początek nowej sekcji.

Podczas wykonywania programu sekcja pamięci dynamicznej jest dostępna wszędzie tam, gdzie dostępny jest wskaźnik adresujący tę sekcję. W związku z tym możliwe są następujące trzy warianty pracy z pamięcią dynamiczną zaalokowaną w jakimś bloku (na przykład w ciele funkcji innej niż główna).

    Wskaźnik (do obszaru pamięci dynamicznej) jest zdefiniowany jako lokalny obiekt pamięci automatycznej. W takim przypadku przydzielona pamięć nie będzie dostępna po wyjściu z bloku lokalizacji wskaźnika i musi zostać zwolniona przed wyjściem z bloku.

( int* p= (int *) calloc(n, sizeof(int))

wolny(p); // wolny dyn. pamięć

    Wskaźnik jest zdefiniowany jako lokalny statyczny obiekt pamięci. Pamięć dynamiczna przydzielona raz w bloku jest dostępna za pomocą wskaźnika za każdym razem, gdy blok jest ponownie wprowadzany. Pamięć powinna być zwalniana tylko wtedy, gdy nie jest już używana.

(statyczny int* p = (int *) calloc(n, sizeof(int));

p= (int *) calloc(n, sizeof(int));

f(50); //wyróżnij din. pamięć do uwolnienia

f1(100); //wyróżnij din. pamięć (pierwszy dostęp)

f1(100); // praca z din. pamięć

f1(0); // wolny dyn. pamięć

    Wskaźnik jest obiektem globalnym w odniesieniu do bloku. Pamięć dynamiczna jest dostępna we wszystkich blokach, w których wskaźnik jest „widoczny”. Pamięć powinna być zwalniana tylko wtedy, gdy nie jest już używana.

int*pG; //działający wskaźnik dla din. pamięć (zmienna globalna)

void init (rozmiar int)

dla (i=0; i< size; i++) //цикл ввода чисел

( printf("x[%d]=",i);

scanf("%d", &pG[i]);

suma wewnętrzna (rozmiar wewnętrzny)

dla (i=0; i< size; i++) //цикл суммирования

// alokacja pamięci

pG= (int *) calloc(n, sizeof(int));

// praca z pamięcią dynamiczną

printf(\ns=%d\n",sum(n));

wolny (pG); pG=NULL; // cofnij alokację pamięci

Praca z pamięcią dynamiczną w C++

C++ posiada własny mechanizm przydzielania i zwalniania pamięci - są to funkcje Nowy oraz kasować. Przykład użycia Nowy: int * p = nowy int; // przydział pamięci na 1000 e-tj. podczas korzystania z funkcji Nowy nie ma potrzeby rzucania wskaźnika i nie ma potrzeby używania rozmiar(). Uwalnianie zaznaczenia za pomocą Nowy pamięć jest obsługiwana przez wywołanie: delete p; Jeśli potrzebujesz przydzielić pamięć dla jednego elementu, możesz użyć int * q = new int; lub int * q = nowy int(10); // przydzielone int zostanie zainicjowane wartością 10 w tym przypadku, usunięcie będzie wyglądało tak: usuń q;

C++ obsługuje trzy podstawowe typy przydział(albo więcej „dystrybucje”) pamięć, z których dwa już znamy:

Statyczna alokacja pamięci trzyma dla i zmienne. Pamięć jest przydzielana jednorazowo po uruchomieniu programu i jest zachowywana przez cały program.

Automatyczne przydzielanie pamięci jest wykonywany dla i . Pamięć jest przydzielana po wejściu do bloku zawierającego te zmienne i usuwana po jego wyjściu.

Dynamiczna alokacja pamięci jest tematem tej lekcji.

Dynamiczna alokacja zmiennych

Zarówno statyczna, jak i automatyczna alokacja pamięci mają dwie wspólne właściwości:

Jak działa dynamiczna alokacja pamięci?

Twój komputer ma pamięć (może być jej dużo), która jest dostępna do wykorzystania przez programy. Kiedy uruchamiasz program, twój system operacyjny ładuje ten program do jakiejś części tej pamięci. A ta pamięć używana przez twój program jest podzielona na kilka części, z których każda wykonuje określone zadanie. Jedna część zawiera Twój kod, druga służy do wykonywania normalnych operacji (śledzenie wywoływanych funkcji, tworzenie i niszczenie zmiennych globalnych i lokalnych itp.). Porozmawiamy o tym później. Jednak większość dostępnej pamięci po prostu czeka na żądania alokacji z programów.

Gdy dynamicznie przydzielasz pamięć, prosisz system operacyjny o zarezerwowanie części tej pamięci do użytku przez program. Jeśli system operacyjny może spełnić to żądanie, adres tej pamięci jest zwracany z powrotem do twojego programu. Od teraz twój program będzie mógł używać tej pamięci tak, jak chce. Kiedy już zrobiłeś wszystko, co było konieczne z tą pamięcią, należy ją zwrócić z powrotem do systemu operacyjnego w celu dystrybucji między innymi żądaniami.

W przeciwieństwie do statycznej lub automatycznej alokacji pamięci, program jest odpowiedzialny za żądania i zwracanie dynamicznie przydzielanej pamięci.

Zwalnianie pamięci

Gdy dynamicznie alokujesz zmienną, możesz również zainicjować ją za pomocą lub jednolitej inicjalizacji (w C++11):

int *ptr1 = nowy int(7); // użyj bezpośredniej inicjalizacji int *ptr2 = new int ( 8 ); // użyj jednolitej inicjalizacji

Kiedy wszystko, co było potrzebne, zostało już zrobione z dynamicznie alokowaną zmienną, musisz wyraźnie powiedzieć C++, aby zwolnił tę pamięć. W przypadku zmiennych odbywa się to za pomocą operator kasować:

// Załóżmy, że ptr został już przydzielony z nowym usunięciem ptr; // zwróć pamięć wskazywaną przez ptr z powrotem do systemu operacyjnego ptr = 0; // ustaw ptr null (użyj nullptr zamiast 0 w C++11)

Operator delete w rzeczywistości niczego nie usuwa. Po prostu zwraca pamięć, która została wcześniej przydzielona z powrotem do systemu operacyjnego. System operacyjny może następnie ponownie przypisać tę pamięć do innej aplikacji (lub ponownie do tej samej).

Chociaż może się wydawać, że usuwamy zmienny ale nie jest! Zmienna wskaźnikowa nadal ma ten sam zakres co poprzednio i można jej przypisać nową wartość, tak jak każdej innej zmiennej.

Zauważ, że usunięcie wskaźnika, który nie wskazuje na dynamicznie przydzieloną pamięć, może prowadzić do problemów.

wiszące wskaźniki

C++ nie daje żadnych gwarancji co do tego, co stanie się z zawartością zwolnionej pamięci lub z wartością usuwanego wskaźnika. W większości przypadków pamięć zwrócona do systemu operacyjnego będzie zawierać te same wartości, które miała wcześniej wydanie, a wskaźnik będzie nadal wskazywał tylko już zwolnioną (usuniętą) pamięć.

Wskaźnik wskazujący na zwolnioną pamięć nazywa się wiszący wskaźnik. Wyłuskanie lub usunięcie wiszącego wskaźnika spowoduje nieoczekiwane wyniki. Rozważ następujący program:

#włączać int main() ( int *ptr = new int; *ptr = 8; // umieść wartość w przydzielonej lokalizacji pamięci usuń ptr; // zwróć pamięć z powrotem do systemu operacyjnego. ptr jest teraz wiszącym wskaźnikiem std:: Cout<< *ptr; // разыменование висячего указателя приведёт к неожиданным результатам delete ptr; // попытка освободить память снова приведёт к неожиданным результатам также return 0; }

#włączać

int main()

int * ptr = nowy int ; // dynamicznie przydziel zmienną całkowitą

* pkt = 8 ; // umieść wartość w przydzielonej lokalizacji pamięci

usuń pkt ; // zwróć pamięć do systemu operacyjnego. ptr jest teraz wiszącym wskaźnikiem

std::cout<< * ptr ; // wyłuskanie zwisającego wskaźnika doprowadzi do nieoczekiwanych wyników

usuń pkt ; // ponowna próba zwolnienia pamięci również doprowadzi do nieoczekiwanych rezultatów

zwróć 0 ;

W powyższym programie wartość 8, która była wcześniej przypisana do zmiennej dynamicznej, może, ale nie musi, nadal tam być po zwolnieniu. Możliwe jest również, że zwolniona pamięć mogła zostać już przydzielona innej aplikacji (lub na własny użytek systemu operacyjnego), a próba uzyskania do niej dostępu spowoduje automatyczne zamknięcie programu przez system operacyjny.

Proces uwalniania pamięci może również prowadzić do tworzenia kilka wiszące wskaźniki. Rozważmy następujący przykład:

#włączać int main() ( int *ptr = new int; // dynamicznie przydziel zmienną całkowitą int *otherPtr = ptr; // otherPtr wskazuje teraz na tę samą przydzieloną pamięć co ptr usuń ptr; // zwróć pamięć do systemu operacyjnego ptr i otherPtr są teraz zawieszonymi wskaźnikami ptr = 0; // ptr jest teraz nullptr // Jednak otherPtr nadal jest zawieszonym wskaźnikiem! return 0; )

#włączać

int main()

int * ptr = nowy int ; // dynamicznie przydziel zmienną całkowitą

int * innyPtr = ptr ; // otherPtr wskazuje teraz na tę samą przydzieloną pamięć co ptr

usuń pkt ; // zwróć pamięć do systemu operacyjnego. ptr i innePtr są teraz zwisającymi wskaźnikami

ptr = 0 ; // ptr jest teraz nullptr

// Jednak otherPtr nadal jest wiszącym wskaźnikiem!

zwróć 0 ;

Po pierwsze, staraj się unikać sytuacji, w których wiele wskaźników wskazuje ten sam fragment przydzielonej pamięci. Jeśli nie jest to możliwe, wyjaśnij, który wskaźnik ze wszystkich „jest właścicielem” pamięci (i jest odpowiedzialny za jej usunięcie), a które po prostu uzyskują do niej dostęp.

Po drugie, gdy usuniesz wskaźnik i jeśli nie zakończy się natychmiast po usunięciu, to musi być zerowany, tj. przypisz wartość 0 (lub w C++11). Przez „poza zakresem natychmiast po usunięciu” rozumiem, że usuwasz wskaźnik na samym końcu bloku, w którym jest zadeklarowany.

Reguła: Ustaw usunięte wskaźniki na 0 (lub nullptr w C++11), chyba że wyjdą one poza zakres natychmiast po usunięciu.

nowy operator

Gdy system operacyjny zażąda pamięci, w rzadkich przypadkach może być ona niedostępna (tzn. może być niedostępna).

Domyślnie, jeśli nowy operator nie działał, pamięć nie została przydzielona, ​​to a wyjątek bad_alloc. Jeśli ten wyjątek nie jest właściwie obsługiwany (a tak będzie, ponieważ jeszcze nie zajrzeliśmy do wyjątków i ich obsługi), program po prostu przerwie działanie (zawiesi się) z błędem nieobsłużonego wyjątku.

W wielu przypadkach proces zgłaszania wyjątku z nowym operatorem (jak również zawieszanie programu) jest niepożądany, dlatego istnieje alternatywna forma nowego operatora, która zwraca wskaźnik null, jeśli nie można przydzielić pamięci. Musisz tylko dodać std::nothrow stała między nowym słowem kluczowym a typem danych:

int *wartość = nowy (std::nothrow) int; // wartość wskaźnika stanie się pusta, jeśli dynamiczna alokacja zmiennej całkowitej nie powiedzie się

W powyższym przykładzie, jeśli new nie zwraca wskaźnika z dynamicznie przydzieloną pamięcią, zostanie zwrócony wskaźnik pusty.

Wyłuskiwanie go również nie jest zalecane, ponieważ doprowadzi to do nieoczekiwanych wyników (najprawdopodobniej awarii programu). Dlatego najlepszą praktyką jest sprawdzenie wszystkich żądań alokacji pamięci, aby upewnić się, że te żądania są wykonywane pomyślnie, a pamięć jest alokowana:

int *wartość = nowy (std::nothrow) int; // żądanie alokacji pamięci dynamicznej dla wartości całkowitej if (!value) // obsłuż przypadek, gdy new zwraca null (tzn. nie jest przydzielona pamięć) ( // obsługa tego przypadku std::cout<< "Could not allocate memory"; }

Ponieważ niealokacja pamięci przez nowego operatora jest niezwykle rzadka, programiści zwykle zapominają o tym sprawdzeniu!

Wskaźniki zerowe i dynamiczna alokacja pamięci

Wskaźniki zerowe (wskaźniki o wartości 0 lub nullptr) są szczególnie przydatne w dynamicznej alokacji pamięci. Ich obecność niejako mówi nam: „Nie przydzielono żadnej pamięci do tego wskaźnika”. A to z kolei może posłużyć do wykonania warunkowej alokacji pamięci:

// Jeśli ptr nie ma jeszcze przydzielonej pamięci, przydziel ją if (!ptr) ptr = new int;

Usunięcie wskaźnika zerowego nie wpływa na nic. Więc nie jest konieczne:

if (ptr) usuń ptr;

jeśli (ptr)

usuń pkt ;

Zamiast tego możesz po prostu napisać:

usuń pkt ;

Jeśli ptr nie ma wartości null, dynamicznie alokowana zmienna zostanie usunięta. Jeśli wartość wskaźnika jest równa null, nic się nie stanie.

Wyciek pamięci

Pamięć alokowana dynamicznie nie ma zasięgu, tj. pozostaje on przydzielony, dopóki nie zostanie jawnie zwolniony lub dopóki program nie zakończy jego wykonywania (a system operacyjny sam opróżni wszystkie bufory pamięci). Jednak wskaźniki używane do przechowywania dynamicznie przydzielanych adresów pamięci są zgodne z zasadami normalnego określania zakresu zmiennych. Ta niezgodność może powodować interesujące zachowanie. Na przykład:

void doCos() ( int *ptr = new int; )