delete na wskazniku - i wciaz mozna sie przez niego odwolac?

1

Witam,
Moim pytaniem-problemem jest zwrot wskaźnika. Chodzi o to że mając np stos muszę go zaopatrzeć w funkcje zdejmującą element. I teraz taka przykładowa funkcja kończy się tak:

.
.
.
Element *P = HEAD;
HEAD = HEAD->next;
delete HEAD;
return &P->dane;
}

Uogólniając pod koniec funkcji zwracamy wskaźnik na jakąś daną która (chyba) nie powinna już istnieć bo międzyinnymi zwolniliśmy jej pamięć operatorem delete. Później za pomocą zwracanego wskaźnika dajmy na to wypiszemy jej wartość.

cout << *(Funkcja()) << endl;

I teraz taki przykład znalazłem taki "zabieg" w książce "thinking in C++" str 204 (funkcja pop()), nie będę ukrywał że fakt iż to działa bardzo mnie zdumiał. Jednak spróbowałem napisać coś podobnego:

#include <iostream>
using namespace std;
char *Wskfun();
int main(){
    char *j = Wskfun();
    cout << j << endl;
    system("PAUSE");
    return 0;
}
char *Wskfun(){
	char *A = new char[] = "awdawdd";
        delete []A;
	return A;
}
 

No i tu już zwiecha ... Czy ktoś może mi wytłumaczyć dlaczego tak się dzieje ?

0

Doczytałem że usuwanie (delete) elementow zawierajacych wskazniki void nie idzie do konca z planem i mozna sie potem dalej do nich odwołac. Czy mógłby ktoś rozwinąć dlaczego tak się dzieje ? I jak je do końca "wymazać" ?

przykladowy kod:

#include <iostream>
using namespace std;
void *Wskfun();
struct C{
        void *B; 
	};
int main(){
    char *j = (char*)Wskfun();
    cout << j << endl;
    system("PAUSE");
    return 0;
}
void *Wskfun(){
	C *cC = new C;
	cC->B = new char[] = "awdawdd";
	void *P = cC->B;
	delete cC;
	return P;
}
0

Ale o co dokładnie pytasz? Wskaźnik wskazuje na miejsce w pamięci i już. W pewnej chwili miałeś tam dynamicznie stworzoną tablicę i wskaźnik pokazywał na nią. Ale jak skasowałeś tablicę, to przecież nie zniszczyłeś tego "miejsca" w pamięci, więc wskaźnik moze na to miejsce wskazywać (przecież możesz nawet ręcznie wpisać adres w pamięci gdzie chcesz pokazać wskaźnikiem).
Próba odczytania wartości to jeszcze nie jest problem, ot wyrzuci ci jakies śmieci. Gorzej jakbyś chciał tam cos wpisać!
Grębosz w Symfonii poleca żeby w chwili kasowania takiej tablicy przez delete[] od razu ustawiać wskaźnik na adres 0 (po prostu przez wsk=0)
Wtedy przynajmniej nie zepsujesz nic próbując wpisać dane do "nie swojego" miejsca w pamięci.

0

Shalom: Próbowałeś skompilować te 2 powyższe kody ?

0

@Shalom, odczyt zwolnionej pamięci może spowodować błąd, ale nie musi, tak samo jak zapis zwolnionej pamięci MOŻE, ale nie MUSI. OS zwykle przydziela pamięć całymi stronami, więc jeśli dana strona jest we władaniu procesu, to zapis do nieprzydzielonego fragmentu strony nie musi spowodować błędu.

Nawet lepiej, błąd może być zgłoszony wiele tysięcy taktów później, przy PRAWIDŁOWEJ, niczemu nie winnej operacji na pamięci, jeśli poprzednia operacja zapisu uszkodziła wewnętrzne struktury alokatora.

Ogólnie zwolnienie jakiegoś fragmentu pamięci nie oznacza automatycznie ochrony tego fragmentu przed zapisem / odczytem, podobnie nie oznacza też niestety automatycznie udostępnienia tego fragmentu innym aplikacjom (oddania do OS). Zwolnienie informuje jedynie alokator, że ten fragment może być wykorzystany przy ponownym wywołaniu new lub malloc przez ten sam proces. Ewentualnie alokator może oddać całą stronę pamięci do OS, ale musi być ona czysta.

Stąd da się w C/C++ napisać aplikację, która mimo wywoływania wszystkich delete jak trzeba, zabiera z systemu kilka razy tyle pamięci, co suma wszystkich zaalokowanych bloków.

