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 alokowana na żądanie programu z zasobów system operacyjny i kontrolowany 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

Pracować z pamięć dynamiczna W języku C używane 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 tymi 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, który odnosi się do tej sekcji. 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;

Ostatnia aktualizacja: 28.05.2017

Podczas tworzenia tablicy o stałym rozmiarze przydzielana jest dla niej pewna ilość pamięci. Na przykład zróbmy tablicę z pięcioma elementami:

liczby podwójne = (1,0, 2,0, 3,0, 4,0, 5,0);

Dla takiej tablicy pamięć jest alokowana 5 * 8 (rozmiar typu double) = 40 bajtów. Dzięki temu wiemy dokładnie, ile elementów znajduje się w tablicy i ile zajmuje pamięci. Jednak nie zawsze jest to wygodne. Czasami konieczne jest, aby liczba elementów i odpowiednio wielkość przydzielonej pamięci dla tablicy były określane dynamicznie w zależności od pewnych warunków. Na przykład sam użytkownik może wprowadzić rozmiar tablicy. W tym przypadku możemy użyć dynamicznej alokacji pamięci do stworzenia tablicy.

Do zarządzania alokacją pamięci dynamicznej wykorzystuje się szereg funkcji, które są zdefiniowane w pliku nagłówkowym stdlib.h:

    malloc() . Ma prototyp

    Nieważne *malloc(s bez znaku);

    Alokuje s bajtów pamięci i zwraca wskaźnik do początku przydzielonej pamięci. Zwraca NULL w przypadku niepowodzenia

    calloc() . Ma prototyp

    Nieważne *calloc(unsigned n, unsigned m);

    Alokuje pamięć na n elementów po m bajtów każdy i zwraca wskaźnik do początku przydzielonej pamięci. Zwraca NULL w przypadku niepowodzenia

    realloc() . Ma prototyp

    Void *realloc(void *bl, unsigned ns);

    Zmienia rozmiar wcześniej przydzielonego bloku pamięci wskazywanego przez bl na ns bajtów. Jeśli wskaźnik bl ma wartość NULL , tzn. nie przydzielono żadnej pamięci, to działanie funkcji jest podobne do działania malloc

    darmowy() . Ma prototyp

    nieważne *wolne(nieważne *bl);

    Zwalnia wcześniej przydzielony blok pamięci wskazywany przez wskaźnik bl.

    Jeśli nie skorzystamy z tej funkcji, to pamięć dynamiczna zostanie zwolniona automatycznie po zakończeniu programu. Jednak nadal dobrą praktyką jest wywoływanie funkcji free(), która pozwala jak najszybciej zwolnić pamięć.

Rozważ zastosowanie funkcji w prostym problemie. Długość tablicy jest nieznana i jest wprowadzana w czasie wykonywania przez użytkownika, a także wartości wszystkich elementów są wprowadzane przez użytkownika:

#włączać #włączać int main(void) ( int *block; // wskaźnik do bloku pamięci int n; // liczba elementów tablicy // wprowadź liczbę elementów printf("Rozmiar tablicy="); scanf("%d", // alokacja pamięci dla tablicy // funkcja malloc zwraca wskaźnik typu void* // który jest automatycznie konwertowany na typ int* block = malloc(n * sizeof(int)); tablica dla(int i=0;i

Konsola wyjścia programu:

Rozmiar tablicy=5 blok=23 blok=-4 blok=0 blok=17 blok=81 23 -4 0 17 81

Tutaj wskaźnik bloku typu int jest zdefiniowany do zarządzania pamięcią dla tablicy. Liczba elementów tablicy nie jest z góry znana, jest reprezentowana przez zmienną n.

Najpierw użytkownik wprowadza liczbę elementów, które trafiają do zmiennej n. Następnie musisz przydzielić pamięć na tę liczbę elementów. Aby przydzielić tutaj pamięć, moglibyśmy użyć dowolnej z trzech funkcji opisanych powyżej: malloc, calloc, realloc. Ale konkretnie w tej sytuacji użyjemy funkcji malloc:

