¿Cómo eliminar [] sabe que es una matriz?

136

Muy bien, creo que todos estamos de acuerdo en que lo que sucede con el siguiente código no está definido, dependiendo de lo que se pase,

void deleteForMe(int* pointer)
{
     delete[] pointer;
}

El puntero podría ser todo tipo de cosas diferentes, por lo que realizar un incondicional delete[]en él no está definido. Sin embargo, supongamos que de hecho estamos pasando un puntero de matriz,

int main()
{
     int* arr = new int[5];
     deleteForMe(arr);
     return 0;
}

Mi pregunta es, en este caso donde el puntero es una matriz, ¿quién es el que sabe esto? Quiero decir, desde el punto de vista del lenguaje / compilador, no tiene idea de si arres un puntero de matriz frente a un puntero a un solo int. Diablos, ni siquiera sabe si arrfue creado dinámicamente. Sin embargo, si hago lo siguiente en su lugar,

int main()
{
     int* num = new int(1);
     deleteForMe(num);
     return 0;
}

El sistema operativo es lo suficientemente inteligente como para eliminar solo un int y no ir a algún tipo de 'juerga asesina' eliminando el resto de la memoria más allá de ese punto (contraste eso con strleny una \0cadena no terminada, continuará hasta que hits 0).

Entonces, ¿de quién es el trabajo de recordar estas cosas? ¿El sistema operativo mantiene algún tipo de registro en segundo plano? (Quiero decir, me doy cuenta de que comencé esta publicación diciendo que lo que sucede no está definido, pero el hecho es que el escenario de la 'ola de asesinatos' no ocurre, por lo que en el mundo práctico alguien está recordando).

GRB
fuente
66
se sabe entre corchetes después de la eliminación
JoelFan
"el puntero es una matriz". No, los punteros nunca son matrices. A menudo apuntan al primer elemento de la matriz, pero eso es algo diferente.
Aaron McDaid el

Respuestas:

99

El compilador no sabe que es una matriz, confía en el programador. Eliminar un puntero a un solo intcon delete []resultaría en un comportamiento indefinido. Su segundo main()ejemplo es inseguro, incluso si no se bloquea de inmediato.

El compilador tiene que realizar un seguimiento de cuántos objetos deben eliminarse de alguna manera. Puede hacer esto al sobreasignar lo suficiente como para almacenar el tamaño de la matriz. Para obtener más detalles, consulte las preguntas frecuentes de C ++ Super .

Fred Larson
fuente
14
En realidad, usar delete [] para eliminar algo creado con new es explotable. taossa.com/index.php/2007/01/03/…
Rodrigo
23
@Rodrigo El enlace en su comentario está roto, pero afortunadamente la máquina del camino tiene una copia en replay.web.archive.org/20080703153358/http://taossa.com/…
David Gardner
103

Una pregunta que las respuestas dadas hasta ahora no parecen abordar: si las bibliotecas de tiempo de ejecución (no el sistema operativo, realmente) pueden realizar un seguimiento de la cantidad de cosas en la matriz, entonces ¿por qué necesitamos la delete[]sintaxis? ¿Por qué no se deletepuede usar un solo formulario para manejar todas las eliminaciones?

La respuesta a esto se remonta a las raíces de C ++ como un lenguaje compatible con C (que ya no se esfuerza realmente por ser). La filosofía de Stroustrup era que el programador no debería tener que pagar por las funciones que no están utilizando. Si no están usando matrices, entonces no deberían tener que cargar con el costo de las matrices de objetos por cada porción de memoria asignada.

Es decir, si su código simplemente lo hace

Foo* foo = new Foo;

entonces el espacio de memoria asignado foono debería incluir ninguna sobrecarga adicional que sería necesaria para admitir matrices Foo.

Dado que solo las asignaciones de matriz se configuran para transportar la información adicional del tamaño de la matriz, debe indicar a las bibliotecas de tiempo de ejecución que busquen esa información cuando elimine los objetos. Es por eso que necesitamos usar

delete[] bar;

en lugar de solo

delete bar;

si bar es un puntero a una matriz.

Para la mayoría de nosotros (incluido yo mismo), esa inquietud acerca de unos pocos bytes adicionales de memoria parece pintoresca en estos días. Pero todavía hay algunas situaciones en las que guardar algunos bytes (de lo que podría ser un número muy alto de bloques de memoria) puede ser importante.

Dan Breslau
fuente
20
"inquietud acerca de unos pocos bytes adicionales de memoria parece pintoresco en estos días". Afortunadamente, para esas personas, las matrices desnudas también comienzan a parecer pintorescas, por lo que pueden usar un vector o boost :: array y olvidarse de eliminar [] para siempre :-)
Steve Jessop
28