@Daniel:
delete nie działa rekursywnie. Tzn. jeśli zwalniasz obiekt, który zawiera wskaźniki, pamięć wskazywana przez te wskaźniki NIE JEST ZWALNIANA przez delete. Zwalniana jest tylko pamięć zajęta przez obiekt przekazany do delete. Stąd, kod który przedstawiłeś w ostatnim poście jest w 100% poprawny.
Jeżeli chcesz zwalniać obiekty wskazywane przez wskaźniki składowe Twojego obiektu, musisz napisać destruktor a w nim umieścić kolejne wywołania delete dla wskaźników składowych.

Podobnie jest z tablicami. Zwolnienie tablicy wskaźników zwalnia tylko miejsce zajęte przez te wskaźniki, ale nie zwalnia pamięci wskazywanych obiektów.

0

Czyli operator delete mowi alokatorowi że dana pamięć jest zwolniona tzn można znów coś na niej zapisać natomiast owa pamięć nie jest czyszczona, a usuwany jest tylko wskaźnik na tą pamięć.

W takim razie jakbyś rozwiązał problem zwrócenia i jednocześnie usunięcia jakiegoś elementu ze stosu w jednej funkcji? Nie chciałbym by klient-programista po otrzymaniu wskaźnika musiał sam zwalniać jego pamięć.

0

1a. A musisz przekazywać wskaźnik? Nie możesz z funkcji przekazać po prostu obiektu?
1b. Napisz w dokumentacji: "Zwolnić wskaźnik po użyciu, bo jak nie, to #$%&%&^%&"
2. Użyj shared_ptr z Boost (mało wydajne niestety).
3. Użyj GC, np. Boehm GC i nie zwalniaj wskaźnika w ogóle (też mało wydajne, ale chyba lepsze niż shared_ptr)
4. Napisz to w Javie albo C#. (wbrew pozorom wydajniejsze niż 2. i 3.) [diabel]

0
Daniel___ napisał(a)

Czyli operator delete mowi alokatorowi że dana pamięć jest zwolniona tzn można znów coś na niej zapisać natomiast owa pamięć nie jest czyszczona, a usuwany jest tylko wskaźnik na tą pamięć.

czyli operator delete(free) mówi alokatorowi, że wcześniej przydzielony fragment jest już nie używany przez twój program i może być potem przydzielony innemu programowi, żaden wskaźnik nie jest usuwany, ba nawet delete nie modyfikuje wartość wskaźnika podanego jako parametr, dlatego warto czasem po wywołaniu delete nadać mu NULL

Daniel___ napisał(a)

W takim razie jakbyś rozwiązał problem zwrócenia i jednocześnie usunięcia jakiegoś elementu ze stosu w jednej funkcji? Nie chciałbym by klient-programista po otrzymaniu wskaźnika musiał sam zwalniać jego pamięć.

Wybacz, ale tego o co ci chodzi to ja nie rozumiem. Zasada jest taka: każda alokacja musi być zakończona dealokacją delete i tyle, nie ma co kombinować...

Jeśli masz jakiś zlepek obiektów, które są mają być tworzone na życzenie użytkownika i potem zachodzi potrzeba zwolnienia ich wszystkich, można by zrobić vector czy listę wskaźników:

vector<void*> lp_obiekty;
lp_obiekty.push_back(new Klasa);//alokacja nowego obiektu
for(int i=0;i<lp_obiekty.size();i++) delete lp_obiekty[i];
lp_obiekty.clear();
0
crayze napisał(a)
Daniel___ napisał(a)

Czyli operator delete mowi alokatorowi że dana pamięć jest zwolniona tzn można znów coś na niej zapisać natomiast owa pamięć nie jest czyszczona, a usuwany jest tylko wskaźnik na tą pamięć.

czyli operator delete(free) mówi alokatorowi, że wcześniej przydzielony fragment jest już nie używany przez twój program i może być potem przydzielony innemu programowi

Nie. Bardzo często po zwolnieniu ta pamięć nadal należy do TWOJEGO programu i NIE MOŻE być użyta przez inne. Spędzało to sen z powiek twórcom Firefoxa przez bardzo długi czas...

0

Dziekuje za wyjaśnienie problemu.

P.S.

Krolik napisał(a)

Nie. Bardzo często po zwolnieniu ta pamięć nadal należy do TWOJEGO programu i NIE MOŻE być użyta przez inne. Spędzało to sen z powiek twórcom Firefoxa przez bardzo długi czas...

Ciekaw jestem jak to zostało rozwiązane, swoją drogą pamiętam że firefox po dłuższym używaniu potrafił zużywać ogromne ilości pamięci.

0

Podejrzewam, że napisali własny, dedykowany alokator dla pewnej grupy obiektów.

Języki korzystające z defragmentujących GC (Java, C#, VB.NET, Lisp) nie cierpią z powodu fragmentacji pamięci, przy zachowaniu podobnej wydajności* systemu zarządzania pamięcią jak C++.

*) dzięki czemu w pewnych sytuacjach może Cię zdziwić, jak program napisany w C#/Java będzie chodził szybciej niż odpowiednik w C++.

