Skąd delete [] wie, że to tablica?

136

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 arrjest to wskaźnik tablicy czy wskaźnik do pojedynczego int. Heck, nawet nie wie, czy arrzostał 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 strleni \0niezakoń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.)

GRB
źródło
6
wie z nawiasów kwadratowych po usunięciu
JoelFan
„wskaźnik jest tablicą”. Nie, wskaźniki nigdy nie są tablicami. Często wskazują na pierwszy element tablicy, ale to już inna sprawa.
Aaron McDaid

Odpowiedzi:

99

Kompilator nie wie, że to tablica, ufa programiście. Usunięcie wskaźnika do pojedynczego elementu intz delete []spowodowałoby niezdefiniowane zachowanie. Twój drugi main()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 .

Fred Larson
źródło
14
W rzeczywistości użycie delete [] do usunięcia czegoś utworzonego za pomocą nowego jest możliwe do wykorzystania. taossa.com/index.php/2007/01/03/…
Rodrigo
23
@Rodrigo Link w Twoim komentarzu jest uszkodzony, ale na szczęście maszyna Wayback ma jego kopię pod adresem replay.web.archive.org/20080703153358/http://taossa.com/…
David Gardner
103

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ć jednego deleteformularza 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

Foo* foo = new Foo;

wtedy przestrzeń pamięci, dla której jest przydzielona, foonie powinna obejmować żadnych dodatkowych kosztów, które byłyby potrzebne do obsługi tablic Foo.

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ć

delete[] bar;

zamiast po prostu

delete bar;

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.

Dan Breslau
źródło
20
„zamieszanie związane z kilkoma dodatkowymi bajtami pamięci wydaje się ostatnio dziwne”. Na szczęście dla takich ludzi gołe tablice również zaczynają wyglądać osobliwie, więc mogą po prostu użyć wektora lub boost :: array i zapomnieć o delete [] na zawsze :-)
Steve Jessop
28

Tak, system operacyjny utrzymuje pewne rzeczy w „tle”. Na przykład, jeśli biegasz

int* num = new int[5];

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.

bsdfish
źródło
26
+1 - ogólnie ważny punkt, z wyjątkiem tego, że zwykle środowisko wykonawcze języka jest odpowiedzialne za przechowywanie tych metadanych, a nie system operacyjny.
ostry ząb
Co dzieje się z rozmiarem tablicy lub rozmiarem obiektu, który ma zdefiniowaną tablicę? Czy pokazuje dodatkowe 4 bajty, gdy robisz sizeof na tym obiekcie?
Shree
3
Nie, sizeof pokazuje tylko rozmiar tablicy. Jeśli środowisko wykonawcze zdecyduje się zaimplementować to w / w opisaną przeze mnie metodę, jest to ściśle określony szczegół implementacji iz punktu widzenia użytkownika, powinien zostać zamaskowany. Pamięć przed wskaźnikiem nie „należy” do użytkownika i nie jest zliczana.
bsdfish
2
Co ważniejsze, sizeof w żadnym przypadku nie zwróci prawdziwego rozmiaru tablicy alokowanej dynamicznie. Może zwracać tylko rozmiary znane w czasie kompilacji.
bdonlan,
Czy można użyć tych metadanych w pętli for, aby dokładnie zapętlić tablicę? np. for(int i = 0; i < *(arrayPointer - 1); i++){ }
Sam,
13

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

int* pArray = new int[5];
int size = *(pArray-1);
JaredPar
źródło
Czy to zadziała? W systemie Windows i Linux to nie działało.
kumpel
1
spróbuj size_t size = *(reinterpret_cast<size_t *>(pArray) - 1)zamiast tego
9

deletelub delete[]prawdopodobnie oba zwolnią przydzieloną pamięć (wskazano na pamięć), ale duża różnica polega deletena tym, że tablica nie wywoła destruktora każdego elementu tablicy.

Zresztą miksowanie new/new[]i delete/delete[]to chyba UB.

Benoît
źródło
1
Jasna, krótka i najbardziej użyteczna odpowiedź!
GntS
6

Nie wie, że to tablica, dlatego musisz podać delete[]zamiast zwykłego starego delete.

opuchnięty
źródło
5

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 ...

classes = new MyClass[3];

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.

ProdigySim
źródło
3

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.

Uri
źródło
3

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

int* i = new int[4];

kompilator przydzieli sizeof (int) * 5 bajtów.

int *temp = malloc(sizeof(int)*5)

Przechowuje 4w pierwszych sizeof(int)bajtach

*temp = 4;

i nastaw i

i = temp + 1;

Oznacza ito tablicę 4 elementów, a nie 5.

I

delete[] i;

będą przetwarzane w następujący sposób

int *temp = i - 1;
int numbers_of_element = *temp; // = 4
... call destructor for numbers_of_element elements if needed
... that are stored in temp + 1, temp + 2, ... temp + 4
free (temp)
Avt
źródło
1

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.

mloskot
źródło
1

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

