Czy usuwanie pustego wskaźnika jest bezpieczne?


96

Załóżmy, że mam następujący kod:

void* my_alloc (size_t size)
{
   return new char [size];
}

void my_free (void* ptr)
{
   delete [] ptr;
}

Czy to jest bezpieczne? A może musi ptrzostać rzucony char*przed usunięciem?


Dlaczego sam zarządzasz pamięcią? Jaką strukturę danych tworzysz? Konieczność jawnego zarządzania pamięcią jest dość rzadka w C ++; zwykle powinieneś używać klas, które obsługują to za Ciebie z STL (lub z Boost w razie potrzeby).
Brian

6
Tylko dla osób czytających używam zmiennych void * jako parametrów dla moich wątków w win c ++ (zobacz _beginthreadex). Zwykle dokładnie wskazują na zajęcia.
ryansstack

4
W tym przypadku jest to opakowanie ogólnego przeznaczenia dla nowych / usuniętych, które może zawierać statystyki śledzenia alokacji lub zoptymalizowaną pulę pamięci. W innych przypadkach widziałem wskaźniki obiektów niepoprawnie przechowywane jako zmienne składowe void * i niepoprawnie usuwane w destruktorze bez rzutowania z powrotem na odpowiedni typ obiektu. Byłem więc ciekawy bezpieczeństwa / pułapek.
An̲̳̳drew

2
W przypadku opakowania ogólnego przeznaczenia dla new / delete można przeciążać operatory new / delete. W zależności od używanego środowiska prawdopodobnie uzyskujesz podpięcia do zarządzania pamięcią w celu śledzenia alokacji. Jeśli znajdziesz się w sytuacji, w której nie wiesz, co usuwasz, potraktuj to jako mocną wskazówkę, że Twój projekt jest nieoptymalny i wymaga refaktoryzacji.
Hans,

41
Myślę, że za dużo jest kwestionowania tego pytania, zamiast na nie odpowiadać. (Nie tylko tutaj, ale we wszystkich SO)
Petruza,

Odpowiedzi:


24

To zależy od „bezpiecznego”. Zwykle zadziała, ponieważ informacje są przechowywane wraz ze wskaźnikiem o samym przydziale, więc osoba zwalniająca może zwrócić je we właściwe miejsce. W tym sensie jest to „bezpieczne”, o ile alokator używa wewnętrznych znaczników granic. (Wielu tak robi.)

Jednak, jak wspomniano w innych odpowiedziach, usunięcie pustego wskaźnika nie wywoła destruktorów, co może stanowić problem. W tym sensie nie jest to „bezpieczne”.

Nie ma dobrego powodu, aby robić to, co robisz, tak jak to robisz. Jeśli chcesz napisać własne funkcje zwalniania alokacji, możesz użyć szablonów funkcji do generowania funkcji o odpowiednim typie. Dobrym powodem jest wygenerowanie alokatorów puli, które mogą być niezwykle wydajne dla określonych typów.

Jak wspomniano w innych odpowiedziach, jest to niezdefiniowane zachowanie w C ++. Generalnie dobrze jest unikać niezdefiniowanych zachowań, chociaż sam temat jest złożony i pełen sprzecznych opinii.


9
Jak jest to akceptowana odpowiedź? Nie ma sensu „usuwać pustego wskaźnika” - bezpieczeństwo jest kwestią sporną.
Kerrek SB,

6
„Nie ma dobrego powodu, aby robić to, co robisz, tak jak to robisz”. To jest twoja opinia, a nie fakt.
rxantos

8
@rxantos Podaj kontrprzykład, w którym robienie tego, co chce zrobić autor pytania, jest dobrym pomysłem w C ++.
Christopher

Myślę, że ta odpowiedź jest właściwie w zasadzie rozsądna, ale myślę również, że każda odpowiedź na to pytanie musi przynajmniej wspomnieć, że jest to niezdefiniowane zachowanie.
Kyle Strand

