Używanie delete

0

Prawie każdy i w prawie każdej książce, powiedziane jest, że przed zakończeniem programu wszystkie zmienne dynamiczne należy usuwać poprzez operator delete (oczywiście zakładając, że tworzymy je operatorem new). Dziś znajomy powiedział mi, że po wyłączeniu programu, wszystkie zmienne, które były deklarowane na stercie, są automatycznie zwalniane przez system operacyjny i dzieje się tak już od Windowsa 98 włącznie. Czy w takim razie faktycznie zwalnianie pamięci poprzez delete jest koniecznie przed zamknięciem programu? I czy naprawdę tak się dzieje, że systemy operacyjne same zwalniają pamięć, która została zaniechana przez program?

1

Zwalnianie nie jest konieczne, jednak sprzątanie po sobie jest dobrym nawykiem (jak napisałeś, sprzątnie to system po zamknięciu aplikacji). W szczególności w aplikacjach, które pracują ciągle.
Kolejne alokacje powodują, że program ma "używa" coraz więcej pamięci, której system nie może przeznaczyć na coś innego. W przypadku, gdy skończy się pamięć fizyczna, zaczyna się zrzucanie części pamięci na dysk (swapowanie) i późniejsze odczytywanie na żądanie, co ma znaczny wpływ na wydajność całego systemu.

Tak z grubsza.

1

Jeśli pracujesz na systemie, który to zrobi (a niemal na pewno tak jest) to możesz zostawić zwolnienie pamięci systemowi - choć dobrą praktyką tego bym nie nazwał. Jak z każdą "troszkę szemraną" praktyką - ważne, żeby wiedzieć co się robi i kiedy może ona zawieść, a jeśli chcemy, żeby w ogóle nigdy nie zawodziła, to pewnie trzeba to zrobić inaczej. ;P
Może być też tak, że system zwolni pamięć za Ciebie trochę szybciej (bo jako jeden spory kawałek w jednym wywołaniu). Dla przykładu, programy w tutorialach FLTK wyglądają tak:

// hello.cxx (example1)

#include <fltk/Window.h>
#include <fltk/Widget.h>
#include <fltk/run.h>
using namespace fltk;

int main(int argc, char **argv) {
  Window *window = new Window(300, 180);
  window->begin();
  Widget *box = new Widget(20, 40, 260, 100, "Hello, World!");
  box->box(UP_BOX);
  box->labelfont(HELVETICA_BOLD_ITALIC);
  box->labelsize(36);
  box->labeltype(SHADOW_LABEL);
  window->end();
  window->show(argc, argv);
  // a deleta, sory, dilejta, brak ;)
  return run();
}

Co oczywiście nie znaczy, że można sobie delete i delete[] w ogóle podarować. Nie chcesz przecież, żeby Twój program np co wywołanie funkcji foo() gubił pamięć, aż do wywalenia się z powodu braku pamięci? :P
edit: No chyba, że użyjemy sprytnych wskaźników, wtedy delete znacznie rzadziej zobaczymy. ;)

2

i dzieje się tak już od Windowsa 98 włącznie
Windows 98 nie ma tu nic do rzeczy. Dzieje się tak i w DOS-ie. Od zawsze, nazwijmy to.

Ale dlaczego mimo to należy używać delete? Bo czasem alokujesz pamięć (new) w pętli. Czasem w inny powtarzalny sposób (np. pod kliknięciem przycisku).
Brak zwalniania pamięci wtedy spowoduje, że z czasem program będzie zajmował coraz więcej pamięci, aż do niebotycznych rozmiarów.
W małym programie, gdzie alokujesz sobie jakąś tablicę, coś tam wyświetlasz, zwalniasz i koniec – to nie ma znaczenia. Ale warto wyrabiać sobie dobre nawyki, a nie złe.

0

Okej, dzięki za odpowiedzi.

Mam jeszcze jedno pytanie (tak mnie naszło przy kolacji :D) związane z pamięcią, pośrednio dynamiczną. Gdy alokujemy pamięć dla jakiejś struktury danych, to we wskaźniku trzymamy sobie jej adres pamięci. No i właśnie ... ten adres pamięci wskazuje na co? Na blok pamięci? Chodzi mi głównie o to, w jaki sposób system potem jest w stanie rozpoznać co kryje się pod danym adresem. Każdy adres, gdy go sobie wyświetlam, składa się z kilku znaków (jeśli dobrze pamiętam to 6) i jak się domyślam, raz może wskazywać na blok o wielkości 8 bitów, a raz 10 Mb. W jaki sposób to działa? Czy system wyszukuje blok pamięci o odpowiednim rozmiarze, któremu potem nadaje numer czy działa to jakoś inaczej? Przepraszam, jeśli napisałem to nie jasno, ale sam nie wiem za bardzo jak skonstruować stosowne pytanie.

0

Rozpoznaje to po typie tego wskaźnika-i tak na przykład definicja

int *w=new int;

mówi kompilatorowi,że pod adresem przechowywanym we wskaźniku w siedzi dana typu int.To samo jeśli chodzi o struktury czy klasy.
Ciutkę innaczej ma się sprawa z tablicami,bo po stworzeniu trzeba samemu pamiętać ich rozmiar,nie ma jak go pobrać po stworzeniu.Nadto dobrze jest wskaźnik na początek tablicy definiować jako stały:

int *const tab=new int[someNumber];
0
MasterBLB napisał(a)

Rozpoznaje to po typie tego wskaźnika-i tak na przykład definicja

