W porządku, myślę, że wszyscy zgadzamy się, że to, co dzieje się z następującym kodem, jest nieokreślone, w zależności od tego, co zostanie przekazane,
void deleteForMe(int* pointer)
{
delete[] pointer;
}
Wskaźnik może być najróżniejszymi rzeczami, więc wykonanie delete[]
na nim bezwarunkowego działania jest nieokreślone. Załóżmy jednak, że rzeczywiście przekazujemy wskaźnik tablicy,
int main()
{
int* arr = new int[5];
deleteForMe(arr);
return 0;
}
Moje pytanie brzmi: w tym przypadku, gdy wskaźnik jest tablicą, kto to wie? Chodzi mi o to, że z punktu widzenia języka / kompilatora nie ma pojęcia, czy arr
jest to wskaźnik tablicy czy wskaźnik do pojedynczego int. Heck, nawet nie wie, czy arr
został stworzony dynamicznie. Jeśli jednak wykonam następujące czynności,
int main()
{
int* num = new int(1);
deleteForMe(num);
return 0;
}
System operacyjny jest na tyle inteligentny, że usuwa tylko jedną int i nie wykonuje jakiegoś `` szaleństwa zabijania '', usuwając resztę pamięci poza tym punktem (w przeciwieństwie do tego strlen
i \0
niezakończonego ciągu - będzie kontynuował trafienia 0).
Więc do kogo należy zapamiętywanie tych rzeczy? Czy system operacyjny przechowuje w tle jakiś rodzaj rekordów? (Mam na myśli, zdaję sobie sprawę, że zacząłem ten post od stwierdzenia, że to, co się dzieje, jest nieokreślone, ale faktem jest, że scenariusz `` szaleństwa zabijania '' nie ma miejsca, więc w praktycznym świecie ktoś pamięta.)
Odpowiedzi:
Kompilator nie wie, że to tablica, ufa programiście. Usunięcie wskaźnika do pojedynczego elementu
int
zdelete []
spowodowałoby niezdefiniowane zachowanie. Twój drugimain()
przykład jest niebezpieczny, nawet jeśli nie ulega natychmiastowej awarii.Kompilator musi w jakiś sposób śledzić, ile obiektów trzeba usunąć. Może to zrobić przez nadmierną alokację wystarczającą do przechowywania rozmiaru tablicy. Aby uzyskać więcej informacji, zobacz C ++ Super FAQ .
źródło
Jedno pytanie, na które odpowiedzi udzielone do tej pory wydają się nie uwzględniać: jeśli biblioteki uruchomieniowe (a tak naprawdę nie system operacyjny) mogą śledzić liczbę elementów w tablicy, to dlaczego w ogóle potrzebujemy
delete[]
składni? Dlaczego nie można użyć jednegodelete
formularza do obsługi wszystkich operacji usunięcia?Odpowiedź na to pytanie sięga korzeni C ++ jako języka kompatybilnego z C (do czego już nie dąży). Filozofia Stroustrupa polegała na tym, że programista nie powinien płacić za żadne funkcje, których nie używa. Jeśli nie używają tablic, nie powinni być zmuszeni do ponoszenia kosztu tablic obiektów dla każdego przydzielonego fragmentu pamięci.
To znaczy, jeśli twój kod po prostu to robi
wtedy przestrzeń pamięci, dla której jest przydzielona,
foo
nie powinna obejmować żadnych dodatkowych kosztów, które byłyby potrzebne do obsługi tablicFoo
.Ponieważ tylko alokacje tablic są skonfigurowane tak, aby przenosić dodatkowe informacje o rozmiarze tablicy, należy następnie powiedzieć bibliotekom wykonawczym, aby szukały tych informacji podczas usuwania obiektów. Dlatego musimy użyć
zamiast po prostu
jeśli bar jest wskaźnikiem do tablicy.
Dla większości z nas (łącznie ze mną) ta kłopotliwa kwestia kilku dodatkowych bajtów pamięci wydaje się w dzisiejszych czasach osobliwa. Ale nadal istnieją sytuacje, w których oszczędność kilku bajtów (z bardzo dużej liczby bloków pamięci) może być ważna.
źródło
Tak, system operacyjny utrzymuje pewne rzeczy w „tle”. Na przykład, jeśli biegasz
system operacyjny może przydzielić 4 dodatkowe bajty, zapisać rozmiar alokacji w pierwszych 4 bajtach przydzielonej pamięci i zwrócić wskaźnik przesunięcia (tj. przydziela przestrzenie pamięci od 1000 do 1024, ale wskaźnik zwrócony wskazuje 1004, z lokalizacjami 1000- 1003 przechowujący wielkość alokacji). Następnie, gdy wywoływane jest usuwanie, może spojrzeć na 4 bajty przed przekazaniem wskaźnika, aby znaleźć rozmiar alokacji.
Jestem pewien, że istnieją inne sposoby śledzenia wielkości alokacji, ale to jedna z opcji.
źródło
for(int i = 0; i < *(arrayPointer - 1); i++){ }
Jest to bardzo podobne do tego pytania i zawiera wiele szczegółów, których szukasz.
Ale wystarczy powiedzieć, że śledzenie tego wszystkiego nie jest zadaniem systemu operacyjnego. W rzeczywistości to biblioteki uruchomieniowe lub bazowy menedżer pamięci będą śledzić rozmiar tablicy. Odbywa się to zwykle przez przydzielenie dodatkowej pamięci z góry i przechowywanie rozmiaru tablicy w tej lokalizacji (większość używa węzła głównego).
Można to zobaczyć w niektórych implementacjach, wykonując następujący kod
źródło
size_t size = *(reinterpret_cast<size_t *>(pArray) - 1)
zamiast tegodelete
lubdelete[]
prawdopodobnie oba zwolnią przydzieloną pamięć (wskazano na pamięć), ale duża różnica polegadelete
na tym, że tablica nie wywoła destruktora każdego elementu tablicy.Zresztą miksowanie
new/new[]
idelete/delete[]
to chyba UB.źródło
Nie wie, że to tablica, dlatego musisz podać
delete[]
zamiast zwykłego staregodelete
.źródło
Miałem podobne pytanie. W C alokujesz pamięć za pomocą malloc () (lub innej podobnej funkcji) i usuwasz ją za pomocą free (). Jest tylko jedna funkcja malloc (), która po prostu przydziela określoną liczbę bajtów. Jest tylko jedna funkcja free (), która jako parametr przyjmuje po prostu wskaźnik.
Dlaczego więc w C możesz po prostu przekazać wskaźnik do zwolnienia, ale w C ++ musisz powiedzieć, czy jest to tablica, czy pojedyncza zmienna?
Dowiedziałem się, że odpowiedź ma związek z niszczycielami klas.
Jeśli przydzielisz wystąpienie klasy MyClass ...
I usuń go za pomocą delete, możesz uzyskać destruktor tylko dla pierwszego wystąpienia MyClass o nazwie. Jeśli użyjesz delete [], możesz być pewien, że destruktor zostanie wywołany dla wszystkich instancji w tablicy.
TO jest ważna różnica. Jeśli po prostu pracujesz ze standardowymi typami (np. Int), tak naprawdę nie zobaczysz tego problemu. Ponadto należy pamiętać, że zachowanie przy używaniu usuwania na nowym [] i usuwaniu [] na nowym jest niezdefiniowane - może nie działać w ten sam sposób na każdym kompilatorze / systemie.
źródło
To środowisko wykonawcze jest odpowiedzialne za alokację pamięci, w ten sam sposób, w jaki można usunąć tablicę utworzoną za pomocą malloc w standardowym C, używając free. Myślę, że każdy kompilator implementuje to inaczej. Jednym z powszechnych sposobów jest przydzielenie dodatkowej komórki na rozmiar tablicy.
Jednak środowisko wykonawcze nie jest wystarczająco inteligentne, aby wykryć, czy jest to tablica lub wskaźnik, musisz o tym poinformować, a jeśli się pomylisz, albo nie usuniesz poprawnie (np. Ptr zamiast array) lub w końcu przyjmujesz niepowiązaną wartość rozmiaru i powodujesz znaczne szkody.
źródło
JEDNYM Z PODEJŚĆ DLA kompilatorów jest przydzielenie trochę więcej pamięci i przechowywanie liczby elementów w elemencie head.
Przykład, jak można to zrobić: Tutaj
kompilator przydzieli sizeof (int) * 5 bajtów.
Przechowuje
4
w pierwszychsizeof(int)
bajtachi nastaw
i
Oznacza
i
to tablicę 4 elementów, a nie 5.I
będą przetwarzane w następujący sposób
źródło
Semantycznie, obie wersje operatora usuwania w C ++ mogą „zjadać” dowolny wskaźnik; jednakże, jeśli podamy wskaźnik do pojedynczego obiektu, to otrzymamy
delete[]
UB, co oznacza, że może się zdarzyć wszystko, w tym awaria systemu lub nic.C ++ wymaga od programisty wybrania odpowiedniej wersji operatora delete w zależności od przedmiotu cofnięcia alokacji: tablicy lub pojedynczego obiektu.
Gdyby kompilator mógł automatycznie określić, czy wskaźnik przekazany do operatora usuwania był tablicą wskaźników, w C ++ byłby tylko jeden operator usuwania, który wystarczyłby w obu przypadkach.
źródło
Zgadzam się, że kompilator nie wie, czy jest to tablica, czy nie. To zależy od programisty.
Kompilator czasami śledzi, ile obiektów należy usunąć, przydzielając ich zbyt dużo, aby przechowywać rozmiar tablicy, ale nie zawsze jest to konieczne.
Aby uzyskać pełną specyfikację dotyczącą przydzielania dodatkowej pamięci, zapoznaj się z C ++ ABI (jak implementowane są kompilatory): Itanium C ++ ABI: Array Operator new Cookies
źródło
Nie możesz użyć delete dla tablicy i nie możesz użyć delete [] dla nie-macierzy.
źródło
„Nieokreślone zachowanie” oznacza po prostu, że specyfikacja języka nie daje żadnych gwarancji co do tego, co się stanie. Nie oznacza to oczywiście, że stanie się coś złego.
Zwykle są tutaj dwie warstwy. Podstawowy menedżer pamięci i implementacja C ++.
Ogólnie rzecz biorąc, menedżer pamięci zapamięta (między innymi) rozmiar przydzielonego bloku pamięci. To może być większe niż blok, o który prosiła implementacja C ++. Zazwyczaj menedżer pamięci przechowuje swoje metadane przed przydzielonym blokiem pamięci.
Implementacja C ++ zazwyczaj zapamiętuje rozmiar tablicy tylko wtedy, gdy musi to zrobić do swoich własnych celów, zazwyczaj dlatego, że typ ma nieniszczący destruktor.
Tak więc dla typów z trywialnym destruktorem implementacja „delete” i „delete []” jest zazwyczaj taka sama. Implementacja C ++ po prostu przekazuje wskaźnik do bazowego menedżera pamięci. Coś jak
Z drugiej strony dla typów z nietrywialnym destruktorem wartości „delete” i „delete []” będą prawdopodobnie różne. „usuń” byłoby czymś w rodzaju (gdzie T jest typem wskazywanym przez wskaźnik)
Podczas gdy „delete []” byłoby czymś w rodzaju.
źródło
iteruj przez tablicę obiektów i wywołuj destruktor dla każdego z nich. Stworzyłem ten prosty kod, który przeciąża nowe wyrażenia [] i delete [] oraz zapewnia funkcję szablonu do zwalniania pamięci i wywoływania destruktora dla każdego obiektu w razie potrzeby:
źródło
po prostu zdefiniuj destruktor wewnątrz klasy i wykonaj swój kod za pomocą obu składni
zgodnie z wyjściem u można znaleźć rozwiązania
źródło
Odpowiedź:
int * pArray = new int [5];
int size = * (pArray-1);
Zamieszczone powyżej jest nieprawidłowe i daje nieprawidłową wartość. Wartość „-1” liczy elementy W 64-bitowym systemie operacyjnym Windows prawidłowy rozmiar bufora znajduje się w adresie Ptr - 4 bajty
źródło