@Christopher Spróbuj napisać pojedynczy system odśmiecania pamięci, który nie jest specyficzny dla typu, ale po prostu działa. Fakt, że sizeof(T*) == sizeof(U*)dla wszystkich T,Usugeruje, że powinno być możliwe posiadanie 1 void *implementacji garbage collectora opartej na szablonach . Ale wtedy, gdy gc faktycznie musi usunąć / zwolnić wskaźnik, pojawia się dokładnie to pytanie. Aby to zadziałało, potrzebujesz opakowań destruktora funkcji lambda (urgh) lub jakiegoś rodzaju dynamicznego typu „typ jako dane”, który umożliwia przechodzenie między typem a czymś, co można przechowywać.
BitTickler

148

Usuwanie za pomocą wskaźnika void jest niezdefiniowane w standardzie C ++ - patrz sekcja 5.3.5 / 3:

W pierwszym wariancie (usuń obiekt), jeśli statyczny typ argumentu jest inny niż jego typ dynamiczny, typ statyczny powinien być klasą bazową typu dynamicznego argumentu, a typ statyczny powinien mieć wirtualny destruktor lub zachowanie jest niezdefiniowane . W drugiej możliwości (usuń tablicę), jeśli typ dynamiczny usuwanego obiektu różni się od jego statycznego, zachowanie jest niezdefiniowane.

I przypis:

Oznacza to, że obiektu nie można usunąć za pomocą wskaźnika typu void *, ponieważ nie ma obiektów typu void

.


6
Czy na pewno trafiłeś we właściwy cytat? Myślę, że przypis odnosi się do tego tekstu: „W pierwszej alternatywie (usuń obiekt), jeśli typ statyczny argumentu różni się od jego typu dynamicznego, typ statyczny powinien być klasą bazową typu dynamicznego operandu, a typ statyczny typ powinien mieć wirtualny destruktor lub zachowanie jest niezdefiniowane. W drugiej alternatywie (tablica usuwania), jeśli typ dynamiczny usuwanego obiektu różni się od jego typu statycznego, zachowanie jest nieokreślone. " :)
Johannes Schaub - litb

2
Masz rację - zaktualizowałem odpowiedź. Nie sądzę jednak, że neguje to podstawowy punkt?

Nie, oczywiście nie. Nadal mówi, że to UB. Co więcej, teraz normalnie stwierdza, że ​​usuwanie void * to UB :)
Johannes Schaub - litb

Wypełnij wskazaną pamięć adresową pustego wskaźnika, NULLrobiąc jakąkolwiek różnicę w zarządzaniu pamięcią aplikacji?
artu-hnrq

25

To nie jest dobry pomysł i nie jest to coś, co można by zrobić w C ++. Bez powodu tracisz informacje o typie.

Twój destruktor nie będzie wywoływany na obiektach w twojej tablicy, które usuwasz, gdy wywołasz go dla typów innych niż pierwotne.

Zamiast tego powinieneś nadpisać nowy / usuń.

Usunięcie pustki * prawdopodobnie przez przypadek poprawnie zwolni pamięć, ale jest to błąd, ponieważ wyniki są niezdefiniowane.

Jeśli z jakiegoś nieznanego mi powodu musisz przechowywać swój wskaźnik w pustce *, to uwolnij go, powinieneś użyć malloc i free.


5
Masz rację co do tego, że destruktor nie został wywołany, ale mylisz się co do nieznanego rozmiaru. Jeśli dasz usunąć wskaźnik, który dostał od nowa, to jest w rzeczywistości znać wielkość rzeczy są usuwane całkowicie niezależnie od typu. Jak to działa, nie jest określone przez standard C ++, ale widziałem implementacje, w których rozmiar jest przechowywany bezpośrednio przed danymi, na które wskazuje wskaźnik zwracany przez „nowy”.
KeyserSoze

Usunięto część dotyczącą rozmiaru, chociaż standard C ++ mówi, że jest niezdefiniowany. Wiem, że malloc / free zadziałaby jednak dla wskaźników void *.
Brian R. Bondy