Blok = malloc(n * sizeof(int));

Przede wszystkim należy zauważyć, że wszystkie trzy wymienione wyżej funkcje zwracają wskaźnik typu void * jako wynik uniwersalności zwracanej wartości. Ale w naszym przypadku tworzona jest tablica typu int, która jest zarządzana przez wskaźnik typu int * , więc wynik funkcji malloc jest niejawnie rzutowany na typ int * .

Sama funkcja malloc przekazuje liczbę bajtów dla przydzielonego bloku. Obliczenie tej liczby jest dość proste: wystarczy pomnożyć liczbę elementów przez rozmiar jednego elementu n * sizeof(int) .

Po zakończeniu wszystkich akcji pamięć jest zwalniana za pomocą funkcji free():

wolny(blok);

Ważne jest, że po wykonaniu tej funkcji nie będziemy już mogli korzystać z tablicy, np. wypisywać jej wartości do konsoli:

wolny(blok); for(int i=0;i

A jeśli spróbujemy to zrobić, otrzymamy niezdefiniowane wartości.

Zamiast funkcji malloc moglibyśmy podobnie użyć funkcji calloc(), która pobiera liczbę elementów i rozmiar jednego elementu:

Blok = calloc(n, sizeof(int));

Możesz też użyć funkcji realloc():

int *blok = NULL; blok = realloc(blok, n * sizeof(int));

W przypadku korzystania z realloc pożądane jest (w niektórych środowiskach, na przykład w Visual Studio, obowiązkowe), aby zainicjować wskaźnik na co najmniej NULL.

Ale ogólnie wszystkie trzy wywołania w tym przypadku miałyby podobny efekt:

Blok = malloc(n * sizeof(int)); blok = calloc(n, sizeof(int)); blok = realloc(blok, n * sizeof(int));

Rozważmy teraz bardziej złożony problem - dynamiczną alokację pamięci dla tablicy dwuwymiarowej:

#włączać #włączać int main(void) ( int **table; // wskaźnik do bloku pamięci dla tablicy wskaźników int *rows; // wskaźnik do bloku pamięci do przechowywania informacji w wierszach int rowscount; // liczba wierszy int d; // wprowadź liczbę // wprowadź liczbę wierszy printf("Liczba wierszy="); scanf("%d", &rowscount); // alokacja pamięci na tablicę dwuwymiarową = calloc(rowscount, sizeof( int*)); rows = malloc(sizeof(int )*rowscount); // pętla między wierszami for (int i = 0; i

Zmienna table reprezentuje wskaźnik do tablicy wskaźników typu int* . Każdy wskaźnik table[i] w tej tablicy reprezentuje wskaźnik do podtablicy elementów int, tj. pojedynczych wierszy tabeli. A zmienna table w rzeczywistości reprezentuje wskaźnik do tablicy wskaźników do wierszy tabeli.

Aby przechowywać liczbę elementów w każdej podtablicy, zdefiniowany jest wskaźnik wierszy typu int. W rzeczywistości przechowuje liczbę kolumn dla każdego wiersza tabeli.

Najpierw do zmiennej rowscount wpisywana jest liczba wierszy. Liczba wierszy to liczba wskaźników w tablicy wskazywanej przez wskaźnik tabeli. Co więcej, liczba wierszy to liczba elementów w tablicy dynamicznej wskazywanej przez wskaźnik wierszy. Dlatego najpierw należy przydzielić pamięć dla wszystkich tych tablic:

Tabela = calloc(liczba wierszy, sizeof(int*)); wiersze = malloc(sizeof(int)*liczba wierszy);

W dalszej części pętli wprowadzana jest liczba kolumn dla każdego wiersza. Wprowadzona wartość trafia do tablicy wierszy. I zgodnie z wprowadzoną wartością dla każdej linii przydzielany jest wymagany rozmiar pamięci:

Scanf("%d", &rows[i]); table[i] = calloc(wiersze[i], sizeof(int));

Następnie dla każdego wiersza wprowadzane są elementy.

Pod koniec działania programu pamięć jest zwalniana. Program alokuje pamięć na wiersze tabeli, więc ta pamięć musi zostać zwolniona:

wolny(tabela[i]);

A poza tym pamięć przydzielona na wskaźniki do tabel i wierszy jest zwolniona:

wolny(stół); wolny(wiersze);

Konsola wyjścia programu:

Liczba wierszy=2 Liczba kolumn dla 1=3 tabela=1 tabela=2 tabela=3 Liczba kolumn dla 2=2 tabela=4 tabela=5 1 2 3 4 5

Dynamiczna alokacja pamięci jest niezbędna do efektywnego wykorzystania pamięci komputera. Na przykład napisaliśmy program, który przetwarza tablicę. Podczas pisania tego programu konieczne było zadeklarowanie tablicy, czyli ustawienie dla niej stałego rozmiaru (np. od 0 do 100 elementów). Wtedy ten program nie będzie uniwersalny, ponieważ może przetwarzać tablicę nie większą niż 100 elementów. A jeśli potrzebujemy tylko 20 elementów, ale pamięć przydzieli miejsce na 100 elementów, bo deklaracja tablicy była statyczna, a takie wykorzystanie pamięci jest wyjątkowo nieefektywne.

W C++ operatory new i delete są przeznaczone do dynamicznej alokacji pamięci komputera. Nowa operacja przydziela pamięć z wolnego obszaru pamięci, a operacja usuwania zwalnia przydzieloną pamięć. Przydzielona pamięć, po jej wykorzystaniu, musi zostać zwolniona, aby operacje new i kasowania były wykonywane parami. Nawet jeśli nie zwolnisz jawnie pamięci, zostanie ona zwolniona przez zasoby systemu operacyjnego po zakończeniu programu. Nadal polecam, aby nie zapomnieć o operacji usuwania.

// przykład użycia operacji new int *ptrvalue = new int; //gdzie ptrvalue to wskaźnik do przydzielonego obszaru pamięci typu int //new to operacja przydzielenia wolnej pamięci dla tworzonego obiektu.

Nowy operator tworzy obiekt danego typu, alokuje dla niego pamięć i zwraca wskaźnik właściwego typu do podanej lokalizacji pamięci. Jeśli pamięć nie może być przydzielona, ​​na przykład, jeśli nie ma wolnych obszarów, to zwracany jest wskaźnik pusty, czyli wskaźnik zwróci wartość 0. Przydział pamięci jest możliwy dla dowolnego typu danych: int, platforma,podwójnie,zwęglać itp.

// przykład użycia operacji usuwania: delete ptrvalue; // gdzie ptrvalue jest wskaźnikiem do przydzielonego obszaru pamięci typu int // delete to operacja zwolnienia pamięci

Opracujmy program, który stworzy dynamiczną zmienną.

// new_delete.cpp: definiuje punkt wejścia dla aplikacji konsolowej. #include "stdafx.h" #include << "ptrvalue = "<< *wartośćptr << endl; usuń wartość ptr; // zwolnij system pamięci("pause"); return 0; }

// kod Kod::Bloki

// kod Dev-C++

// new_delete.cpp: definiuje punkt wejścia dla aplikacji konsolowej. #włączać używając standardowej przestrzeni nazw; int main(int argc, char* argv) ( int *ptrvalue = new int; // dynamiczna alokacja pamięci dla obiektu typu int *ptrvalue = 9; // inicjalizacja obiektu za pomocą wskaźnika //int *ptrvalue = new int (9); inicjalizacja może być wykonana natychmiast po deklarowaniu obiektu dynamicznego cout<< "ptrvalue = "<< *wartośćptr << endl; usuń wartość ptr; // zwolnienie pamięci return 0; )

(!LANG:In linia 10 pokazuje, jak zadeklarować i zainicjować obiekt dynamiczny z dziewięcioma, wystarczy podać wartość w nawiasach po typie danych. Wynik programu pokazano na rysunku 1.

Ptrvalue = 9 Naciśnij dowolny klawisz, aby kontynuować. . .

Rysunek 1 - Zmienna dynamiczna

Tworzenie dynamicznych tablic

Jak wspomniano wcześniej, tablice mogą być również dynamiczne. Najczęściej operacje new i delete służą do tworzenia tablic dynamicznych, a nie do tworzenia zmiennych dynamicznych. Rozważ fragment kodu do tworzenia jednowymiarowej tablicy dynamicznej.

// deklaracja jednowymiarowej tablicy dynamicznej z 10 elementami: float *ptrarray = new float ; // gdzie ptrarray jest wskaźnikiem do zaalokowanego obszaru pamięci dla tablicy liczb rzeczywistych typu float // w nawiasach kwadratowych wskazuje rozmiar tablicy

Gdy tablica dynamiczna stanie się niepotrzebna, należy zwolnić obszar pamięci, który został dla niej przydzielony.

// zwolnienie pamięci przeznaczonej na jednowymiarową tablicę dynamiczną: delete ptrarray;

Po operatorze delete umieszczane są nawiasy kwadratowe, które wskazują, że obszar pamięci przydzielony dla tablicy jednowymiarowej jest zwalniany. Stwórzmy program, w którym stworzymy jednowymiarową tablicę dynamiczną wypełnioną liczbami losowymi.

// new_delete_array.cpp: definiuje punkt wejścia dla aplikacji konsolowej. #włączać"stdafx.h" #include !} // w pliku nagłówkowym // w pliku nagłówkowym < 10; count++) ptrarray = (rand() % 10 + 1) / float((rand() % 10 + 1)); //заполнение массива случайными числами с масштабированием от 1 до 10 cout << "array = "; for (int count = 0; count < 10; count++) cout << setprecision(2) << ptrarray << " "; delete ptrarray; // высвобождение памяти cout << endl; system("pause"); return 0; }

// kod Kod::Bloki

// kod Dev-C++

// new_delete_array.cpp: definiuje punkt wejścia dla aplikacji konsolowej. #włączać // w pliku nagłówkowym zawiera prototyp funkcji time() #include // w pliku nagłówkowym zawiera prototyp funkcji setprecision() #include #włączać używając standardowej przestrzeni nazw; int main(int argc, char* argv) ( srand(time(0)); // generowanie liczb losowych float *ptrarray = new float ; // tworzenie dziesięcioelementowej dynamicznej tablicy liczb rzeczywistych for (int count = 0; liczyć< 10; count++) ptrarray = (rand() % 10 + 1) / float((rand() % 10 + 1)); //заполнение массива случайными числами с масштабированием от 1 до 10 cout << "array = "; for (int count = 0; count < 10; count++) cout << setprecision(2) << ptrarray << " "; delete ptrarray; // высвобождение памяти cout << endl; system("pause"); return 0; }

Utworzona jednowymiarowa tablica dynamiczna jest wypełniona losowymi liczbami rzeczywistymi uzyskanymi za pomocą funkcji generowania liczb losowych, a liczby generowane są w zakresie od 1 do 10, interwał ustawiamy następująco - rand() % 10 + 1 . Aby uzyskać losowe liczby rzeczywiste, wykonywana jest operacja dzielenia przy użyciu jawnego rzutowania na typ rzeczywisty mianownika - float((rand() % 10 + 1)) . Aby wyświetlić tylko dwa miejsca po przecinku, użyj funkcji setprecision(2) , prototyp tej funkcji znajduje się w pliku nagłówkowym . Funkcja time(0) inicjuje generator liczb losowych wartością tymczasową, a zatem okazuje się, że odtwarza losowość występowania liczb (patrz rysunek 2).

Tablica = 0,8 0,25 0,86 0,5 2,2 10 1,2 0,33 0,89 3,5 Aby kontynuować, naciśnij dowolny klawisz. . .

Rysunek 2 — Tablica dynamiczna w C++

Po zakończeniu pracy z macierzą jest ona usuwana, zwalniając w ten sposób pamięć przydzieloną do jej przechowywania.

Nauczyliśmy się tworzyć i pracować z jednowymiarowymi tablicami dynamicznymi. Przyjrzyjmy się teraz fragmentowi kodu, który pokazuje, jak deklarowana jest dwuwymiarowa tablica dynamiczna.

// deklaracja dwuwymiarowej tablicy dynamicznej z 10 elementami: float **ptrarray = new float* ; // dwa łańcuchy w tablicy for (int liczba = 0; liczba< 2; count++) ptrarray = new float ; // и пять столбцов // где ptrarray – массив указателей на выделенный участок памяти под массив вещественных чисел типа float

Najpierw deklarowany jest float **ptrarray wskaźnik drugiego rzędu, który odwołuje się do tablicy wskaźników float*, gdzie rozmiar tablicy wynosi dwa . Następnie w pętli for każdy wiersz tablicy zadeklarowanej w linia 2 pamięć jest przydzielona na pięć elementów. W efekcie otrzymujemy dwuwymiarową tablicę dynamiczną ptrarray Rozważmy przykład zwalniania pamięci przydzielonej dla dwuwymiarowej tablicy dynamicznej.

// zwalnianie pamięci przydzielonej dla dwuwymiarowej tablicy dynamicznej: for (int count = 0; count< 2; count++) delete ptrarray; // где 2 – количество строк в массиве

Deklarowanie i usuwanie dwuwymiarowej tablicy dynamicznej odbywa się za pomocą pętli, jak pokazano powyżej, musisz zrozumieć i zapamiętać, jak to się robi. Stwórzmy program, w którym stworzymy dwuwymiarową tablicę dynamiczną.

// new_delete_array2.cpp: definiuje punkt wejścia dla aplikacji konsolowej. #include "stdafx.h" #include #włączać #włączać < 2; count++) ptrarray = new float ; // и пять столбцов // заполнение массива for (int count_row = 0; count_row < 2; count_row++) for (int count_column = 0; count_column < 5; count_column++) ptrarray = (rand() % 10 + 1) / float((rand() % 10 + 1)); //заполнение массива случайными числами с масштабированием от 1 до 10 // вывод массива for (int count_row = 0; count_row < 2; count_row++) { for (int count_column = 0; count_column < 5; count_column++) cout << setw(4) <

// kod Kod::Bloki

// kod Dev-C++

// new_delete_array2.cpp: definiuje punkt wejścia dla aplikacji konsolowej. #włączać #włączać #włączać #włączać używając standardowej przestrzeni nazw; int main(int argc, char* argv) ( srand(time(0)); // generowanie liczb losowych // dynamiczne tworzenie dziesięcioelementowej dwuwymiarowej tablicy liczb rzeczywistych float **ptrarray = new float* ;/ / dwa ciągi w tablicy for (int liczba = 0; liczba< 2; count++) ptrarray = new float ; // и пять столбцов // заполнение массива for (int count_row = 0; count_row < 2; count_row++) for (int count_column = 0; count_column < 5; count_column++) ptrarray = (rand() % 10 + 1) / float((rand() % 10 + 1)); //заполнение массива случайными числами с масштабированием от 1 до 10 // вывод массива for (int count_row = 0; count_row < 2; count_row++) { for (int count_column = 0; count_column < 5; count_column++) cout << setw(4) <

Podczas wyprowadzania tablicy użyto funkcji setw(), jeśli nie zapomniałeś, to przydziela ona miejsce o określonej wielkości dla danych wyjściowych. W naszym przypadku są cztery pozycje dla każdego elementu tablicy, co pozwala wyrównać w kolumnach liczby o różnych długościach (patrz rysunek 3).

2,7 10 0,33 3 1,4 6 0,67 0,86 1,2 0,44 Naciśnij dowolny klawisz, aby kontynuować. . .

Rysunek 3 — Tablica dynamiczna w C++

Praca z pamięcią dynamiczną jest często wąskim gardłem wielu algorytmów, chyba że stosuje się specjalne sztuczki.

W tym artykule przyjrzę się kilku takim technikom. Przykłady w artykule różnią się (na przykład od tego) tym, że operatory new i delete są przeciążone, przez co konstrukcje składniowe będą minimalistyczne, a modyfikacja programu prosta. Opisano również pułapki wykryte w procesie (oczywiście guru, którzy czytają normę od deski do deski, nie będą zaskoczeni).

0. Czy potrzebujemy ręcznej pracy z pamięcią?

Przede wszystkim sprawdźmy, jak sprytny alokator może przyspieszyć pracę z pamięcią.

Napiszmy proste testy dla C++ i C# (C# jest znany z doskonałego menedżera pamięci, który dzieli obiekty na generacje, używa różnych pul dla obiektów o różnych rozmiarach itp.).

Węzeł klasy ( public: Węzeł* następny; ); // ... for (int i = 0; i< 10000000; i++) { Node* v = new Node(); }

Węzeł klasy ( public Node next; ) // ... for (int l = 0; l< 10000000; l++) { var v = new Node(); }

Pomimo całej „sferycznej próżni” tego przykładu, różnica czasu okazała się dziesięciokrotna (62 ms w porównaniu z 650 ms). Dodatkowo przykład w c# jest już skończony i zgodnie z zasadami dobrych obyczajów w c++ należy usunąć wybrane obiekty, co jeszcze bardziej zwiększy lukę (do 2580 ms).

1. Pula obiektów

Oczywistym rozwiązaniem jest pobranie dużego bloku pamięci z systemu operacyjnego i podzielenie go na równe bloki o rozmiarze sizeof (Node), pobranie bloku z puli, gdy pamięć jest przydzielona, ​​i zwrócenie go do puli po zwolnieniu. Najłatwiejszym sposobem na zorganizowanie puli jest lista powiązana pojedynczo (stos).

Ponieważ celem jest jak najmniejsze manipulowanie programem, wszystko, co można zrobić, to dodać mixin BlockAlloc do klasy Node:
Węzeł klasy: public BlockAlloc

Przede wszystkim potrzebujemy puli dużych bloków (stron), które pobieramy z systemu operacyjnego lub C-runtime. Może być zorganizowany nad funkcjami malloc i free, ale dla większej wydajności (aby pominąć dodatkową warstwę abstrakcji), używamy VirtualAlloc/VirtualFree. Te funkcje alokują pamięć w blokach, które są wielokrotnościami 4K, a także rezerwują przestrzeń adresową procesu w blokach, które są wielokrotnościami 64K. Określając jednocześnie opcje zatwierdzenia i rezerwy, pomijamy kolejny poziom abstrakcji, rezerwując przestrzeń adresową i przydzielając strony pamięci w jednym wywołaniu.

Klasa Page Pool

inline size_t align(size_t x, size_t a) ( return ((x-1) | (a-1)) + 1; ) //#define align(x, a) ((((x)-1) | ( (a)-1)) + 1) szablon class PagePool ( public: void* GetPage() ( void* page = VirtualAlloc(NULL, PageSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); pages.push_back(strona); strona powrotna; ) ~PagePool() ( for (wektor ::iterator i = strony.początek(); i != strony.end(); ++i) ( VirtualFree(*i, 0, MEM_RELEASE); ) ) prywatne: wektor strony; );

Następnie organizujemy pulę klocków o określonej wielkości

Klasa BlockPool

szablon klasa BlockPool: PagePool ( public: BlockPool() : head(NULL) ( BlockSize = align(sizeof(T), Alignment); count = PageSize / BlockSize; ) void* AllocBlock() ( // todo: lock(this) if (!head) FormatNewPage(); void* tmp = head; head = *(void**)head; return tmp; ) void FreeBlock(void* tmp) ( // todo: lock(this) *(void**)tmp = head; head = tmp; ) private: void* head; size_t BlockSize; size_t count; void FormatNewPage() ( void* tmp = GetPage(); head = tmp; for(size_t i = 0; i< count-1; i++) { void* next = (char*)tmp + BlockSize; *(void**)tmp = next; tmp = next; } *(void**)tmp = NULL; } };

komentarz // do zrobienia: zablokuj(to) miejsca wymagające synchronizacji między wątkami są zaznaczone (na przykład użyj EnterCriticalSection lub boost::mutex).

Pozwólcie, że wyjaśnię, dlaczego podczas „formatowania” strony abstrakcja FreeBlock nie jest używana do dodawania bloku do puli. Gdyby było napisane coś takiego

Dla (rozmiar_t i = 0; i< PageSize; i += BlockSize) FreeBlock((char*)tmp+i);

Wtedy strona zgodnie z zasadą FIFO byłaby oznaczona „w odwrotnej kolejności”:

Kilka bloków żądanych z puli z rzędu miałoby malejące adresy. A procesor nie lubi się cofać, to psuje Prefetch ( UPD: Nie dotyczy nowoczesnych procesorów). Jeśli robisz znaczniki w pętli
for (size_t i = RozmiarStrony-(Rozmiar Bloku-(RozmiarStrony%RozmiarBloku)); i != 0; i -=Rozmiar Bloku) FreeBlock...
wtedy cykl znaczników wróci do adresów.

Teraz, gdy przygotowania są zakończone, możemy opisać klasę mixinów.
szablon class BlockAlloc ( public: static void* operator new(size_t s) ( if (s != sizeof(T)) ( return::operator new(s); ) return pool.AllocBlock(); ) static void operator delete(void * m, size_t s) ( if (s != sizeof(T)) ( ::operator delete(m); ) else if (m != NULL) ( pool.FreeBlock(m); ) ) // todo: zaimplementuj przeciążenia nothrow_t, zgodnie z komentarzem borisko // http://habrahabr.ru/post/148657/#comment_5020297 // Unikaj ukrywania nowego miejsca, które jest potrzebne kontenerom stl... static void* operator new(size_t, void * m) ( return m; ) // ...i ostrzeżenie o braku miejsca delete... static void operator delete(void*, void*) ( ) private: static BlockPool basen; ); szablon blokuj pulę Blokuj Przydział ::basen;

Wyjaśnij, dlaczego kontrole są potrzebne if (s != rozmiar(T))
Kiedy pracują? Następnie, gdy klasa zostanie utworzona/usunięta, dziedziczona z bazy T.
Potomkowie będą używać zwykłego nowego/usuniętego, ale BlockAlloc można również mieszać. Dzięki temu możemy łatwo i bezpiecznie określić, które klasy powinny korzystać z puli bez obaw o złamanie czegoś w programie. Dziedziczenie wielokrotne również działa świetnie z tą mieszanką.

Gotowy. Dziedziczymy Node z BlockAlloc i ponownie uruchamiamy test.
Czas testu wynosi teraz 120 ms. 5 razy szybciej. Ale w c# alokator jest jeszcze lepszy. To prawdopodobnie nie jest tylko połączona lista. (Jeżeli jednak zaraz po new, od razu wywołamy delete, a tym samym nie zmarnujemy dużo pamięci, wpasowując dane do pamięci podręcznej, to dostajemy 62 ms. Dziwne. Dokładnie tak jak .NET CLR, jakby zwracał zwolnione zmienne lokalne natychmiast do odpowiedniej puli, bez czekania na GC)

2. Pojemnik i jego kolorowa zawartość

Jak często spotykasz klasy, które przechowują wiele różnych obiektów podrzędnych, tak że czas życia tych ostatnich nie jest dłuższy niż czas życia rodzica?

Na przykład może to być klasa XmlDocument wypełniona klasami Node i Attribute, a także c-strings (char*) pobrane z tekstu wewnątrz węzłów. Lub lista plików i katalogów w menedżerze plików, które są ładowane raz podczas ponownego czytania katalogu i już się nie zmieniają.

Jak pokazano na wstępie, usuwanie jest droższe niż nowe. Ideą drugiej części artykułu jest przydzielenie pamięci dla obiektów podrzędnych w dużym bloku skojarzonym z obiektem Parent. Gdy obiekt nadrzędny zostanie usunięty, destruktory zostaną jak zwykle wywołane w dzieciach, ale pamięć nie będzie musiała zostać zwrócona - zostanie zwolniona w jednym dużym bloku.

Utwórzmy klasę PointerBumpAllocator, która może odgryźć kawałki o różnych rozmiarach z dużego bloku i przydzielić nowy duży blok, gdy stary zostanie wyczerpany.

Klasa PointerBumpAllocator

szablon class PointerBumpAllocator ( public: PointerBumpAllocator() : free(0) ( ) void* AllocBlock(size_t block) ( // todo: lock(this) block = align(block, Alignment); if (block > free) ( free = align (block, PageSize); head = GetPage(free); ) void* tmp = head; head = (char*)head + block; free -= block; return tmp; ) ~PointerBumpAllocator() ( for (vector ::iterator i = strony.początek(); i != strony.end(); ++i) ( VirtualFree(*i, 0, MEM_RELEASE); ) ) private: void* GetPage(rozmiar_t rozmiar) ( void* strona = VirtualAlloc(NULL, rozmiar, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); pages.push_back(strona) ; strona powrotna; ) wektor strony; pustka*głowa; rozmiar_t za darmo; ); typedef PointerBumpAllocator<>Alokator domyślny;

Na koniec opiszmy mixin ChildObject z przeciążonym new i usuńmy dostęp do danego alokatora:

Szablon struct ChildObject ( static void* operator new(size_t s, A& allocator) ( return allocator.AllocBlock(s); ) static void* operator new(size_t s, A* allocator) ( return allocator->AllocBlock(s); ) static void operator delete(void*, size_t) ( ) // *1 statyczny void operator delete(void*, A*) ( ) static void operator delete(void*, A&) ( ) private: static void* operator nowy(size_t s ); );

W tym przypadku, oprócz dodania domieszki do klasy potomnej, będziesz musiał również naprawić wszystkie wywołania na nowe (lub użyć wzorca "fabryki"). Składnia nowego operatora będzie wyglądać tak:

New(...parametry dla operatora...) ChildObject(...parametry dla konstruktora...)

Dla wygody zdefiniowałem dwa nowe operatory, które przyjmują A& lub A*.
Jeśli alokator zostanie dodany do klasy nadrzędnej jako członek, pierwsza opcja jest wygodniejsza:
węzeł = nowy(alokator) XmlNode(nazwa węzła);
Jeśli podzielnik zostanie dodany jako przodek (nieczystość), wygodniejszy jest drugi:
węzeł = new(to) XmlNode(nazwa węzła);

Nie ma specjalnej składni do wywoływania usuwania, kompilator wywoła standardowe usuwanie (oznaczone *1) bez względu na to, który nowy operator został użyty do utworzenia obiektu. Oznacza to, że składnia usuwania jest normalna:
Usuń węzeł;

Jeśli wystąpi wyjątek w konstruktorze ChildObject (lub jego potomku), delete zostanie wywołane z podpisem zgodnym z podpisem nowego operatora użytego do utworzenia tego obiektu (pierwszy parametr size_t zostanie zastąpiony przez void*).

Umieszczenie nowego operatora w sekcji prywatnej zapobiega wywołaniu nowego bez podania alokatora.

Podam kompletny przykład użycia pary Allocator-ChildObject:

Przykład

class XmlDocument: public DefaultAllocator ( public: ~XmlDocument() ( for (vector ::iterator i = węzły.początek(); i != nodes.end(); ++i) ( usuń (*i); ​​​​) ) void AddNode(char* treść, znak* nazwa) ( char* c = (char*)AllocBlock(strlen(content)+1); strcpy(c, treść ); char* n = (char*) AllocBlock(strlen(name)+1); strcpy(n, content); nodes.push_back(new(this) XmlNode(c, n)); ) class XmlNode: public ChildObject ( public: XmlNode(char* _content, char* _name) : content(_content), name(_name) ( ) private: char* content; char* name; ); prywatny:wektor węzły; );

Wniosek. Artykuł został napisany 1,5 roku temu dla piaskownicy, ale niestety moderatorowi się to nie podobało.

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 raz, na początku 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

Kiedy 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 prawidłowo obsługiwany (a tak będzie, ponieważ nie zajrzeliśmy jeszcze 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 twój 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; )