0
Krolik napisał(a)

Nie. Bardzo często po zwolnieniu ta pamięć nadal należy do TWOJEGO programu i NIE MOŻE być użyta przez inne. Spędzało to sen z powiek twórcom Firefoxa przez bardzo długi czas...

Będę polemizował...
Wszystko zależy od wielkość alokacji, jak wyniesie ona kilka stron pamięci, to przecież przy zwolnieniu zostaną oddane do OS'a...
Przy małych alokacjach całkiem logiczne że wszystko zostanie...

Niemniej jednak, CZĘSTO przy przekraczaniu tablicy o chociażby ten 1 bajt, poinformowało mnie że ją przekraczam :>
nie wiem, może mam małą stronę pamięci(total: 386MB,na XP), nie chce mi się sprawdzać ile dokładnie mam, a jakoś z przyzwyczajenia tablicom często przydzielam wartości potęgi 2 (128,256), stąd być może zajmuję/dopełniam dokładnie całą stronę/strony pamięci i dlatego każde często przekroczenie skutkuje AccessViolation'em, więc w tym przypadku delete może rzeczywiście zwalniać całą stronę...

IMO zakładanie, że delete może nie zwolnić całej pamięci, nawet przy małych alokacjach jest dla mnie niedopuszczalne, jest od tego żeby zwolnić przydzielony fragment i już, a to, że ktoś ma ponad 1GB pamięci i cały proces+wszystkie alokacje mieszczą mu się w jednej przydzielonej stronie pamięci, nic nie znaczy, a może ktoś spróbuje uruchomić program na sprzęcie o bardzo małej ilości pamięci rzędu 32MB/16MB, gdzie strona pamięci będzie naprawdę mała...

0
Daniel___ napisał(a)

W takim razie jakbyś rozwiązał problem zwrócenia i jednocześnie usunięcia jakiegoś elementu ze stosu w jednej funkcji?
Zwróć element przez wartość:

void pop(Dane &daneOut)
{
   Element *P = HEAD->next;
   daneOut = HEAD->dane;
   delete HEAD;
   HEAD = P;
}

Dane pop()
{
   Dane daneOut;
   pop(daneOut);
   return daneOut;
}
Krolik napisał(a)

1b. Napisz w dokumentacji: "Zwolnić wskaźnik po użyciu, bo jak nie, to #$%&%&^%&"
złooo [diabel]

0
Krolik napisał(a)

1b. Napisz w dokumentacji: "Zwolnić wskaźnik po użyciu, bo jak nie, to #$%&%&^%&"
złooo [diabel]

</quote>

Fakt. Ale w pewnych sytuacjach niestety w C++ inaczej się nie da. Przykładowo potrzebujesz jakiegoś dużego obiektu nietymczasowego, który następnie chcesz gdzieś później przez jakiś czas przechowywać. Masz do wyboru albo przekazać go przez wartość i później go skopiować ze stosu na stertę (niewydajne), albo przekazać przez wskaźnik i albo zastosować jakiś trick z inteligentnymi pointerami, albo narzucić na użytkownika obowiązek zwolnienia tego wskaźnika po ostatnim użyciu.

Gdzieś kiedyś czytałem nawet, że przez te problematyczne zwracanie obiektów, C++ nie jest do końca modularne, bo nie pozwala w 100% ukryć szczegółów implementacyjnych danego modułu.

0
Krolik napisał(a)

albo narzucić na użytkownika obowiązek zwolnienia tego wskaźnika po ostatnim użyciu.
Można, ale niech to bezpośrednio wynika z implementacji. Na pewno nie powinno się zlecać niszczenie takiego obiektu operatorem delete. Można zrobić coś takiego:

 class ResourceData
{
  //...
};

class ResourceProvider
{
private:
   struct Resource
   {
      int refCount;
      ResourceData *data;
      Resource(): refCount(0), data(NULL);
   };
   Resource resources[n];
public:
   class ResourceHandle
   {
      friend class ResourceProvider;
   private:
      Resource* resource;
      ResourceHandle(Resource* _resource): resource(_resource) {}
   public: 
      ResourceData& data() { return *(resource->data); }
   }

   ResourceHandle ProvideResource(int num)
   {
      if(resources[num].data == NULL) resources[num].data = new ResourceData();
      resources[num].refCount++;
      return ResourceHandle(resources + num);
   }
   void ReleaseResource(ResourceHandle)
   {
      resources[num].refCount--;
      if(resources[num].refCount == 0) delete resources[num].data;      
   }
}