Nie myśl, że masz link do odpowiedniej sekcji normy? Wiem, że kilka implementacji new / delete, które sprawdziłem, zdecydowanie działa poprawnie bez znajomości typu, ale przyznaję, że nie sprawdziłem, co określa standard. IIRC C ++ początkowo wymagało liczby elementów tablicy podczas usuwania tablic, ale nie jest już wymagane w przypadku najnowszych wersji.
KeyserSoze

Zobacz odpowiedź @Neil Butterworth. Moim zdaniem jego odpowiedź powinna być akceptowana.
Brian R. Bondy

2
@keysersoze: Generalnie nie zgadzam się z twoim stwierdzeniem. Tylko dlatego, że niektóre implementacje zapisały rozmiar przed przydzieloną pamięcią, nie oznacza, że ​​jest to reguła.

13

Usunięcie pustego wskaźnika jest niebezpieczne, ponieważ destruktory nie będą wywoływane na podstawie wartości, na którą faktycznie wskazuje. Może to spowodować wycieki pamięci / zasobów w aplikacji.


2
char nie ma konstruktora / destruktora.
rxantos

9

Pytanie nie ma sensu. Twoje zamieszanie może być częściowo spowodowane niechlujnym językiem, którego ludzie często używają z delete:

Służy deletedo niszczenia obiektu, który został przydzielony dynamicznie. Zrób to, tworząc wyrażenie usuwające ze wskaźnikiem do tego obiektu . Nigdy nie „usuwasz wskaźnika”. To, co naprawdę robisz, to „usuwanie obiektu, który jest identyfikowany przez jego adres”.

Teraz widzimy, dlaczego to pytanie nie ma sensu: wskaźnik pustki nie jest „adresem obiektu”. To tylko adres, bez żadnej semantyki. To może pochodzić z adresu rzeczywistego obiektu, jednak informacje te zostaną utracone, ponieważ została zakodowana w rodzaju oryginalnego wskaźnika. Jedynym sposobem przywrócenia wskaźnika obiektu jest rzutowanie wskaźnika void z powrotem na wskaźnik obiektu (co wymaga od autora wiedzy, co oznacza wskaźnik). voidsam w sobie jest niekompletnym typem, a zatem nigdy nie jest typem obiektu, a wskaźnik void nigdy nie może być użyty do identyfikacji obiektu. (Obiekty są identyfikowane łącznie na podstawie ich rodzaju i adresu).


Trzeba przyznać, że pytanie nie ma większego sensu bez kontekstu. Niektóre kompilatory C ++ nadal będą szczęśliwie kompilować taki bezsensowny kod (jeśli czują się pomocni, mogą wyszczekać ostrzeżenie). Tak więc zadano pytanie w celu oceny znanych zagrożeń związanych z uruchomieniem starszego kodu zawierającego tę nierozważną operację: czy ulegnie awarii? przeciekać część lub całą pamięć tablicy znaków? coś innego, co jest specyficzne dla platformy?
An̲̳̳drew

Dzięki za przemyślaną odpowiedź. Głosowanie za!
An̲̳̳drew

1
@Andrew: Obawiam się, że standard jest dość jasny w tej kwestii: „Wartością argumentu deletemoże być wskaźnik zerowy, wskaźnik do obiektu niebędącego tablicą utworzonego przez poprzednie nowe wyrażenie lub wskaźnik do podobiekt reprezentujący klasę bazową takiego obiektu. Jeśli nie, zachowanie jest niezdefiniowane. " Więc jeśli kompilator zaakceptuje twój kod bez diagnostyki, to nic innego jak błąd w kompilatorze ...
Kerrek SB