Sí, el sistema operativo mantiene algunas cosas en segundo plano. Por ejemplo, si corres

int* num = new int[5];

el sistema operativo puede asignar 4 bytes adicionales, almacenar el tamaño de la asignación en los primeros 4 bytes de la memoria asignada y devolver un puntero de compensación (es decir, asigna espacios de memoria 1000 a 1024 pero el puntero devolvió puntos a 1004, con ubicaciones 1000- 1003 almacenando el tamaño de la asignación). Luego, cuando se llama a delete, puede mirar 4 bytes antes de pasarle el puntero para encontrar el tamaño de la asignación.

Estoy seguro de que hay otras formas de rastrear el tamaño de una asignación, pero esa es una opción.

bsdfish
fuente
26
+1: punto válido en general, excepto que generalmente el tiempo de ejecución del idioma es responsable de almacenar estos metadatos, no el sistema operativo.
Sharptooth
¿Qué sucede con el tamaño de la matriz o el tamaño de un objeto que tiene la matriz definida? ¿Muestra los 4 bytes adicionales cuando haces un sizeof en ese objeto?
Shree
3
No, sizeof muestra solo el tamaño de la matriz. Si el tiempo de ejecución elige implementarlo con el método que describí, eso es estrictamente un detalle de implementación y, desde la perspectiva del usuario, debe enmascararse. La memoria antes del puntero no 'pertenece' al usuario y no se cuenta.
bsdfish
2
Más importante aún, sizeof no devolverá el tamaño verdadero de una matriz asignada dinámicamente en ningún caso. Solo puede devolver tamaños conocidos en tiempo de compilación.
bdonlan
¿Es posible usar estos metadatos en un bucle for para recorrer con precisión la matriz? por ejemplo for(int i = 0; i < *(arrayPointer - 1); i++){ }
Sam
13

Esto es muy similar a esta pregunta y tiene muchos de los detalles que está buscando.

Pero basta con decir que no es el trabajo del sistema operativo rastrear nada de esto. En realidad, las bibliotecas de tiempo de ejecución o el administrador de memoria subyacente rastrearán el tamaño de la matriz. Esto generalmente se hace asignando memoria adicional por adelantado y almacenando el tamaño de la matriz en esa ubicación (la mayoría usa un nodo principal).

Esto se puede ver en algunas implementaciones ejecutando el siguiente código

int* pArray = new int[5];
int size = *(pArray-1);
JaredPar
fuente
¿esto funcionara? En Windows y Linux no conseguimos que esto funcionara.
amigo
1
intente en su size_t size = *(reinterpret_cast<size_t *>(pArray) - 1)lugar
9

deleteo delete[]probablemente ambos liberarían la memoria asignada (memoria puntiaguda), pero la gran diferencia es que deleteen una matriz no se llamará al destructor de cada elemento de la matriz.

De todos modos, mezcla new/new[]y delete/delete[]probablemente sea UB.

Benoît
fuente
1
Claro, corto y la respuesta más útil!
GntS
6

No sabe que es una matriz, es por eso que debe suministrar en delete[]lugar de la antigua delete.

eduffy
fuente
5

Tenía una pregunta similar a esta. En C, asigna memoria con malloc () (u otra función similar) y la elimina con free (). Solo hay un malloc (), que simplemente asigna un cierto número de bytes. Solo hay un free (), que simplemente toma un puntero como parámetro.

Entonces, ¿por qué en C puedes simplemente pasar el puntero para liberarlo, pero en C ++ debes decir si se trata de una matriz o una sola variable?

La respuesta, he aprendido, tiene que ver con destructores de clases.

Si asigna una instancia de una clase MyClass ...

classes = new MyClass[3];

Y elimínelo con eliminar, solo puede obtener el destructor para la primera instancia de MyClass llamada. Si usa delete [], puede estar seguro de que se llamará al destructor para todas las instancias de la matriz.

Esta es la diferencia importante. Si simplemente está trabajando con tipos estándar (por ejemplo, int), realmente no verá este problema. Además, debe recordar que el comportamiento para usar delete en new [] y delete [] en new no está definido: puede que no funcione de la misma manera en cada compilador / sistema.

ProdigySim
fuente
3

El tiempo de ejecución es el responsable de la asignación de memoria, de la misma manera que puede eliminar una matriz creada con malloc en el estándar C usando free. Creo que cada compilador lo implementa de manera diferente. Una forma común es asignar una celda adicional para el tamaño de la matriz.

Sin embargo, el tiempo de ejecución no es lo suficientemente inteligente como para detectar si es o no una matriz o un puntero, debe informarlo y, si está equivocado, no lo elimina correctamente (por ejemplo, ptr en lugar de matriz), o terminas tomando un valor no relacionado para el tamaño y causando un daño significativo.

