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 ptr
zostać rzucony char*
przed usunięciem?
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 ptr
zostać rzucony char*
przed usunięciem?
Odpowiedzi:
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.
sizeof(T*) == sizeof(U*)
dla wszystkich T,U
sugeruje, ż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ć.
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
.
NULL
robiąc jakąkolwiek różnicę w zarządzaniu pamięcią aplikacji?
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.
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 delete
do 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). void
sam 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).
delete
moż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 ...
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.
Jeśli naprawdę musi to zrobić, dlaczego nie wyciąć w środku człowieka (The new
i delete
operatorów) i wywołać światowy operator new
i operator delete
bezpośrednio? (Oczywiście, jeśli próbujesz oprzyrządować operatory new
and delete
, w rzeczywistości powinieneś ponownie zaimplementować operator new
i 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 new
rzuca std::bad_alloc
w przypadku niepowodzenia (lub wywołuje, new_handler
jeśli jest zarejestrowany).
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.
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);
}
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ł delete
bezpiecznie 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 ++.
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
/, free
aby 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 delete
go.
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.
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;
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.
new
naprawdę 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.