1
@KerrekSB - Czy to nic innego jak błąd w kompilatorze - nie zgadzam się. Norma mówi, że zachowanie jest nieokreślone. Oznacza to, że kompilator / implementacja może zrobić wszystko i nadal być zgodna ze standardem. Jeśli odpowiedzią kompilatora jest stwierdzenie, że nie można usunąć wskaźnika void *, to w porządku. Jeśli odpowiedzią kompilatora jest wymazanie dysku twardego, to również jest OK. OTOH, jeśli odpowiedzią kompilatora jest nie generowanie żadnej diagnostyki, ale zamiast tego generowanie kodu, który zwalnia pamięć skojarzoną z tym wskaźnikiem, to też jest OK. To prosty sposób radzenia sobie z tą formą UB.
David Hammen

Wystarczy dodać: nie zgadzam się na używanie delete void_pointer. To niezdefiniowane zachowanie. Programiści nigdy nie powinni wywoływać niezdefiniowanego zachowania, nawet jeśli odpowiedź wydaje się robić to, czego chciał programista.
David Hammen

7

Jeśli naprawdę musi to zrobić, dlaczego nie wyciąć w środku człowieka (The newi deleteoperatorów) i wywołać światowy operator newi operator deletebezpośrednio? (Oczywiście, jeśli próbujesz oprzyrządować operatory newand delete, w rzeczywistości powinieneś ponownie zaimplementować operator newi operator delete.)

void* my_alloc (size_t size)
{
   return ::operator new(size);
}

void my_free (void* ptr)
{
   ::operator delete(ptr);
}

Zauważ, że w przeciwieństwie do malloc(), operator newrzuca std::bad_allocw przypadku niepowodzenia (lub wywołuje, new_handlerjeśli jest zarejestrowany).


To jest poprawne, ponieważ char nie ma konstruktora / destruktora.
rxantos

5

Ponieważ char nie ma specjalnej logiki destruktora. TO nie zadziała.

class foo
{
   ~foo() { printf("huzza"); }
}

main()
{
   foo * myFoo = new foo();
   delete ((void*)foo);
}

Aktor nie zostanie wezwany.


5

Jeśli chcesz użyć void *, dlaczego nie użyjesz tylko malloc / free? new / delete to coś więcej niż tylko zarządzanie pamięcią. Zasadniczo new / delete wywołuje konstruktor / destruktor i dzieje się więcej. Jeśli używasz tylko typów wbudowanych (takich jak char *) i usuniesz je przez void *, zadziała, ale nadal nie jest to zalecane. Najważniejsze jest użycie malloc / free, jeśli chcesz użyć void *. W przeciwnym razie dla wygody możesz użyć funkcji szablonów.

template<typename T>
T* my_alloc (size_t size)
{
   return new T [size];
}

template<typename T>
void my_free (T* ptr)
{
   delete [] ptr;
}

int main(void)
{
    char* pChar = my_alloc<char>(10);
    my_free(pChar);
}

Nie napisałem kodu w przykładzie - natknąłem się na ten wzorzec używany w kilku miejscach, ciekawie mieszając zarządzanie pamięcią C / C ++ i zastanawiałem się, jakie są konkretne zagrożenia.
An̲̳̳drew

Pisanie C / C ++ to przepis na porażkę. Ktokolwiek to napisał, powinien był napisać jedno lub drugie.
David Thornley

2
@David To jest C ++, a nie C / C ++. C nie ma szablonów ani nie używa nowych ani usuwania.
rxantos

5

Wiele osób już skomentowało, że nie, usuwanie pustego wskaźnika nie jest bezpieczne. Zgadzam się z tym, ale chciałem również dodać, że jeśli pracujesz ze wskaźnikami void w celu przydzielenia ciągłych tablic lub czegoś podobnego, możesz to zrobić new, abyś mógł deletebezpiecznie używać (z, ahem trochę dodatkowej pracy). Odbywa się to poprzez przydzielenie pustego wskaźnika do obszaru pamięci (zwanego „areną”), a następnie dostarczenie wskaźnika do areny do nowego. Zobacz tę sekcję w C ++ FAQ . Jest to typowe podejście do implementowania pul pamięci w C ++.


0

Nie ma powodu, aby to robić.