Potrzebujemy zasób - wywołujemy ProvideResource, nie potrzebujemy już zasobu - wywołujemy ReleaseResource. Wszystkie implementacyjne szczegóły ukryte, na "zewnątrz" nie operujemy żadnymi wskaźnikami a jedynie obiektem ResourceHandle, którego przeznaczenie jest jasno określone.

Inteligentny wskaźnik też jest jakimś wyjściem, ale osobiście niepolecam. Uważam, że lepiej samemu coś takiego napisać aby było konkretnie wiadomo, z czym to się je.

Poza tym często to "niewydajne" rozwiązanie (przekazywanie przez wartość) jest jednak wydajniejsze niż zarządzanie referencjami, lub jego koszt jest i tak znikomy. Owszem trzeba dbać o wydajność kodu, ale nie należy szukać jej za wszelką cenę.

0

Nie zapominaj, że kopiowanie nie jest obojętne dla semantyki kodu. Czasami CHCĘ mieć współdzielony obiekt, więc muszę posługiwać się wskaźnikami do niego.

Z drugiej strony zabawa zliczaniem referencji może stać się bardzo niefajna w aplikacjach wielowątkowych i ze względu na wydajność, i potencjalne błędy (np. Twoja implementacja używana nierozważnie w przypadku wielu wątków może niezłego kosmosu narobić). Czyli znowu - musisz napisać w specyfikacji - "niebezbieczne ze względu na wątki".

0

Czasami CHCĘ mieć współdzielony obiekt, więc muszę posługiwać się wskaźnikami do niego
Nie dla wskaźników. Uważam, że lepiej zaimplementować uchwyt. Jeśli udostępnisz zasób w postaci wskaźnika, to można z tym wskaźnikiem zrobić praktycznie wszystko co robi się ze wskaźnikami i można się przy tym łatwo pogubić. Nie może być takiej sytuacji, że po części pamięcią zarządza biblioteka a po części programista. Prowadzi to w efekcie do powstawania wycieków.
Jeśli jakiś zasób tworzy biblioteka to niech biblioteka go niszczy. Jeśli mamy coś zniszczyć operatorem delete, to niech będzie to obiekt utworzony przez nas operatorem new.

Z drugiej strony zabawa zliczaniem referencji może stać się bardzo niefajna w aplikacjach wielowątkowych i ze względu na wydajność, i potencjalne błędy (np. Twoja implementacja używana nierozważnie w przypadku wielu wątków może niezłego kosmosu narobić). Czyli znowu - musisz napisać w specyfikacji - "niebezbieczne ze względu na wątki".
Potencjalne błędy w aplikacji wielowątkowej ? Wystarczy zabezpieczyć ProvideResource i ReleaseResource i po niebezpieczeństwie. Pomijam kwestię zabezpieczenia samego zasobu, bo to nie zależy od poruszanego przez nas tematu. Co do wydajności, złożoność dalej zostaje O(1), nie sądzę aby przejście przez sekcję krytyczną i zwiększenie licznika referencji miało jakiś znaczący wpływ na wydajność, skoro zasób to "jakiś duży obiekt nietymczasowy".

Jeśli chciałbyś się jednak posługiwać wskaźnikami to też da się to w miarę bezpiecznie zrealizować. Ale na miłość boską, jeśli mamy coś zniszczyć delete'em to twórzmy to new'em.
Np. (pomijam widoczność pól):

class ResourceProvider //bezpieczniej, jak będzie singletonem
{
   void ProvideResource(Resource *resource);
   void ReleaseResource(Resource *resource);
}

class Resource
{
   ResourceProvider *resourceProvider;
   Resource(ResourceProvider *resourceProvider) 
   { 
      this.resourceProvider = resourceProvider;
      resourceProvider->ProvideResource(this); 
   }
   ~Resource() { resourceProvider->ReleaseResource(this); }
}
0

Wiecie co pomysł ze wzróceniem przez wartość zamiast przez wskaźnik może i jest dobry kiedy znamy typ. Natomiast w przypadku kiedy ten stos jest uniwersalny tzn. może przechowywać dowolne typy za pomocą wskaźników void to już taki zabieg nie można przeprowadzić.

0

Ale wtedy stos przechowuje po prostu wskaźniki void*. Stos nie może zarządzać tym na co wskazują. Samodzielnie nie może utworzyć obiektu bo nie zna typu. Nie może też go samodzielnie niszczyć podczas ściągania ze stosu bo przecież po ściągnięciu obiekt musi być do wykorzystania.

0
adf88 napisał(a)

Ale wtedy stos przechowuje po prostu wskaźniki void*. Stos nie może zarządzać tym na co wskazują. Samodzielnie nie może utworzyć obiektu bo nie zna typu. Nie może też go samodzielnie niszczyć podczas ściągania ze stosu bo przecież po ściągnięciu obiekt musi być do wykorzystania.

No faktycznie, ale głupotę napisałem :-O

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