Uri
fuente
3

UNO DE LOS enfoques para los compiladores es asignar un poco más de memoria y almacenar el recuento de elementos en el elemento principal.

Ejemplo de cómo se podría hacer: Aquí

int* i = new int[4];

el compilador asignará sizeof (int) * 5 bytes.

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

Se almacenará 4en los primeros sizeof(int)bytes

*temp = 4;

y establecer i

i = temp + 1;

Entonces i apunta a una matriz de 4 elementos, no 5.

Y

delete[] i;

será procesado de la siguiente manera

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
fuente
1

Semánticamente, ambas versiones del operador delete en C ++ pueden "comer" cualquier puntero; sin embargo, si se da un puntero a un solo objeto adelete[] , entonces UB resultará, lo que significa que puede suceder cualquier cosa, incluido un bloqueo del sistema o nada en absoluto.

C ++ requiere que el programador elija la versión adecuada del operador de eliminación según el tema de la desasignación: matriz u objeto único.

Si el compilador pudiera determinar automáticamente si un puntero pasado al operador de eliminación era una matriz de punteros, entonces solo habría un operador de eliminación en C ++, lo que sería suficiente para ambos casos.

mloskot
fuente
1

Acuerde que el compilador no sabe si es una matriz o no. Depende del programador.

El compilador a veces realiza un seguimiento de cuántos objetos deben eliminarse al sobreasignar lo suficiente como para almacenar el tamaño de la matriz, pero no siempre es necesario.

Para obtener una especificación completa cuando se asigna almacenamiento adicional, consulte C ++ ABI (cómo se implementan los compiladores): Itanium C ++ ABI: nuevas cookies del operador de matriz

shibo
fuente
Solo desearía que cada compilador observara algún ABI documentado para C ++. +1 para el enlace, que he visitado antes. Gracias.
Don Wakefield
0

No puede usar eliminar para una matriz, y no puede usar eliminar [] para una no matriz.

Don Wakefield
fuente
8
Creo que quiere decir que no debería , ya que su compilador promedio no detectará el abuso.
Don Wakefield
0

"comportamiento indefinido" simplemente significa que la especificación del lenguaje no garantiza lo que sucederá. No significa necesariamente que algo malo sucederá.

Entonces, ¿de quién es el trabajo de recordar estas cosas? ¿El sistema operativo mantiene algún tipo de registro en segundo plano? (Quiero decir, me doy cuenta de que comencé esta publicación diciendo que lo que sucede no está definido, pero el hecho es que el escenario de la 'ola de asesinatos' no ocurre, por lo que en el mundo práctico alguien está recordando).

Normalmente hay dos capas aquí. El administrador de memoria subyacente y la implementación de C ++.

En general, el administrador de memoria recordará (entre otras cosas) el tamaño del bloque de memoria asignado. Esto puede ser mayor que el bloque que solicitó la implementación de C ++. Por lo general, el administrador de memoria almacenará sus metadatos antes del bloque de memoria asignado.

La implementación de C ++ generalmente solo recordará el tamaño de la matriz si necesita hacerlo para sus propios fines, generalmente porque el tipo tiene un destructor no trival.

Entonces, para los tipos con un destructor trivial, la implementación de "eliminar" y "eliminar []" suele ser la misma. La implementación de C ++ simplemente pasa el puntero al administrador de memoria subyacente. Algo como

free(p)

Por otro lado, para los tipos con un destructor no trivial, "delete" y "delete []" probablemente sean diferentes. "eliminar" sería algo así (donde T es el tipo al que apunta el puntero)

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

Mientras que "eliminar []" sería algo así.

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);
lavado
fuente
-1

iterar a través de una matriz de objetos y llamar al destructor para cada uno de ellos. He creado este código simple que sobrecarga nuevas expresiones [] y eliminar [] y proporciona una función de plantilla para desasignar memoria y llamar al destructor para cada objeto si es necesario:

// 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 );
}
///////////////////////////////////////////
Rafal Rebisz
fuente
-2

solo defina un destructor dentro de una clase y ejecute su código con ambas sintaxis

delete pointer

delete [] pointer

de acuerdo con la salida puedes encontrar las soluciones

bubu
fuente
use eliminar [] cuando cree un nuevo tipo de matriz. por ejemplo int * a = new int; int * b = nuevo int [5]; eliminar a; eliminar [] b;
Lineesh K Mohan
-3

La respuesta:

int * pArray = new int [5];

int tamaño = * (pArray-1);

Publicado anteriormente no es correcto y produce un valor no válido. El "-1" cuenta elementos En el sistema operativo Windows de 64 bits, el tamaño correcto del búfer reside en la dirección Ptr - 4 bytes

Evgeni Raikhel
fuente