Po pierwsze, jeśli nie znasz typu danych, a wiesz tylko, że to one void*, to naprawdę powinieneś po prostu traktować te dane jako beztypową plamkę danych binarnych ( unsigned char*) i używać malloc/, freeaby sobie z tym poradzić . Jest to czasami wymagane w przypadku takich rzeczy, jak dane falowe i tym podobne, gdzie trzeba je przekazaćvoid* wskaźniki do C apis. W porządku.

Jeśli zrobić znać typ danych (czyli ma konstruktor / dtor), ale z jakiegoś powodu zakończył się z void*wskaźnika (z jakiegokolwiek powodu masz) to naprawdę powinien rzucać go z powrotem do typu znasz go być i wzywaj deletego.


0

Używałem void *, (czyli nieznanych typów) w moim frameworku podczas odzwierciedlania kodu i innych niejednoznaczności, i jak dotąd nie miałem żadnych problemów (wyciek pamięci, naruszenia dostępu itp.) Z żadnych kompilatorów. Tylko ostrzeżenia związane z niestandardową obsługą.

Całkowicie sensowne jest usuwanie nieznanego (void *). Po prostu upewnij się, że wskaźnik jest zgodny z tymi wytycznymi, w przeciwnym razie może przestać mieć sens:

1) Nieznany wskaźnik nie może wskazywać na typ, który ma trywialny dekonstruktor, a więc rzucony jako nieznany wskaźnik NIGDY nie powinien BYĆ USUWANY. Usuń nieznany wskaźnik tylko PO rzuceniu go z powrotem do typu ORIGINAL.

2) Czy do instancji odwołuje się nieznany wskaźnik w pamięci związanej ze stosem lub ze stertą? Jeśli nieznany wskaźnik odwołuje się do instancji na stosie, to NIGDY NIE POWINIEN BYĆ USUWANY!

3) Czy jesteś w 100% pewien, że nieznany wskaźnik jest prawidłowym obszarem pamięci? Nie, to NIGDY nie powinno być USUWANE!

W sumie niewiele jest bezpośredniej pracy, którą można wykonać przy użyciu nieznanego typu wskaźnika (void *). Jednak pośrednio void * jest wielkim atutem dla programistów C ++, na którym mogą polegać, gdy wymagana jest niejednoznaczność danych.


0

Jeśli potrzebujesz tylko bufora, użyj malloc / free. Jeśli musisz użyć new / delete, rozważ trywialną klasę opakowującą:

template<int size_ > struct size_buffer { 
  char data_[ size_]; 
  operator void*() { return (void*)&data_; }
};

typedef sized_buffer<100> OpaqueBuffer; // logical description of your sized buffer

OpaqueBuffer* ptr = new OpaqueBuffer();

delete ptr;

0

W szczególnym przypadku char.

char jest typem wewnętrznym, który nie ma specjalnego destruktora. Zatem argumenty dotyczące przecieków są dyskusyjne.

sizeof (char) to zwykle jeden, więc nie ma również argumentu wyrównania. W przypadku rzadkich platform, na których sizeof (char) nie jest jeden, alokują pamięć wyrównaną wystarczająco dla ich char. Zatem argument wyrównania jest również dyskusyjny.

malloc / free byłoby w tym przypadku szybsze. Ale tracisz std :: bad_alloc i musisz sprawdzić wynik malloc. Wywołanie globalnych operatorów new i delete może być lepsze, ponieważ pomija pośrednika.


1
sizeof (char) to zwykle jeden ” sizeof (char) to ZAWSZE jeden
ciekawy,

Dopiero niedawno (2019) ludzie myśleli, że tak newnaprawdę jest zdefiniowane rzucanie. To nie jest prawda. Jest zależny od kompilatora i przełącznika kompilatora. Zobacz na przykład /GX[-] enable C++ EH (same as /EHsc)przełączniki MSVC2019 . Również w systemach wbudowanych wielu decyduje się nie płacić podatku od wydajności za wyjątki C ++. Więc zdanie zaczynające się od „Ale straciłeś std :: bad_alloc ...” jest wątpliwe.
BitTickler
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.