Uwalnianie pamięci po wskaźnikach (Dispose)

0

Mam klasę TVector, która przypina się do danych wewnątrz macierzy (przechowywanych jako dwuwymiarowa tablica dynamiczna array of array of Double), za pomocą swoich wskaźników X, Y, Z, W: ^Double (dzięki takiemu rozwiązaniu mogę jedne i te same dane obrabiać raz jako macierz, raz jako zbiór wektorów, co jest bardzo wygodne, bo mogę np. wywoływać metody z klasy TVector odpowiedzialne za normalizację, mnożenie itd). Niemniej czasem potrzebuję skorzystać z samodzielnego wektora, który nie jest podpięty do żadnej macierzy i posiada własne zmienne. Żeby więc nie tworzyć dla takiego wektora osobnej klasy (co poza dublowaniem kodu, rozwaliłoby mi wszystkie funkcje i procedury, które jako parametr przyjmują TVector i korzystają z jego wskaźników), po prostu alokuję pamięć pod te wskaźniki (używając New) i przypisuję do nich dane. Sęk w tym, że wbrew wszelkim opisom (także z Embarcadero) Dispose wcale tych wskaźników nie zwalnia. Pamięć przypisana do wskaźnika po wywołaniu Dispose powinna być niedostępna, a u mnie tak się nie dzieje. Baa, nawet po rozwaleniu instancji klasy TVector (przez Destroy lub Free) nadal mogę się do tych wskaźników odwoływać. Moje pytanie brzmi więc: Jak prawidłowo zwalniać pamięć po takich wskaźnikach?

0
Crow napisał(a):

Żeby więc nie tworzyć dla takiego wektora osobnej klasy (co poza dublowaniem kodu, rozwaliłoby mi wszystkie funkcje i procedury, które jako parametr przyjmują TVector i korzystają z jego wskaźników), po prostu alokuję pamięć pod te wskaźniki (używając New) i przypisuję do nich dane.

A przechowujesz informację o tym, czy sam alokowałeś pamięć czy wskazujesz na dane w macierzy? W tym przypadku prosta flaga w postaci pola logicznego powinna wystarczyć. Podczas zwalniania klasy sprawdzasz czy flaga jest zapalona i jeśli tak to wołasz Dispose.

Sęk w tym, że wbrew wszelkim opisom (także z Embarcadero) Dispose wcale tych wskaźników nie zwalnia.

Jeśli sam zaalokowałeś pamieć to bezproblemu powinna być ona zwolniona. Jeśli te wskaźniki wskazują na dane z macierzy, to Dispose może nic z tym blokiem nie zrobić (i nie powinien).

Pamięć przypisana do wskaźnika po wywołaniu Dispose powinna być niedostępna, a u mnie tak się nie dzieje. Baa, nawet po rozwaleniu instancji klasy TVector (przez Destroy lub Free) nadal mogę się do tych wskaźników odwoływać.

Tylko pod warunkiem, że zwalniana pamięć ma być zerowana, a zgaduję, że tak nie jest.


PS: Metoda Free woła w sobie Destroy, więc żadna różnica którą wywołasz na istniejącym obiekcie.

0

Te wektory, które mają alokowaną przeze mnie pamieć, nigdy na nic nie wskazują i nie łączą się nigdy z żadną macierzą (nie przechowują wierzchołków modeli, a jedynie wektory normalne wielokątów, potrzebne przy teksturowaniu). Zaraz po utworzeniu tych wektorów alokuję im pamięć, a następnie przypisuje wartości, które nigdzie indziej nie są przechowywane.

Zresztą taki kod też działa, chociaż powinien(?) wywalić błąd:

procedure TForm1.Button1Click(Sender: TObject);
var
  P: ^Integer;
begin
  New(P);
  P^:= 12;
  Dispose(P);
  ShowMessage(IntToStr(P^));
end;

//Poprawnie wyświetla 12
0

Pokaż jakiś kod, bo ten opis z pierwszego posta jest trudny do zrozumienia.

Zresztą taki kod też działa, chociaż powinien(?) wywalić błąd: […]

