• Najnowsze pytania
  • Bez odpowiedzi
  • Zadaj pytanie
  • Kategorie
  • Tagi
  • Zdobyte punkty
  • Ekipa ninja
  • IRC
  • FAQ
  • Regulamin
  • Książki warte uwagi

Zwalnianie pamięci delete

VPS Starter Arubacloud
+1 głos
858 wizyt
pytanie zadane 16 września 2018 w C i C++ przez k4to Początkujący (310 p.)

Witam

Jako jest to mój pierwszy post na tym forum to witam wszystkich ;) 

Dopiero uczę się c++ i jestem na poziomie zdobywania wiedzy o wskaźnikach. Mam pytanie do zwalniania pamięci - delete.

int *wsk=new int; 
*wsk=777;
delete wsk; // zwalniam pamięć 
// co się stanie gdy w dalszej części kodu napiszę tak :
cout<< *wsk;
// lub tak :
*wsk=100;
cout<< *wsk; 

Nie mogę sobie poukładać tego w głowie. Czy dalsze używanie wskaźnika po delete jest niedozwolone, niepoprawne ? 

Powyższy kod rozumiem tak :

1. Rezerwuję pamięć na zmienna int na którą wskazuje wskaźnik *wsk

2. Przypisuje tej zmiennej (jest to zmienna dynamiczna ?) wartość 777

3. Zwalniam pamięć czyli pozwalam komputerowi na użycie pamięć w której się znajdowała zmienna o wartości 777 . Ta pamięć teraz może być wykorzystana w inny sposób ( do innej zmiennej lub do innego programu). Natomiast co się dzieje z tą zmienna która przechowywała wartość 777. I dlaczego jeżeli używam dereferencji wskaźnika to w dalszym ciągu mam wartość 777. Skoro zwolniłem pamięć to gdzie ona jest przechowywana? W Code::Blocks konsola wyświetla mi wartość 777 po zwolnieniu pamięci. Tego nie rozumiem. 

4. Dlaczego również mogę przypisać nową wartość 100 do zmiennej ? (Przed ostatnia linijka kodu) Gdzie ta wartość jest teraz przechowywana ? 

Wiem, że może to być napisane w chaotyczny sposób ale miesza mi się to na maksa. Proszę więc o odpowiedzi prostym językiem tak aby osobą dopiero ucząca się mogła to sobie jakoś wyobrazić ;) 

2 odpowiedzi

+4 głosów
odpowiedź 16 września 2018 przez RafalS VIP (122,820 p.)
wybrane 20 września 2018 przez k4to
 
Najlepsza
Zwolnienie pamięci oznacza przywrocenie jej do puli, z której mozna ponownie alokowac. Np przez Twoje kolejne new, ale nie tylko.

To wszystko oznacza, ze zwolniona pamiec mogla nie zostac ponownie wykorzystana i po prostu dalej jest tam Twoja wartosc, a moze tam byc xokolwiek innego. Zapis moze sie udac a moze spowodować crash programu. Tzw niezdefiniowane zachowanie (undefined behaviour).

Jezyk nie ma narzedzi do wykrywania czy uzywasz nieswojej pamieci. Standard po prostu mowi, ze wtedy moze sie stac cokolwiek, dlatego nigdy tego nie rob, mimo ze sie da i czasem to zadziala.
komentarz 17 września 2018 przez RafalS VIP (122,820 p.)

We wskaźnikach nie ma niczego magicznego. Wskaźnik jest po prostu liczbą oznaczającą adres bloku pamięci.

Tablica natomiast to ciągły blok pamięci. Dlatego na podstawie adresu bloku pamięci oraz liczbie przemieszczenia w bajtach jesteś w stanie skoczyć do danego bloku, w którym zapisany jest i-ty element tablicy. Czyli na podstawie adresu zerowego elementu i typu elementów w tablicy (dzieki typowi wiemy o ile trzeba skakać żeby trafić do kolejnego elementu) jesteś w stanie skoczyć do i-tego elementu tablicy.

a co z reszta elementów

Dlatego właśnie potrzebny jest Ci tylko adres zerowego elementu. 

int *nowa= new int[rozmiar +1]; 

Taki kod robi właściwie dwie rzeczy. new znajduje odpowiedni blok i przydziela go Twojemu programowi oraz zwraca wskaźnik na początek tego bloku, który jest zapisywany w zmiennej nowa. Przypisanie tego adresu do innego wskaźnika nie zmienia niczego w alokacji pamięci. Po prostu dwa wskaźniki wskazują na zaalokowany blok.