int *w=new int;

mówi kompilatorowi,że pod adresem przechowywanym we wskaźniku w siedzi dana typu int.To samo jeśli chodzi o struktury czy klasy.

Bredzisz bracie jak potłuczony. new i delete w wersji nieprzeciążonej to tylko bezpieczniejsze nakładki na malloc i free. Czy free jest świadome istnienia typów? free nie pyta nawet o rozmiar bo rozmiar jest zawsze zapisywany przy dynamicznej alokacji, zazwyczaj w strukturze umieszczonej bezpośrednio przed zwróconym z alokatora adresem. Wiesz w ogóle jak wygląda operator delete? Prototypy ma następujące:

  1. operator delete(void*)
  2. operator delete(void*, const std::nothrow_t&)
  3. operator delete[](void*)
  4. operator delete[](void*, const std::nothrow_t&)

Nie ma ani konkretnego typu, ani też przekazywania rozmiaru, bo nikt normalny nie implementuje alokatorów tak żeby zwalnianie tego wymagało. Używałeś kiedykolwiek polimorfizmu? Obiekt klasy pochodnej jest trzymany przez wskaźnik na klasę bazową, wywołanie delete działa poprawnie chociaż rozmiar obiektów może być skrajnie różny (vie emulacja interface'ów poprzez klasy czysto wirtualne i "ciężkie" obiekty je implementujące).

0

Gdy alokujemy pamięć dla jakiejś struktury danych, to we wskaźniku trzymamy sobie jej adres pamięci. No i właśnie ... ten adres pamięci wskazuje na co? Na blok pamięci?
tak. przy czym „blok” to dużo powiedziane. adres to liczba, kolejny numer bajtu w pamięci. nie ma w nim wcale napisane, czy to wskaźnik na inta, czy na dziesięć stringów. ta informacja jest znana w momencie kompilacji programu (dlatego kompilator nie przepuści kodu gdy typ wskaźnika się nie zgadza), ale wszelkie typy i ich nazwy są po tym etapie tracone. wykonywalny program operuje na adresach i bajtach pamięci.

Czy system wyszukuje blok pamięci o odpowiednim rozmiarze, któremu potem nadaje numer czy działa to jakoś inaczej?
przydzielanie pamięci jest dwuetapowe.
Pierwszy etap, to gdy w momencie uruchamiania programu system operacyjny przydziela programowi jakiś początkowy obszar pamięci, na przykład 2 megabajty. zapisuje sobie tę informację gdzieś w tablicy (mapie pamięci) że taki-a-taki program zajmuje tyle-a-tyle RAMu, i od tego-a-tego miejsca. Te informacje służą potem do wyszukiwania wolnych bloków dla innych programów, by się w pamięci nie nachodziły.

Systemu nie interesują takie drobnostki jak zmienne wewnątrz programu: operuje na całych programach.

Drugi poziom alokacji pamięci odbywa się wewnątrz programu. Zajmuje się tym menedżer pamięci będący częścią tzw. runtime, czyli biblioteki standardowej języka programowania, w którym program został napisany. Są to funkcje typu malloc/free, new/delete i inne, zależne od języka.
Istotne jest, że są one częścią naszego programu, a nie systemu operacyjnego. Dlatego działają w ramach przydzielonego przez system obszaru, operując na niewielkich blokach, o rozmiarach pojedynczych zmiennych lub niewiele większych. Jednak zasada działania jest podobna: alokacja pamięci polega na dokonaniu do jakiejś tablicy czy struktury wpisu, że pod danym adresem ileś-tam bajtów jest zajętych.
Algorytm przydzielania pamięci pilnuje, by zmienne dynamiczne nie nachodziły na siebie, oraz by pamięci nie marnować (np. małe zmienne alokować obok siebie, bo jak będą poszatkowane to zabraknie ciągłego miejsca dla dużych).
Są różne algorytmy przydzielania pamięci.
Zwolnienie pamięci polega na usunięciu wpisu z tablicy – w zasadzie nic poza tym. Zmienna nie jest wymazywana, unicestwiana - po prostu blok jest oznaczany jako wolny do przydzielenia innej zmiennej.

Z poziomu języka nie mamy jednak dostępu do wewnętrznych struktur menedżera pamięci - a nawet jeśli się do tego dobierzemy, to nie ma to wielkiego sensu, gdyż format tych struktur może się zmieniać z wersji na wersję kompilatora.

0
Azarien napisał(a)

Zwolnienie pamięci polega na usunięciu wpisu z tablicy – w zasadzie nic poza tym. Zmienna nie jest wymazywana, unicestwiana - po prostu blok jest oznaczany jako wolny do przydzielenia innej zmiennej.
No nie zupełnie, przy zwolnieniu sprawdza się czy poprzedni blok pamięci jest wolny, jeżeli tak to łączy ich w jeden większy, po czym (a może przed tym) sprawdza czy poprzedni blok pamięci jest wolny, jeżeli tak to znowu łączy ich w jeden większy.

0

Z poziomu języka nie mamy jednak dostępu do wewnętrznych struktur menedżera pamięci - a nawet jeśli się do tego dobierzemy, to nie ma to wielkiego sensu, gdyż format tych struktur może się zmieniać z wersji na wersję kompilatora.

A nie przypadkiem systemu operacyjnego? Wydaje mi się że malloc (na systemach Windows) to tylko wrapper na VirtualAlloc czy inny AllocateHeap.

1 użytkowników online, w tym zalogowanych: 0, gości: 1