No nie powinien, póki adres na jaki ten wskaźnik wskazuje jest różny od adresu zerowego (czyli P <> nil). Jeśli koniecznie chcesz zerować porzuconą pamięć to sprawdź czy jest taka opcja w ustawieniach kompilacji.

0
furious programming napisał(a):

No nie powinien, póki adres na jaki ten wskaźnik wskazuje jest różny od adresu zerowego (czyli P <> nil).

Czyli Dispose nie powoduje natychmiastowego efektu w postaci oczyszczenia pamięci? W końcu nawet po jego wywołaniu, wskaźnik dalej wskazuje na to samo miejsce w pamięci (nie zostaje wyzerowany), a dodatkowo to miejsce w pamięci nadal przechowuje tę samą wartość (tutaj integer = 12).

Czy dobrze zatem rozumiem, że Dispose po prostu oznacza daną komórkę w pamięci jako "do dyspozycji buffora", a więc w razie potrzeby będzie ona mogła zostać przez ten buffor nadpisana? Nie wiem tylko dlaczego z dokumentacji wynika, że po wywołaniu Dispose, komórka w pamięci (przypisana wcześniej do wskaźnika) powinna być niedostępna.

0
Crow napisał(a):

Czyli Dispose nie powoduje natychmiastowego efektu w postaci oczyszczenia pamięci?

Na to wygląda, że nie. ;)

[…] a dodatkowo to miejsce w pamięci nadal przechowuje tę samą wartość (tutaj integer = 12).

W tym przykładnie odczytałeś wartość spod wskaźnika tuż po użyciu Dispose, więc ten blok pamięci nie został jeszcze nadpisany, dlatego odczytałeś taką samą wartość.

Nie rozumiem tylko dlaczego z dokumentacji wynika, że po wywołaniu Dispose, komórka w pamięci (przypisana wcześniej do wskaźnika) powinna być niedostępna.

Nie znalazłem takiej informacji. Jest tylko napisane, że po użyciu Dispose, wartość wskaźnika staje się niezdefiniowana i że podanie do Dispose wskaźnika pokazującego na niezaalokowany blok pamięci wyrzuci błąd (zależny od platformy). Nie ma nic na temat używania wskaźnika po jego podaniu do Dispose.

Źródło tutaj: System.Dispose.

0

Czyli wystarczy wywołać Dispose w destruktorze klasy i można uznać, że pamięć alokowana do jej wskaźników (co do których zostało wywołane Dispose) zostanie oddana do dyspozycji bufora? No i spoko :). Czyli robiłem wszystko jak trzeba, ale nie byłem pewien, czy właściwie zarządzam pamięcią. Piszę aplikację, w której na raz muszę przechowywać w pamięci dosyć spore obiekty trójwymiarowe (mające nawet po 70-80 tysięcy wielokątów, gdzie każdy musi mieć wyznaczony wektor normalny i to właśnie dla nich alokuję pamięć), więc nie chciałem, żeby przy wczytywaniu kolejnych modeli pamięć się kitowała.

Tak czy inaczej, dzięki za pomoc :).

PS. furious, jakich bibliotek używasz do rysowania w swojej grze platformowej? Ja póki co zainwestowałem w GDI+, ale zastanawiam się nad przesiadką na Direct2D, bo zapewnia wsparcie dla renderowania na GPU.

1
Crow napisał(a):

Czyli wystarczy wywołać Dispose w destruktorze klasy i można uznać, że pamięć alokowana do jej wskaźników (co do których zostało wywołane Dispose) zostanie oddana do dyspozycji bufora? No i spoko :)

To co w środku robi Dispose nie ma znaczenia – ważne jest to, że ta procedurka powinna zostać wywołana dla każdego ręcznie zaalokowanego bloku pamięci. Ewentualnie, zamiast New i Dospose możesz skorzystać z GetMem i FreeMem – inna składnia, ale ten sam efekt.

Czyli robiłem wszystko jak trzeba, ale nie byłem pewien, czy właściwie zarządzam pamięcią.