shibo
źródło
Żałuję tylko, każdy kompilator zaobserwować pewne udokumentowane ABI C ++. +1 dla linku, który odwiedziłem wcześniej. Dzięki.
Don Wakefield
0

Nie możesz użyć delete dla tablicy i nie możesz użyć delete [] dla nie-macierzy.

Don Wakefield
źródło
8
Myślę, że nie powinieneś , ponieważ przeciętny kompilator nie wykryje nadużycia.
Don Wakefield
0

„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.

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.)

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

free(p)

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)

p->~T();
free(p);

Podczas gdy „delete []” byłoby czymś w rodzaju.

size_t * pcount = ((size_t *)p)-1;
size_t count = *count;
for (size_t i=0;i<count;i++) {
  p[i].~T();
}
char * pmemblock = ((char *)p) - max(sizeof(size_t),alignof(T));
free(pmemblock);
plugwash
źródło
-1

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:

// overloaded new expression 
void* operator new[]( size_t size )
{
    // allocate 4 bytes more see comment below 
    int* ptr = (int*)malloc( size + 4 );

    // set value stored at address to 0 
    // and shift pointer by 4 bytes to avoid situation that
    // might arise where two memory blocks 
    // are adjacent and non-zero
    *ptr = 0;
    ++ptr; 

    return ptr;
}
//////////////////////////////////////////

// overloaded delete expression 
void static operator delete[]( void* ptr )
{
    // decrement value of pointer to get the
    // "Real Pointer Value"
    int* realPtr = (int*)ptr;
    --realPtr;

    free( realPtr );
}
//////////////////////////////////////////

// Template used to call destructor if needed 
// and call appropriate delete 
template<class T>
void Deallocate( T* ptr )
{
    int* instanceCount = (int*)ptr;
    --instanceCount;

    if(*instanceCount > 0) // if larger than 0 array is being deleted
    {
        // call destructor for each object
        for(int i = 0; i < *instanceCount; i++)
        {
            ptr[i].~T();
        }
        // call delete passing instance count witch points
        // to begin of array memory 
        ::operator delete[]( instanceCount );
    }
    else
    {
        // single instance deleted call destructor
        // and delete passing ptr
        ptr->~T();
        ::operator delete[]( ptr );
    }
}

// Replace calls to new and delete
#define MyNew ::new
#define MyDelete(ptr) Deallocate(ptr)

// structure with constructor/ destructor
struct StructureOne
{
    StructureOne():
    someInt(0)
    {}
    ~StructureOne() 
    {
        someInt = 0;
    }

    int someInt;
};
//////////////////////////////

// structure without constructor/ destructor
struct StructureTwo
{
    int someInt;
};
//////////////////////////////


void main(void)
{
    const unsigned int numElements = 30;

    StructureOne* structOne = nullptr;
    StructureTwo* structTwo = nullptr;
    int* basicType = nullptr;
    size_t ArraySize = 0;

/**********************************************************************/
    // basic type array 

    // place break point here and in new expression
    // check size and compare it with size passed 
    // in to new expression size will be the same
    ArraySize = sizeof( int ) * numElements;

    // this will be treated as size rather than object array as there is no 
    // constructor and destructor. value assigned to basicType pointer
    // will be the same as value of "++ptr" in new expression
    basicType = MyNew int[numElements];

    // Place break point in template function to see the behavior
    // destructors will not be called and it will be treated as 
    // single instance of size equal to "sizeof( int ) * numElements"
    MyDelete( basicType );

/**********************************************************************/
    // structure without constructor and destructor array 

    // behavior will be the same as with basic type 

    // place break point here and in new expression
    // check size and compare it with size passed 
    // in to new expression size will be the same
    ArraySize = sizeof( StructureTwo ) * numElements;

    // this will be treated as size rather than object array as there is no 
    // constructor and destructor value assigned to structTwo pointer
    // will be the same as value of "++ptr" in new expression
    structTwo = MyNew StructureTwo[numElements]; 

    // Place break point in template function to see the behavior
    // destructors will not be called and it will be treated as 
    // single instance of size equal to "sizeof( StructureTwo ) * numElements"
    MyDelete( structTwo );

/**********************************************************************/
    // structure with constructor and destructor array 

    // place break point check size and compare it with size passed in
    // new expression size in expression will be larger by 4 bytes
    ArraySize = sizeof( StructureOne ) * numElements;

    // value assigned to "structOne pointer" will be different 
    // of "++ptr" in new expression  "shifted by another 4 bytes"
    structOne = MyNew StructureOne[numElements];

    // Place break point in template function to see the behavior
    // destructors will be called for each array object 
    MyDelete( structOne );
}
///////////////////////////////////////////
Rafał Rebisz
źródło
-2

po prostu zdefiniuj destruktor wewnątrz klasy i wykonaj swój kod za pomocą obu składni

delete pointer

delete [] pointer

zgodnie z wyjściem u można znaleźć rozwiązania

bubu
źródło
użyj delete [] podczas tworzenia nowego typu tablicy. na przykład int * a = new int; int * b = new int [5]; usunąć; usuń [] b;
Lineesh K Mohan
-3

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

Evgeni Raikhel
źródło