12.3 Zwalnianie pamięci — operator delete

Zarezerwowaną za pomocą new pamięć należy zwolnić, gdy nie będzie już potrzebna. Wykonuje się to za pomocą operatora delete. Jeśli zaalokowaliśmy, za pomocą new, pamięć na pojedynczy obiekt, to składnia jest następująca:

       delete pi;
Argumentem operatora (w przykładzie powyżej jest to wartość zmiennej wskaźnikowej pi) musi być wyrażenie, którego wartością jest dokładnie ten sam adres, który został nam „przysłany” przez operator new.

Operatora delete nie wolno zastosować do adresu który nie został uprzednio zwrócony przez operator new.

Natychmiast po wykonaniu tej instrukcji pamięć, którą do tej pory zajmował obiekt wskazywany przez pi, jest zwalniana i może być użyta przez system do zapisania innych danych. Z tego powodu, choć nie tylko z tego,

operatora delete nie wolno zastosować dwa razy do tego samego adresu.

Zwróćmy uwagę, że sama zmienna pi nie jest usuwana, ani nawet nie jest zmieniana jej wartość — to pamięć wskazywana przez tę zmienną jest zwalniana!

Inna jest składnia przy zwalnianiu pamięci przydzielonej na tablicę, czyli poprzez new z podaniem wymiaru w nawiasach kwadratowych. Aby tak zaalokowaną pamięć zwolnić, należy użyć operatora delete[]

       delete [] pi;
Wartość wyrażenia, które jest argumentem delete (w powyższym przykładzie pi) musi być adresem zwróconym uprzednio przez operator new alokującego tablicę, a nie pojedynczy obiekt. W nawiasach kwadratowych nie podajemy żadnego wymiaru — musiał być podany podczas alokowania tej pamięci za pomocą new i jest zapamiętywany. Dlatego, jak wspominaliśmy, alokowanie pamięci na jeden egzemplarz obiektu jest czym innym niż alokowanie pamięci na tablicę jednoelementową.

Jeśli alokujemy pamięć na pojedynczy obiekt, to trzeba ją zwolnić za pomocą delete. Jeśli alokujemy pamięć na tablicę (nawet jednoelementową), to trzeba ją zwolnić za pomocą delete[].

Zwalnianie pamięci przydzielonej przez „nietablicowe” new za pomocą „tablicowego” delete lub odwrotnie jest błędem niewychwytywanym przez kompilator, a powodującym nieprzewidywalne, ale raczej opłakane, skutki.

Operator delete, w obu formach — zwykłej i tablicowej, jest tak zdefiniowany, że jego użycie nie powoduje żadnego skutku, jeśli podany adres jest pusty (czyli NULL, nullptr, albo po prostu zero). Dlatego, aby uniknąć przypadkowego wielokrotnego zwalniania tego samego obszaru pamięci, co powoduje najczęściej katastrofalny błąd wykonania, można, po użyciu delete, w odpowiednią zmienna wskaźnikową wpisać adres pusty

      1.      int* pi = new int[40];
      2.      // ...
      3.      delete [] pi;
      4.      pi = 0;
      5.      //
      6.      delete [] pi; // teraz nieszkodliwe
W linii 3 zwalniamy pamięć przydzieloną w linii 1 i natychmiast zerujemy zmienną pi. Teraz, jeśli w dalszej części programu spróbujemy jeszcze raz zwolnić tę samą pamięć, nic złego się nie stanie.

Jako przykład rozważmy następujący problem. Chcemy napisać funkcję minmaxmed znajdującą element najmniejszy, największy i medianę z danych zawartych w tablicy (mediana to taka wartość, że połowa elementów jest od niej nie większa, i połowa jest od niej nie mniejsza). Choć istnieją szybsze metody znajdowania mediany, najprościej zrozumieć następującą. Najpierw sortujemy tablicę, czyli przestawiamy jej elementy tak, aby uzyskać kolejność niemalejącą. Wtedy pierwszy element jest na pewno minimalnym, ostatni maksymalnym, a mediana środkowym lub średnią z dwóch środkowych, jeśli liczba elementów jest parzysta.

Problem w tym, że aby ten algorytm zrealizować, musimy poprzestawiać elementy tablicy, co być może nie jest dopuszczalne, bo będzie ona jeszcze potrzebna w takiej formie, w jakiej jest. Możemy zatem stworzyć tablicę roboczą, przekopiować tam naszą tablicę i szukać wyniku używając tylko tej tablicy roboczej. Realizuje to funkcja minmaxmed poniższego programu:


P85: mediana.cpp     Alokowanie tablic

      1.  #include <iostream>
      2.  using namespace std;
      3.  
      4.  void   pisztab(ostream&,const int[],size_t);
      5.  void   inssort(int[],size_t);
      6.  double minmaxmed(const int[],size_t,int&,int&);
      7.  
      8.  int main() {
      9.      int tab[] = {7,2,6,4,7,5}, min, max;              
     10.      size_t size = sizeof(tab)/sizeof(tab[0]);
     11.  
     12.      double mediana = minmaxmed(tab,size,min,max);
     13.  
     14.      cout << "min = " << min << ", max = " << max
     15.           << ", mediana = "  <<  mediana   << endl;
     16.  
     17.      cout << "Tablica  oryginalna: ";
     18.      pisztab(cout, tab, size);                         
     19.  
     20.      inssort(tab, size);                               
     21.  
     22.      cout << "Tablica posortowana: ";
     23.      pisztab(cout, tab, size);                         
     24.  }
     25.  
     26.  void pisztab(ostream& str, const int t[], size_t size) {
     27.      str << "[ ";
     28.      for (size_t i = 0; i < size; ++i) str << t[i] << " ";
     29.      str << "]" << endl;
     30.  }
     31.  
     32.  void inssort(int a[], size_t siz) {
     33.      size_t indmin = 0;
     34.      for (size_t i = 1; i < siz; ++i)
     35.          if (a[i] < a[indmin]) indmin = i;
     36.      if (indmin != 0) {
     37.          int p = a[0];
     38.          a[0] = a[indmin];
     39.          a[indmin] = p;
     40.      }
     41.      for (size_t i = 2; i < siz; ++i) {
     42.          size_t j = i;
     43.          int v = a[i];
     44.          while (v < a[j-1]) { a[j] = a[j-1]; j--; }
     45.          if (i != j ) a[j] = v;
     46.      }
     47.  }
     48.  
     49.  double minmaxmed(const int t[], size_t size,
     50.                   int& min, int& max) {
     51.      int* tab = new int[size];                         
     52.  
     53.      // byłoby lepiej za pomocą memcpy...
     54.      for (size_t i = 0; i < size; ++i) tab[i] = t[i];  
     55.  
     56.      inssort(tab, size);                               
     57.  
     58.      min = tab[0];
     59.      max = tab[size-1];
     60.  
     61.      double mediana = size%2 == 0 ?
     62.                  0.5*(tab[size/2] + tab[size/2-1])
     63.                : tab[size/2];
     64.      delete [] tab;                                    
     65.      return mediana;
     66.  }

Funkcja minmaxmed otrzymuje tablicę (wskaźnik) t — odpowiadający jej parametr deklarujemy z modyfikatorem const, aby nie dopuścić do przypadkowego zniszczenia oryginalnej tablicy. Następnie () alokujemy pamięć na nową tablicę o takim rozmiarze jak t i przekopiowujemy tam wszystkie elementy (). Dopiero tę tablicę sortujemy (), znajdujemy rezultaty i wpisujemy je do minmax, które były przekazane przez referencje, a więc zmiana ich wartości będzie widoczna w funkcji wywołującej. Na końcu zwracamy przez wartość medianę.

Przed wyjściem z funkcji musimy zwolnić zarezerwowaną pamięć (). Zmienna wskaźnikowa tab jest bowiem lokalna; po wyjściu z funkcji przestanie istnieć i nie będzie już żadnego sposobu, aby odnieść się do zaalokowanej tablicy, w szczególności usunąć ją. Pamięć, jaką zajmuje ta tablica robocza byłaby „zajęta” do końca programu, choć bezużyteczna. Zauważmy, że taki wyciek pamięci powstawałby za każdym wywołaniem funkcji minmaxmed i straty pamięci kumulowałyby się.

Użyta w programie funkcja inssort () realizuje znany, choć w ogólnym przypadku nie najszybszy, algorytm sortowania, tzw. sortowanie przez wstawianie (ang. insertion sort), a konkretnie jego wersję z „wartownikiem”. Algorytm ten jest niesłychanie efektywny dla tablic prawie posortowanych, a więc takich, do których uporządkowania wystarcza niewiele przestawień — sytuacje takie zdarzają się zaskakująco często w praktyce.

W programie głównym tworzymy tablicę () i wysyłamy ją do funkcji minmaxmed. Następnie drukujemy wyniki. W lini drukujemy zawartość tablicy, aby przekonać się, że istotnie nie uległa zmianie w funkcji minmaxmed. Tę tablicę następnie sortujemy () i drukujemy jeszcze raz (), aby wygodniej było sprawdzić prawidłowość otrzymanych wyników

    min = 2, max = 7, mediana = 5.5
    Tablica  oryginalna: [ 7 2 6 4 7 5 ]
    Tablica posortowana: [ 2 4 5 6 7 7 ]
Zauważmy, że funkcja pisztab drukująca zawartość tablicy napisana jest tak, że może pisać do dowolnego strumienia reprezentowanego obiektem klasy ostream. My posyłamy do funkcji obiekt cout, bo innego jeszcze nie znamy, ale dzięki temu zabiegowi ta sama funkcja mogłaby być użyta na przykład przy pisaniu do pliku (patrz rozdział o operacjach we/wy ).

T.R. Werner, 25 lutego 2017; 22:31