komentarz 18 września 2018 przez k4to Początkujący (310 p.)
edycja 18 września 2018 przez k4to
Czyli kodem :

tablica= nowa ;

przydzielam adres z bloku pamięci 'nowa' na wskaźnik 'tablica' tak ? Innymi słowy od teraz wskaźnik tablica jako swoją wartość przyjmuje adres bloku pamięci na który wskazuje 'nowa' . Czy ten zapis można traktować jako skrót np. takiego kodu :

int *tablica = new int[2];

int *nowa= new int[2];

for(int i=0; i<2 ; i++)

tablica[i]=nowa[i];
komentarz 19 września 2018 przez RafalS VIP (122,820 p.)
Nie. Po pracy Ci to rozrysuje w paincie :D
komentarz 19 września 2018 przez k4to Początkujący (310 p.)
Oooh :D to prosze Cię rozrysuj :D

Może inaczej  zapis :

tablica=nowa;

powoduje ze od teraz oba wzkazniki pokazują tą samą lokalizacje w pamięci i nieważne, że tablica na poczatku wskazywał puste miejsce - nic, a nowa wskazywała na pierwszy adres konkretnej pamięci plus kolejne następne (zalezy jaki rozmiar tablicy i typ, to tyle bajtow) tak ? Dla wyobrażenia np. nowa rezerwuje miejsce na 3 elementy int a wiec  zajmuje 3 szufladki pamięci po 4 bajty kazdy, i teraz wskaznik tablica wskazuje na adres pierwszego elementu ale tez jednoczesnie na 2 pozostałe ?
komentarz 19 września 2018 przez RafalS VIP (122,820 p.)
Odpisalem w nowej odpowiedzi, żeby ktoś to zweryfikował :P
+2 głosów
odpowiedź 19 września 2018 przez RafalS VIP (122,820 p.)

Powinienem to napisać w komentarzu, ale może komuś sie jeszcze przyda / ktoś to zweryfikuje.

To jest sterta, składająca się z komórek pamięci. Każda komórka ma swój numerek, za pomocą, którego można się do niej odnieść. Dokładnie tak samo wygląda sterta, na której pracujesz, z tym, że jest większa i Twoje alokacje nie zaczynają się od komórki numer 0. Komórki są tak samo ponumerowane. Jak wypiszesz adres jakiegoś wskaźnika to zobaczysz właśnie ten numer, który domyślnie jest wyświetlany w systemie 16-stkowym, ale to wciaż zwykła liczba.

Na początku w każdej komórce są jakieś śmieci. Losowe wartości. Obszar na szaro jest wolny, niezaalokowany.

No to alokujemy. Najpierw pojedyńczego inta:

new int;

W tym momencie pamięc wygląda tak:


Zakładając, że zajmuje 4 bajty. Zaznaczyłem na czerwono zaalokowaną pamięć. Jednak to by było bez sensu bo pamięć jest zaalokowana, ale nie wiemy gdzie. Dlatego new zwraca adres pierwszej zaalokowanej komórki. W tym przypadku zwróci numer 0. Znając ten numer pamięci możemy zrobić tak:

int *ptr = new int;
*ptr = 5;
//*(0) = 00000000 00000000 00000000 00000101

Te binarne liczby wylądują w 4 pierwszych komórkach pamięci mimo, że próbujesz je wpisać do jednego adresu. Po to własnie jest typ wskaźnika. Na jego podstawie kompilator wie ile komórek pamięci ma wykorzystać.

A teraz zaalokujemy tablice:

int* tab = new int[3];

Zakładając, że zaczęliśmy program od nowa, albo zwolniliśmy wcześniej zaalkowanego pojedyńczego inta to tak wygląda pamięć. Został zaalokowany ciągły blok pamięci na 3 inty, czyli 12 bajtów. New też zwróciło numer 0. Ale my wiemy, że startując od tego numeru przydzielono nam miejsca na 3 inty. A kompilator wie, że typ wskaźnika jest int, więc zna jego rozmiar i wiel o ile bajtów się przesunąc gdy zobaczy tab+1. Będzie on równy blokowi pamięci o numerze 0 + 1*sizeof(int) i w ogólnieniu:

wskaźnik_do_poczatku + i*sizeof(typ_elementu)

We wskaźnikach nie ma niczego magicznego. Są to zwykłe liczby. Czemu nie są zatem typem int? Bo to by było mylące i niepraktyczne. Np mając wskaźnik do wskaźnika kompilator pilnuje nas ze mozemy go 2 razy dereferencjować (gwiazdkować). Podobnie pilnuje nas, żeby nie dało się dereferencjować dowolnego inta. No i druga sprawa to wielkość pojedyńczego elementu. Kompilator musi wiedzieć na jaki typ wskaźnik wskazuje, żeby arytmetyka działała. Żeby wiedział o ile bajtów ma się przesunąc gdy zobaczy kod wsk+3. Ale są to tylko mechanizmy sprawdzania.

 

Żeby nie być gołosłownym napisałem taki kod:

#include <iostream>
#include <bitset>
using namespace std;

int main() {
    int *ptr = new int[2];
    ptr[0] = 1;
    ptr[1] = 2;
    cout << "numer bloku pamieci:\n"<< ptr <<endl;
    long long address_number = (long long) ptr;
    cout <<"adres bloku pamieci zrzutowany do liczby:\n"<< std::hex << address_number << endl;
    cout << "a teraz przesuniemy się recznie\n";

    long long address_number_of_1_element = address_number + sizeof(int);
    cout << "recznie policzony numer bloku\n" << std::hex<< address_number_of_1_element << endl;
    cout << "numer bloku obliczony przez kompilator\n" << ptr + 1 << endl;
    int* very_very_hacky_pointer = (int*)address_number_of_1_element;
    cout << "wartosc wyciągnięta na podstawie dereferencji liczby zrzutowanej \n";
    cout<<"do wskaznika:\n" << *very_very_hacky_pointer << endl;
    cout << ":OOOOOOOOOOOOOOOO to dziala" << endl<<endl;

    cout << "A teraz dobierzemy sie do srodkowego bloku skladajacego sie na inta\n";
    long long address = (long long)ptr + 1;
    int* hacky_pointer_to_the_middle_of_int = (int*)address;
    *hacky_pointer_to_the_middle_of_int = 1;
    cout << bitset<32>(*ptr) << endl;
    cout << "Jak widac dodalismy 1 na poczatku drugiego bajtu" << endl;
}

Mam nadzieję, że teraz rozumiesz co się dzieje gdy zrobisz:

int *x = new int[3];
int *p = x;

Po prostu p będzie zawierać numer bloku pamięci, wskaźnik do początku zaalokowanego bloku. Jest tylko jedna tablica. Wskaźnik to adres jej początku. To zwykła liczba.

PS: nie popieram rzutowania w stylu C w C++, ale (int) jest krótsze niż reinterpret_cast<int>

PSS: kod ma charakter czysto dydaktyczny, nigdy nie wykorzystuj!!!

komentarz 20 września 2018 przez k4to Początkujący (310 p.)
Dzięki. Dużo mi to teraz rozjaśniło ;)

Podobne pytania

0 głosów
1 odpowiedź 133 wizyt
0 głosów
3 odpowiedzi 575 wizyt
0 głosów
1 odpowiedź 261 wizyt
pytanie zadane 7 kwietnia 2023 w C i C++ przez Zuzan Początkujący (390 p.)

92,451 zapytań

141,261 odpowiedzi

319,073 komentarzy

61,853 pasjonatów

Motyw:

Akcja Pajacyk

Pajacyk od wielu lat dożywia dzieci. Pomóż klikając w zielony brzuszek na stronie. Dziękujemy! ♡

Oto polecana książka warta uwagi.
Pełną listę książek znajdziesz tutaj.

Akademia Sekuraka

Akademia Sekuraka 2024 zapewnia dostęp do minimum 15 szkoleń online z bezpieczeństwa IT oraz dostęp także do materiałów z edycji Sekurak Academy z roku 2023!

Przy zakupie możecie skorzystać z kodu: pasja-akademia - użyjcie go w koszyku, a uzyskacie rabat -30% na bilety w wersji "Standard"! Więcej informacji na temat akademii 2024 znajdziecie tutaj. Dziękujemy ekipie Sekuraka za taką fajną zniżkę dla wszystkich Pasjonatów!

Akademia Sekuraka

Niedawno wystartował dodruk tej świetnej, rozchwytywanej książki (około 940 stron). Mamy dla Was kod: pasja (wpiszcie go w koszyku), dzięki któremu otrzymujemy 10% zniżki - dziękujemy zaprzyjaźnionej ekipie Sekuraka za taki bonus dla Pasjonatów! Książka to pierwszy tom z serii o ITsec, który łagodnie wprowadzi w świat bezpieczeństwa IT każdą osobę - warto, polecamy!

...