Postępujesz zgodnie z wytycznymi, więc wszystko jest w porządku. Zastanawia mnie natomist implementacja:

Crow napisał(a):

Mam klasę TVector, która przypina się do danych wewnątrz macierzy (przechowywanych jako dwuwymiarowa tablica dynamiczna array of array of Double), za pomocą swoich wskaźników X, Y, Z, W: ^Double (dzięki takiemu rozwiązaniu mogę jedne i te same dane obrabiać raz jako macierz, raz jako zbiór wektorów […]

Czemu używasz macierzy dynamicznej i czterech wskaźników, zamiast zrobić sobie rekord ze wielopostaciową zawartością, tzw. variant record? Mógłbyś mieć jeden blok pamięci, a mimo to traktorać go raz jako macierz czteroelementową, raz jako kwadratową, a innym razem odwoływać się do poszczególnych współrzędnych.

Przykład:

type
  TSuperDuperVertex = record
    case Integer of
      1: (Flat: array [0 .. 3] of Double);
      2: (Squared: array [0 .. 1, 0 .. 1] of Double);
      3: (X, Y, Z, W: Double);
  end;

Wymóg jednak jest taki, że rozmiar rekordu musi być stały – nie można użyć typów danych wymagających inicjalizacji (czyli przede wszystkim macierzy dynamicznych).


PS. furious, jakich bibliotek używasz do rysowania w swojej grze platformowej?

Żadnych dodatkowych – takie sobie założenie przyjąłem. ;)

Wszystko co związane jest z procesem renderowania klatek napisałem sam, wykorzystując jedynie zawartość klasy TCanvas oraz to co zawierają biblioteki standardowe. Absolutnie podstawowe, wieloplatformowe i uniwersalne metody/funkcje, wykorzystywane do malowania różnych rzeczy głównie w aplikacjach okienkowych.

0
furious programming napisał(a):

Czemu używasz macierzy dynamicznej i czterech wskaźników, zamiast zrobić sobie rekord ze wielopostaciową zawartością, tzw. variant record? Mógłbyś mieć jeden blok pamięci, a mimo to traktorać go raz jako macierz czteroelementową, raz jako kwadratową, a innym razem odwoływać się do poszczególnych współrzędnych.

Przykład:

type
  TSuperDuperVertex = record
    case Integer of
      1: (Flat: array [0 .. 3] of Double);
      2: (Squared: array [0 .. 1, 0 .. 1] of Double);
      3: (X, Y, Z, W: Double);
  end;

Wymóg jednak jest taki, że rozmiar rekordu musi być stały – nie można użyć typów danych wymagających inicjalizacji (czyli przede wszystkim macierzy dynamicznych).


Nigdy z tego nie korzystałem, jak to działa? Czy te zmienne wskazują na te same komórki w pamięci? Bo właśnie jest moim celem. Wiadomo, że jak chce się wykonać jakieś przekształcenia na obiekcie 3D, to trzeba jego wszystkie wierzchołki przemnożyć przez odpowiednią macierz. Ja jednak wpadłem na pomysł, że zamiast bawić się w dziesiątki tysięcy mnożeń w stylu wektor x macierz, lepiej zebrać wierzchołki do kupy i przetrzymywać je jako jedną, dużą macierz. Wiem, że mógłbym swoją macierz zbudować na zasadzie array of TVector, ale to ma być w zamyśle macierz uniwersalna, tj. o dowolnych wymiarach (od 2x2, do nawet np. 20x20). Stwierdziłem jednak, że nie chcę zupełnie rezygnować z możliwości interpretowania takiej macierzy jako zbioru wektorów (oczywiście wtedy, gdy jest to możliwe, czyli gdy macierz ma nie więcej niż 4 wiersze) i mogę w każdej chwili taki wektor edytować. Kawałek kodu dla rozjaśnienia:

TVectorPointers = record
  X, Y, Z, W: ^Double;
end;

TVectorValues = record
  X, Y, Z, W: Double;
end;

TVector = record
  private
    PointerVector: Boolean;
  public
    Value: TVectorValues;
    Pointer: TVectorPointers;
    Width: Double;
    procedure Fill(pVector: TVectorValues);
    procedure ReadWidth;
    procedure Normalize;
  end;

TMatrix = class
  strict private
    type
      TSub_Vectors = record
        Vector: array of TVector;
        procedure Add(pVector: TVectorValues);
        procedure Remove(Index: Integer);
      end;
  public
    Grid: TAoAoD; //typ oznaczający array of array of Double;
    RowCount, ColCount: Integer;
    Vectors: TSub_Vectors;
    constructor Create; overload;
    constructor Create(pGrid: TAoAoD); overload;
    constructor Create(pMatrix: TMatrix); overload;
    procedure Multiply(pMatrix: TMatrix);
    procedure Transpose;
  end;

Tu jeszcze widać moją poprzednią koncepcję, to znaczy TVector miał 2 "wyjścia", jedno ze wskaźnikami (TVectorPointers), drugie ze zmiennymi (TVectorValues) i w zależności od flagi (PointerVector) procedury i funkcje (do których TVector wchodził jako parametr) wiedziały skąd mają czerpać, czy z Value czy z Pointer. Teraz jednak wpadłem na ten pomysł, że zamiast rozróżniać na wskaźniki i zmienne, mogę zostawić w kodzie same wskaźniki i w razie potrzeby po prostu alokować je ręcznie (żeby działały jak zwykłe zmienne). Zanim jednak przystąpiłem do przerabiania całego kodu, postanowiłem zadać tu pytanie, żeby nie musieć potem wszystkiego klepać od nowa :).

Tak jak już pisałem, kluczowy jest dla mnie dostęp do Grid w TMatrix. Czyli np. tworzę sobie macierz i wypełniam (w konstruktorze) zawartość Grid:

Macierz:= TMatrix.Create([[1, 0, 0, 0],
                                           [0, 1, 0, 0],
                                           [0, 0, 1, 0],
                                           [0, 0, 0, 1]]);

Nagle jednak stwierdzam, że 3 kolumna (używam wektorów pionowych), powinna zostać znormalizowana. Nie ma problemu, robię to tak:

Macierz.Vectors.Vector[2].Normalize;

I sprawa załatwiona. Nie muszę robić żadnych konwersji, np. przepisywać najpierw z Grid macierzy do Value wektora, znormalizować, a potem w drugą stronę, przepisywać z Value wektora do Grid macierzy. Nie muszę, bo macierz i wektor korzystają z tych samych komórek pamięci. Zmiana w jednym, od razu nanosi zmiany w drugim, to był mój nadrzędny cel.

Żadnych dodatkowych – takie sobie założenie przyjąłem. ;)

Wszystko co związane jest z procesem renderowania klatek napisałem sam, wykorzystując jedynie zawartość klasy TCanvas oraz to co zawierają biblioteki standardowe. Absolutnie podstawowe, wieloplatformowe i uniwersalne metody/funkcje, wykorzystywane do malowania różnych rzeczy głównie w aplikacjach okienkowych.

To w jaki sposób udało ci się uzyskać całkiem niezłą płynność obrazu (tj. stabilne FPSy)?. Rozdzielczość jest tam malutka, ale VCL to i z czymś takim się dławi :).

1
Crow napisał(a):

Nigdy z tego nie korzystałem, jak to działa?

Tak samo jak to:

var
  Buffer: array [0 .. 3] of Double;
var
  Flat: array [0 .. 3] of Double absolute Buffer;
  Squared: array [0 .. 1, 0 .. 1] of Double absolute Buffer;
var
  X: Double absolute Buffer[0];
  Y: Double absolute Buffer[1];
  Z: Double absolute Buffer[2];
  W: Double absolute Buffer[3];

Tyle że mniej pisania.

Czy te zmienne wskazują na te same komórki w pamięci?

Tak – rozmiar struktury wynosi 4 * SizeOf(Double). Wielopostaciowość takiego rekordu pozwala traktować jego zawartość w różny sposób, bez konieczności rzutowania, tworzenia dodatkowych wskaźników o innych typach czy kopiowania pamięci pomiędzy zmiennymi.

