segunda-feira, 22 de fevereiro de 2010

new/delete new[]/delete[]

Não pretendo esgotar o assunto new/delete, pois não é esse o propósito do meu blog. Pretendo demonstrar somente a diferença entre a versão para array e a para objetos e mostrar como o código se comporta em cada caso, em um próximo post pretendo falar sobre sobrecarga dos operadores new/delete, sobre construtores para tipos primitivos, e sobre como atacar código que faz um mau uso do new/delete. Dito isso, e levando em conta uma plataforma Intel 32bits, vamos lá! É regra que os programadores novatos em C++ decoram a forma de utilizar delete e delete[] como se fosse um motto: “Usarei delete quando usar new e delete[] para new[]”. Também é fato que poucos sabem explicar o que motiva o design da linguagem a separar o significado de destruir um único objeto de destruir um array e, principalmente, como eles funcionam! Quanto à separação, tudo remota a uma questão histórica; antigamente, bem nos primórdios do c++, o operador delete para arrays utilizava um argumento que indicava quantos itens deveriam ser liberados, como no exemplo:

p = new AwesomeObj[iSize];

delete[iSize] p;

Não é necessário comentar o tanto que isso potencializa a tendência ao desastre. E vale lembrar que essa tendência de causar desastres é uma característica intrínseca do c++ hehehe. Exigir que o código mantenha uma contagem sobre quantos elementos foram alocados é uma tática muito perigosa e força o design do programa a carregar esse fardo, sem contar que, no caso de a memória poder ser liberada em funções de callback ou fora do escopo da thread corrente, informação adicional teria de ser passada de um lado para o outro criando um overhead, muitas vezes, inaceitável. Em revisões mais novas da linguagem o operador foi modificado não necessitando mais de um indicativo de quantos elementos devem ser destruídos, no entanto a distinção entre destruir um único objeto com delete e um array com delete[] foi mantida.

Para entender melhor o funcionamento de cada um, caso você não saiba exatamente o que tem feito nesses seus dias de programação hehehehe, é necessário saber um tiquinho como C/C++ organiza a memória, mas não se assuste só um tiquinho hehehe. Permita-me antes falar um pouco sobre um fato: C++, ao contrário de C, pode lidar com classes que possuem, pelo menos, dois métodos especiais, o construtor e o destrutor. Quando alocamos memória em C com malloc, por exemplo, tudo o que estamos fazendo é requisitar um pedaço de memória e nada alem disso. Já new além de alocar uma quantidade de memória que possa conter nosso objeto também chama o construtor do mesmo, e isso vale para delete, antes de liberar a memória, como a função free faz em C, o operador delete chama o destrutor do objeto. Esse comportamento vale também para a versão array, ou seja, new[]/delete[], em código isso se traduz em:

char* p1 = new char[100];

char* p2 = (char *) malloc(100);

// p1 e p2 apontam para 100 bytes

// (considerando um char de um byte) alocados na memória.

// nesse caso as duas chamadas são equivalentes em sentido.

delete [] p1;

free(p2);

// Ambos liberam os 100 bytes alocados,

// mais uma vez, em sentido, as chamadas são equivalentes.

Agora vem a diferença, considere uma classe qualquer como, por exemplo, uma chamada AwesomeObj:

class AwesomeObj

{

public:

AwesomeObj() { printf("Construtor\n"); }

~AwesomeObj() { printf("Destrutor\n"); }

};

AwesomeObj *t1 = new AwesomeObj[100];

AwesomeObj *t2 = (AwesomeObj*) malloc( sizeof(AwesomeObj) * 100 );

// Agora t1 e t2 ambos apontam para 100 AwesomeObj na memória.

// Entretanto a primeira chamada, com new[], vai alocar a memoria para os 100

// AwesomeObj e vai chamar o construtor de cada um deles, já a versão com malloc não!

delete [] t1;

free(t2);

// Aqui o mesmo, a versão com delete[] vai chamar o destrutor de cada um dos 100

// AwesomeObj antes de liberar a memória, já o free não vai.

Então vem a pergunta e se eu usar delete para algo alocado com new[] ou usar delete[] para algo alocado com new? A resposta é: depende hehehe, o subsistema de memória e a implementação do seu compilador que vai ditar o que de fato é feito para alocar/desalocar um objeto, mas em linhas gerais quando você usa new o código irá alocar memória de acordo com o tamanho do tipo que você requisitou e chamar o construtor, se houver, do objeto recém alocado. No caso de utilizar new[] ele vai chamar o construtor de todos os itens que forem alocados, alem de manter uma sentinela que indica quantos objetos foram alocados. A localização exata dessa sentinela varia de compilador para compilador e de plataforma para plataforma. Para alocar memória o operador new/new[] vai chamar alguma função intermediaria entre o SO e a biblioteca default, chamada libc no caso do gcc, digamos que malloc seja utilizada para realizar a tarefa de alocar a memória. A função malloc, dependendo da implementação, irá alocar os X bytes requisitados + quatro, sendo que esses quatro bytes adicionais correspondem a uma palavra(WORD) em maquinas 32bits, essa palavra vai ficar no endereço &p – sizeof(unsigned int), ou, em miúdos, quatro bytes antes do inicio da área que é retornada para ser utilizada. Utilizando este valor o free sabe quantos bytes deve liberar. Daí que o operador delete[] irá realizar um trabalho semelhante a:

delete_array(obj * ptr)

{

size_t *tmpptr = ((size_t *)ptr) - 1;

int num_elements = *tmpptr;

for (int i=num_elements-1; i>=0; i--)

{

call_destructors(ptr[i]);

}

free(tmpptr);

}

Traduzindo, ele vai chamar o destrutor de todos objetos, e vai executar num_elements vezes, posteriormente vai chamar free para marcar a área alocada como livre novamente. Lembrando que não é garantido que somente um índice, o num_elements, seja mantido, alguma implementação pode manter duas listas separadas, uma para descrever quantos bytes foram alocados e outra para manter a contagem de quantos objetos estão em memória, facilitando a abstração do código. O delete vai operar de forma semelhante porem não vai procurar uma lista de objetos para serem removidos. Ele vai operar sobre um único objeto. Ai vem a pergunta, porque não usar delete[] quando o ponteiro tiver sido alocado com new? A resposta é simples e complicada ao mesmo tempo hehehe, quando new aloca um objeto ele não necessita marcar quantos objetos foram alocados, ou seja, não necessita criar uma sentinela de quantos objetos foram criados, dependendo da implementação somente a lista de bytes alocados mantida pelo subsistema do SO já baste para o caso new/delete. Isso é possível já que se trata de apenas de um objeto. Isso salva espaço no seu executável final e algumas instruções a menos podem significar algum ganho de velocidade, ok e daí? Daí que se você chamar delete[] é bem provável que ele vá procurar a sentinela que mantém a quantidade de objetos alocados e vai encontrar lixo. Uma vez que ele vai encarar esse lixo como um valor que corresponde à quantidade de objetos alocados ele pode sair chamando o destrutor infinitamente ou simplesmente quebrar o processo ou quem sabe fazer o seu vizinho te perseguir com uma serra elétrica, enfim o resultado é o que chamam de UB (undefined behaviour). O mesmo vale para o caso onde você aloca um array com new[] e chama delete, alguns dirão que ele vai liberar somente o primeiro item e liberar a memoria total, mas na verdade é UB, não é garantido que isso vá acontecer. Então, sabendo disso, mantenha o motto na cabeça só que agora sabendo ;)

That’s all folks!

Nenhum comentário: