Como delete [] sabe que é uma matriz?

136

Tudo bem, acho que todos concordamos que o que acontece com o código a seguir é indefinido, dependendo do que foi passado,

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

O ponteiro pode ter todos os tipos de coisas diferentes e, portanto, executar um incondicional delete[]nele é indefinido. No entanto, vamos supor que estamos realmente passando um ponteiro de matriz,

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

Minha pergunta é, neste caso, onde o ponteiro é uma matriz, quem é que sabe disso? Quero dizer, do ponto de vista da linguagem / compilador, não tem idéia se é ou não arrum ponteiro de matriz versus um ponteiro para um único int. Caramba, ele nem sabe se arrfoi criado dinamicamente. No entanto, se eu fizer o seguinte,

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

O sistema operacional é inteligente o suficiente para excluir apenas um int e não continuar com algum tipo de 'matança', excluindo o restante da memória além desse ponto (em contraste com isso com strlenuma \0string não terminada - ele continuará até atinge 0).

Então, de quem é o trabalho para lembrar dessas coisas? O sistema operacional mantém algum tipo de registro em segundo plano? (Quero dizer, eu percebi que comecei este post dizendo que o que acontece é indefinido, mas o fato é que o cenário de 'matança' não acontece, portanto, no mundo prático, alguém está se lembrando.)

GRB
fonte
6
ele sabe dos colchetes após a exclusão
JoelFan
"o ponteiro é uma matriz". Não, ponteiros nunca são matrizes. Eles costumam apontar para o primeiro elemento da matriz, mas isso é uma coisa diferente.
Aaron McDaid

Respostas:

99

O compilador não sabe que é uma matriz, está confiando no programador. Excluir um ponteiro para um intcom delete []resultaria em um comportamento indefinido. Seu segundo main()exemplo é inseguro, mesmo se não travar imediatamente.

O compilador precisa acompanhar quantos objetos precisam ser excluídos de alguma forma. Isso pode ser superalocado o suficiente para armazenar o tamanho da matriz. Para mais detalhes, consulte as Super Perguntas frequentes do C ++ .

Fred Larson
fonte
14
Na verdade, o uso de delete [] para excluir algo criado com o novo é explorável. taossa.com/index.php/2007/01/03/…
Rodrigo #
23
@Rodrigo O link no seu comentário está quebrado, mas felizmente a máquina wayback tem uma cópia dele em replay.web.archive.org/20080703153358/http://taossa.com/…
David Gardner
103

Uma pergunta que as respostas dadas até agora não parecem abordar: se as bibliotecas de tempo de execução (não o sistema operacional, na verdade) podem acompanhar o número de coisas na matriz, por que precisamos da delete[]sintaxe? Por que não é deletepossível usar um único formulário para lidar com todas as exclusões?

A resposta para isso remonta às raízes do C ++ como uma linguagem compatível com C (que já não se esforça mais para ser). A filosofia do Stroustrup era que o programador não deveria pagar por nenhum recurso que não estivesse usando. Se eles não estiverem usando matrizes, não deverão suportar o custo de matrizes de objetos para cada pedaço de memória alocado.

Ou seja, se o seu código simplesmente

Foo* foo = new Foo;

o espaço de memória alocado foonão deve incluir nenhuma sobrecarga extra necessária para suportar matrizes de Foo.

Como apenas as alocações de matriz são configuradas para transportar informações adicionais sobre o tamanho da matriz, você precisa solicitar às bibliotecas de tempo de execução que procurem essas informações ao excluir os objetos. É por isso que precisamos usar

delete[] bar;

em vez de apenas

delete bar;

se bar é um ponteiro para uma matriz.

Para a maioria de nós (inclusive eu), essa confusão sobre alguns bytes extras de memória parece estranha hoje em dia. Mas ainda existem situações em que salvar alguns bytes (do que poderia ser um número muito alto de blocos de memória) pode ser importante.

Dan Breslau
fonte
20
"confusão sobre alguns bytes extras de memória parece estranha hoje em dia". Felizmente, a tais pessoas matrizes descalços também estão começando a pitoresca olhar, para que eles possam usar apenas um vector ou boost :: array, e esquecer de apagar [] para sempre :-)
Steve Jessop
28

Sim, o sistema operacional mantém algumas coisas em segundo plano. Por exemplo, se você executar

int* num = new int[5];

o sistema operacional pode alocar 4 bytes extras, armazenar o tamanho da alocação nos primeiros 4 bytes da memória alocada e retornar um ponteiro de deslocamento (ou seja, aloca espaços de memória de 1000 a 1024, mas o ponteiro retorna pontos para 1004, com locais de 1000 a 1003 armazenando o tamanho da alocação). Em seguida, quando a exclusão é chamada, ele pode observar 4 bytes antes que o ponteiro passe para encontrar o tamanho da alocação.

Estou certo de que existem outras maneiras de rastrear o tamanho de uma alocação, mas essa é uma opção.

bsdfish
fonte
26
+1 - ponto válido em geral, exceto que geralmente o tempo de execução do idioma é responsável por armazenar esses metadados, não o SO.
Sharptooth
O que acontece com o tamanho da matriz ou o tamanho de um objeto que tem a matriz definida? Ele mostra os 4 bytes adicionais quando você faz um sizeof nesse objeto?
Shree
3
Não, sizeof mostra apenas o tamanho da matriz. Se o tempo de execução optar por implementá-lo com o método que descrevi, isso é estritamente um detalhe de implementação e, da perspectiva do usuário, deve ser mascarado. A memória antes do ponteiro não 'pertence' ao usuário e não é contada.
bsdfish
2
Mais importante, sizeof não retornará o tamanho real de uma matriz alocada dinamicamente em nenhum caso. Só pode retornar tamanhos conhecidos em tempo de compilação.
22411 bdonlan
É possível usar esses metadados em um loop for para fazer um loop preciso sobre a matriz? por exemplo for(int i = 0; i < *(arrayPointer - 1); i++){ }
Sam
13

Isso é muito semelhante a esta pergunta e tem muitos dos detalhes que você está procurando.

Mas basta dizer que não é tarefa do sistema operacional rastrear nada disso. Na verdade, são as bibliotecas de tempo de execução ou o gerenciador de memória subjacente que rastreiam o tamanho da matriz. Isso geralmente é feito alocando memória extra antecipadamente e armazenando o tamanho da matriz nesse local (a maioria usa um nó principal).

Isso é visível em algumas implementações executando o seguinte código

int* pArray = new int[5];
int size = *(pArray-1);
JaredPar
fonte
Isso vai funcionar? No Windows e Linux, não conseguimos isso funcionando.
buddy
1
tente em size_t size = *(reinterpret_cast<size_t *>(pArray) - 1)vez disso
9

deleteou delete[]provavelmente liberaria a memória alocada (memória apontada), mas a grande diferença é que deleteem uma matriz não chamará o destruidor de cada elemento da matriz.

Enfim, misturando new/new[]e delete/delete[]é provavelmente UB.

Benoît
fonte
1
Resposta clara, curta e mais útil!
GntS # 12/17
6

Não sabe que é uma matriz, é por isso que você precisa fornecer, em delete[]vez da idade normal delete.

eduffy
fonte
5

Eu tive uma pergunta semelhante a isso. Em C, você aloca memória com malloc () (ou outra função semelhante) e a exclui com free (). Existe apenas um malloc (), que simplesmente aloca um certo número de bytes. Existe apenas um free (), que simplesmente aceita um ponteiro como parâmetro.

Então, por que em C você pode simplesmente entregar o ponteiro para liberar, mas em C ++ você deve dizer se é uma matriz ou uma única variável?

A resposta, eu aprendi, tem a ver com destruidores de classe.

Se você alocar uma instância de uma classe MyClass ...

classes = new MyClass[3];

E exclua-o com delete, você pode obter apenas o destruidor da primeira instância do MyClass chamada. Se você usar delete [], pode ter certeza de que o destruidor será chamado para todas as instâncias da matriz.

Esta é a diferença importante. Se você está simplesmente trabalhando com tipos padrão (por exemplo, int), não verá esse problema. Além disso, você deve se lembrar que o comportamento do uso de excluir em novo [] e excluir [] em novo não é definido - ele pode não funcionar da mesma maneira em todos os compiladores / sistemas.

ProdigySim
fonte
3

Cabe ao tempo de execução o responsável pela alocação de memória, da mesma maneira que você pode excluir uma matriz criada com malloc no padrão C usando gratuitamente. Eu acho que cada compilador implementa de maneira diferente. Uma maneira comum é alocar uma célula extra para o tamanho da matriz.

No entanto, o tempo de execução não é inteligente o suficiente para detectar se é ou não uma matriz ou um ponteiro, é necessário informá-lo e, se você estiver enganado, não exclui corretamente (por exemplo, ptr em vez de matriz) ou você acaba assumindo um valor não relacionado ao tamanho e causa danos significativos.

Uri
fonte
3

Uma das abordagens para compiladores é alocar um pouco mais de memória e armazenar a contagem de elementos no elemento principal.

Exemplo de como isso pode ser feito: Aqui

int* i = new int[4];

O compilador alocará sizeof (int) * 5 bytes.

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

Armazenará 4nos primeiros sizeof(int)bytes

*temp = 4;

E definir i

i = temp + 1;

Então, iaponta para a matriz de 4 elementos, não 5.

E

delete[] i;

será processado da seguinte maneira

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

Semanticamente, as duas versões do operador de exclusão no C ++ podem "comer" qualquer ponteiro; no entanto, se um ponteiro para um único objeto for fornecido delete[], o UB resultará, significando que tudo pode acontecer, incluindo uma falha no sistema ou nada.

O C ++ exige que o programador escolha a versão correta do operador de exclusão, dependendo do assunto da desalocação: matriz ou objeto único.

Se o compilador pudesse determinar automaticamente se um ponteiro passado para o operador de exclusão era uma matriz de ponteiros, haveria apenas um operador de exclusão no C ++, o que seria suficiente para ambos os casos.

mloskot
fonte
1

Concorde que o compilador não sabe se é uma matriz ou não. Depende do programador.

Às vezes, o compilador monitora quantos objetos precisam ser excluídos, alocando em excesso o suficiente para armazenar o tamanho da matriz, mas nem sempre necessário.

Para obter uma especificação completa quando o armazenamento extra é alocado, consulte C ++ ABI (como os compiladores são implementados): Itanium C ++ ABI: Array Operator new Cookies

shibo
fonte
Eu gostaria que todos os compiladores observassem alguma ABI documentada para C ++. +1 para o link, que eu visitei antes. Obrigado.
Don Wakefield
0

Você não pode usar excluir para uma matriz e não pode usar excluir [] para uma não matriz.

Don Wakefield
fonte
8
Eu acho que você quer dizer que não deveria , pois seu compilador médio não detectará o abuso.
Don Wakefield
0

"comportamento indefinido" significa simplesmente que a especificação do idioma não garante o que acontecerá. Isso geralmente não significa que algo ruim irá acontecer.

Então, de quem é o trabalho para lembrar dessas coisas? O sistema operacional mantém algum tipo de registro em segundo plano? (Quero dizer, eu percebi que comecei este post dizendo que o que acontece é indefinido, mas o fato é que o cenário de 'matança' não acontece, portanto, no mundo prático, alguém está se lembrando.)

Normalmente existem duas camadas aqui. O gerenciador de memória subjacente e a implementação do C ++.

Em geral, o gerenciador de memória lembrará (entre outras coisas) o tamanho do bloco de memória que foi alocado. Isso pode ser maior que o bloco solicitado pela implementação do C ++. Normalmente, o gerenciador de memória armazena seus metadados antes do bloco de memória alocado.

A implementação do C ++ geralmente lembra apenas o tamanho da matriz se for necessário para seus próprios fins, geralmente porque o tipo possui um destruidor não-trival.

Portanto, para tipos com um destruidor trivial, a implementação de "delete" e "delete []" é normalmente a mesma. A implementação C ++ simplesmente passa o ponteiro para o gerenciador de memória subjacente. Algo como

free(p)

Por outro lado, para tipos com um destruidor não trivial, "delete" e "delete []" provavelmente serão diferentes. "delete" seria algo parecido (onde T é o tipo que o ponteiro aponta)

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

Enquanto "delete []" seria algo parecido.

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

itere através da matriz de objetos e chame destruidor para cada um deles. Eu criei esse código simples que sobrecarrega novas expressões [] e exclui [] e fornece uma função de modelo para desalocar memória e chamar destruidor para cada objeto, se necessário:

// 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
fonte
-2

apenas defina um destruidor dentro de uma classe e execute seu código com as duas sintaxes

delete pointer

delete [] pointer

de acordo com a saída você pode encontrar as soluções

bubu
fonte
use delete [] quando criar um novo tipo de matriz. por exemplo int * a = new int; int * b = novo int [5]; excluir a; delete [] b;
Lineesh K Mohan
-3

A resposta:

int * pArray = novo int [5];

int tamanho = * (pArray-1);

Postado acima não está correto e produz valor inválido. O "-1" conta os elementos No sistema operacional Windows de 64 bits, o tamanho correto do buffer reside no endereço Ptr - 4 bytes

Evgeni Raikhel
fonte