Bo właśnie jest moim celem.

No niestety, ale Ty potrzebujesz macierzy dynamicznych, a te nie są dozwolone w takich rekordach.

I sprawa załatwiona. Nie muszę robić żadnych konwersji, np. przepisywać najpierw z Grid macierzy do Value wektora, znormalizować, a potem w drugą stronę, przepisywać z Value wektora do Grid macierzy. Nie muszę, bo macierz i wektor korzystają z tych samych komórek pamięci.

Pamiętaj, że nawet bez wielopostaciowych rekordów, możesz mieć jeden blok pamięci danego typu, a mimo to używać go również do innych celów, bez konieczności kopiowania pamięci. Do tego celu właśnie istnieje absolute, aby można było ”zadeklarować zmienną pod adresem innej zmiennej”, czyli de facto stworzyć sobie do niej alias, tego samego lub innego typu.


To w jaki sposób udało ci się uzyskać całkiem niezłą płynność obrazu (tj. stabilne FPSy)?

Magia. ;)

  • obraz klatki renderowany jest na tylnym buforze w postaci instancji klasy TBitmap, który docelowo malowany jest bezpośrednio na płótnie okna,
  • grafika gry ma być rozpikselowana, więc pomocniczy bufor ma niewielki rozmiar (raptem 224x128 pikseli) i malowany jest na płótnie okna z dopasowaniem do jego obszaru,
  • do prostych operacji graficznych, takich jak kopiowanie całego/fragmentu obrazu z pominięciem koloru określającego przezroczystość, zmianę jasności pikseli, malowanie szumu, wypełnianie platform kolorem itd. wykorzystuję ScanLine i ręczne modyfikowanie pamięci obrazu, co jest szybsze od metod z klasy TCanvas,
  • pozostałe operacje związane z renderowaniem opierają się na standardowych metodach, takich jak StretchDraw, GradientFill, FillRect, Line itd., bo albo trudno było by je samemu napisać (np. odpowiednik StretchDraw), albo nie ma to sensu, bo przyrost wydajności byłby nieznaczny.

Jeśli Cię to bardzo interesuje to możesz pobrać źródła tej gierki i sobie zobaczyć. Tyle tylko że to projekt tworzony w Lazarusie, no i jest dość spory, więc musiałbyś poświęcić trochę czasu na zapoznanie się z jego strukturą. Ale jakby co to wszystkie własne procedury i funkcje dotyczące obróbki grafiki zawarte są w module Platformer.Utils.pp.

0
furious programming napisał(a):

Pamiętaj, że nawet bez wielopostaciowych rekordów, możesz mieć jeden blok pamięci danego typu, a mimo to używać go również do innych celów, bez konieczności kopiowania pamięci. Do tego celu właśnie istnieje absolute, aby można było ”zadeklarować zmienną pod adresem innej zmiennej”, czyli de facto stworzyć sobie do niej alias, tego samego lub innego typu.

Niby spoko, tylko że - o ile dobrze rozumiem - absolute muszę zastosować już na etapie deklaracji, a ja przecież wtedy jeszcze nie wiem, czy wektor będzie pełnił rolę samodzielną, czy też będzie częścią macierzy (o tym decyduję dopiero tworząc instancję). Pewnie mógłbym się pobawić w jakieś kontenery pośrednie, ale czy to cokolwiek uprości?

Mając coś takiego:

TVector = record
  X: Double absolute ?
  Y: Double absolute ?
  Z: Double absolute ?
  W: Double absolute ?
end;

Do czego miałbym się odwoływać?

A i jeszcze jedna rzecz mi się przypomniała. Jeżeli TVector jest rekordem, to czy po jego zwolnieniu (czyli w momencie "śmierci" klasy, do której został doczepiony), Dispose zostaje wywołane automatycznie? No bo wiadomo, że rekordy destruktów mieć nie mogą i nie wiem, czy muszę z tym kombinować jakoś na około (np. pozwalniać te wektory w destruktorze klasy do której przynależą), czy też w momencie ubicia rekordu (na skutek zwolnienia jego macierzystej klasy) pamięć sama się "zdisposuje"? Sprawdzić tego nie mogę, bo jak ustaliliśmy, efekt Dispose nie jest natychmiastowy, więc możliwość odwołania się do tej komórki pamięci po śmierci rekordu nie jest niestety wystarczającą odpowiedzią.

0
Crow napisał(a):

Niby spoko, tylko że - o ile dobrze rozumiem - absolute muszę zastosować już na etapie deklaracji […]

absolute można zastosować wyłącznie na etapie deklaracji zmiennej lokalnej lub globalnej, bo jest to konstrukcja przeznaczona tylko dla zmiennych. Za jego pomocą można stworzyć zmienną istniejącą pod adresem innej zmiennej (lokalnej lub globalnej) lub parametru. Wszystko po to, aby zlikwidować niezgodność typów i wykluczyć konieczność rzutowania bądź używania wskaźników.

Sam często stosuję aliasy w zdarzeniach, żeby nie używać rzutowania, np.:

// "Sender" zawsze jest klasy "TPaintBox"
procedure Form.PaintBoxPaint(Sender: TObject);
var
  // więc tworzę alias konkretnej klasy
  Box: TPaintBox absolute Sender;
begin
  // i go używam, bez rzutowania
  Box.Canvas.Brush.Color := clWhite;
  Box.Canvas.FillRect(Box.ClientRect);
end;

No ale to jeśli chodzi o ogólne możliwości zastosowania absolute – a tych zastosowań jest sporo.

Jeżeli TVector jest rekordem, to czy po jego zwolnieniu (czyli w momencie "śmierci" klasy, do której został doczepiony), Dispose zostaje wywołane automatycznie?

Nie, rekordy są zarządzane, więc alokacją pamięci dla nich i jej zwalnianiem zajmuje się menedżer pamięci. W takim przypadku nigdzie nie ma jawnego wywołania Dispose. Zresztą nie jest to normalna procedura, a pseudoprocedura, której kod generowany jest dynamicznie w trakcie kompilacji.

Tak samo jak w przypadku wielu innych pseudoprocedur, takich jak Write, Read, New, Include, Exit, Concat, Pred, Succ i masy innych. Nagłówków tych dziwadeł nie znajdziesz w bibliotece standardowej.

No bo wiadomo, że rekordy destruktów mieć nie mogą i nie wiem, czy muszę z tym kombinować jakoś na około (np. pozwalniać te wektory w destruktorze klasy do której przynależą), czy też w momencie ubicia rekordu (na skutek zwolnienia jego macierzystej klasy) pamięć sama się "zdisposuje"?

Jeśli używasz zmiennej jakiegokolwiek typu zarządzanego (typu prostego, ciągu znaków, zbioru, macierzy, rekordu, starego obiektu itd.) to menedżer zajmuje się alokacją i dealokacją pamięci. Jeśli dana zmienna uzyskuje zasięg to pamięć dla niej zostaje automatycznie zaalokowana, a po utracie zasięgu automatycznie zwolniona.

Pamięć dla zmiennych lokalnych, dla przykładu zwykłej procedury, alokowana jest podczas wywołania tej procedury, a zwalniana po wyjściu z niej. Dla zmiennych globalnych pamięć alokowana jest podczas tworzenia modułu, a zwalniana przy jego zwalnianiu. Dla pól klas, które de facto też są zmiennymi globalnymi, tyle że ich zasięg jest globalny dla klasy, a nie dla modułu, pamięć alokowana jest automatycznie w trakcie tworzenia instancji klasy, a zwalniana również automatycznie podczas destrukcji tej instancji.

To co napisałem wyżej ma zastosowanie tylko dla typów zarządanych. Jeśli deklaruje się zmienną typu niezarządzanego, to pamięć automatycznie alokowana jest i dealokowana tylko np. dla wskaźnika (np. cztery bajty dla x86), ale nie dla jego danych. Dopiero w momencie wywołania New, GetMem czy konstruktora klasy, alokowana jest pamięć dla danych, i analogicznie zwalniana po wywołaniu Dispose, FreeMem czy destruktora klasy. Przy czym zwolnienie pamięci oznacza tylko jej uwolnienie, a nie uwolnienie i wyzerowanie.

Sprawdzić tego nie mogę, bo jak ustaliliśmy, efekt Dispose nie jest natychmiastowy […]

Dispose jest natychmiastowy – uwalnia obszar pamięci wskazywany przez wskaźnik, ale tego bloku nie zeruje, bo to strata czasu. Tak samo usunięcie pliku z dysku nie powoduje wyzerowania bloku pamięci który zajmował.

[…] więc możliwość odwołania się do tej komórki pamięci po śmierci rekordu nie jest niestety wystarczającą odpowiedzią.

Możesz się odwołać do uwolnionego bloku pamięci tylko dlatego, że kod wynikowy, generowany w trakcie kompilacji nie zawiera takich zabezpieczeń, jakich byś sobie życzył. Dlatego też możesz sobie czytać śmieci z pamięci i nawet o tym nie wiedzieć, ale tylko wtedy, gdy adres na jaki pokazuje wskaźnik nie jest zerowy.

Natomiast to, że uwolniony blok zawiera jeszcze przez jakiś czas dane w postaci identycznej jak sprzed uwolnienia to tylko przypadek. Dokumentacja tę kwestię wyjaśnia – po wywołaniu Dispose, wartość wskaźnika staje się niezdefiniowana (może być taka sama, a może być inna).

Nie można natomiast wielokrotnie zwolnić tego samego bloku pamięci, bo zawsze skończy się to błędem.

0

Czyli... pamięć zostanie zwolniona automatycznie, dla alokowanego przeze mnie wskaźnika w rekordzie, czy nie? O to pytałem, a w sumie odpowiedzi na to pytanie zabrakło :). Bo tak, napisałeś, że Dispose NIE ZOSTANIE wywołane w chwili śmierci rekordu czy klasy, ale to nie odpowiada na moje pytanie, czy ja powinienem je wywołać ręcznie (np. w destruktorze klasy macierzystej dla rekordu), czy też pamięć zostanie zwolniona automatycznie (co prawda bez użycia Dispose, ale jednak efekt zostanie osiągnięty bez mojej ingerencji).

0
Crow napisał(a):

Czyli... pamięć zostanie zwolniona automatycznie, dla alokowanego przeze mnie wskaźnika w rekordzie, czy nie?

Musisz zrozumieć dwie rzeczy. Wskaźnik jako zmienna to nic innego jak liczba, określająca adres danych. Wskaźnik zawsze istnieje, bo pamięć dla niego alokowana jest automatycznie (tylko dla wskaźnika, czyli tylko dla liczby-adresu, którą przechowuje). Natomiast adres jaki ten zawiera, wskazuje na dane i dla nich (dla danych) pamięć trzeba alokować i zwalniać ręcznie.

Tak więc nie „alokuje się wskaźnika”, a blok pamięci, na który ten ma wskazywać. Alokacja pamięci to nic innego jak odnalezienie wolnego bloku, zarezerowanie go i wpisanie adresu pierwszego bajtu tego bloku do miejsca, pod którym wskaźnik istnieje. To się dzieje podczas wywołania np. New.

Mając wskaźnik, np. o nazwie Data typu PInteger, mamy do dyspozycji trzy konstrukcje:

  • @Data – odczytanie adresu wskaźnika, czyli miejsca, w którym zapisany jest adres danych,
  • Data – odczytanie adresu danych, na które wskaźnik pokazuje,
  • Data^ – odczytanie danych, na które wskaźnik pokazuje.

Bo tak, napisałeś, że Dispose NIE ZOSTANIE wywołane w chwili śmierci rekordu czy klasy ale to nie odpowiada na moje pytanie […]

Odpowiada na pytanie…

[…] czy ja powinienem je wywołać ręcznie (np. w destruktorze klasy macierzystej dla rekordu), czy też pamięć zostanie zwolniona automatycznie (co prawda bez użycia Dispose, ale jednak efekt zostanie osiągnięty bez mojej ingerencji).

Musisz zwolnić pamięć po rekordzie ręcznie tylko wtedy, gdy Twoja zmienna jest wskaźnikiem na rekord i sam zaalokowałeś pamięć za pomocą New czy GetMem. Jeśli Twoja zmienna nie jest wskaźnikiem, a po prostu rekordem, to alokacją i dealokacją pamięci zajmuje się menedżer, bo rekordy są zarządzane.

type
  PFoo = ^TFoo;
  TFoo = record
    Num: Integer;
    Str: String;
  end;
  
  procedure UsageViaPointer();
  var
    Foo: PFoo; // wskaźnik na rekord
  begin
    New(Foo); // alokujemy pamięć ręcznie
    
    Foo^.Num := 128; // modyfikujemy zawartość
    Foo^.Str = 'number';
    
    Dispose(Foo); // dealokujemy pamieć ręcznie (w przeciwnym razie mamy wyciek)
  end;
  
  procedure UsageAsRecord();
  var
    Foo: TFoo; // zwykły rekord
  begin
    // niczego nie alokujemy – pamięć alokowana jest automatycznie

    Foo.Num := 128; // modyfikujemy zawartość
    Foo.Str := 'number';
    
    // niczego nie dealokujemy – tym zajmie się menedżer
  end;

Teraz rozumiesz? Jeśli nie, to Ci obrazek namaluje. ;)

0
furious programming napisał(a):

Musisz zwolnić pamięć po rekordzie ręcznie tylko wtedy, gdy Twoja zmienna jest wskaźnikiem na rekord i sam zaalokowałeś pamięć za pomocą New czy GetMem. Jeśli Twoja zmienna nie jest wskaźnikiem, a po prostu rekordem, to alokacją i dealokacją pamięci zajmuje się menedżer, bo rekordy są zarządzane.
[...]
Teraz rozumiesz? Jeśli nie, to Ci obrazek namaluje. ;)

To, to ja rozumem, tylko pytam o troszkę coś innego :D. Chodzi o taką sytuację:

type
  TVector = record
    Value: ^Double
  end;

  TMatrix = class
    Vec: TVector;
  end;

var
  Mat: TMatrix;

//Gdzieś w kodzie

New(Mat.Vec.Value);

//Gdzieś indziej w kodzie

Mat.Free;

Czy powyższa operacja zwolni także pamięć po wskaźniku Value, czy też muszę sam o to zadbać, np. w destruktorze klasy TMatrix?

1
Crow napisał(a):

Czy powyższa operacja zwolni także pamięć po wskaźniku Value, czy też muszę sam o to zadbać, np. w destruktorze klasy TMatrix?

Nie, zadaniem metody Free jest zwolnienie instancji klasy, a konkretniej, wywołaniem destruktora. Pole TMatrix.Vec jest typu rekordowego, więc alokacją i dealokacją pamięci okupowanej przez ten rekord zajmie się menedżer pamięci.

Natomiast pole TVector.Value to wskaźnik, który wskazuje na blok pamięci który sam zaalokowałeś (za pomocą New), więc również sam musisz zadbać o zwolnienie tego bloku (za pomocą Dispose):

// gdzieś w kodzie

Mat := TMatrix.Create();
New(Mat.Vec.Value);

// gdzieś indziej w kodzie

Dispose(Mat.Vec.Value);
Mat.Free();

Podsumowując, każdy New musi mieć do pary Dispose – to sobie zapamiętaj. Jeśli któregoś zabraknie to albo program będzie czytał śmieci (za mało New), albo będzie generował błędy/wyjątki (za dużo Dispose), albo będzie powodował wycieki (za mało Dispose).

Tyle tylko, że absolutnie nie powinieneś alokować pamięci dla składowych klasy z zewnątrz. Czyli nie powinieneś wołać New i Dispose dla pól, tak jak to widać w przykładzie wyżej. Kod za to odpowiedzialny powinien się w całości znajdować wewnątrz niej, w odpowiednich metodach.

0

No i o to mi chodziło, serdeczne dzięki :).

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