Pergunta

Tenho trabalhado em alguns códigos C++ legados que usam estruturas de comprimento variável (TAPI), onde o tamanho da estrutura dependerá de strings de comprimento variável.As estruturas são alocadas lançando array new por isso:

STRUCT* pStruct = (STRUCT*)new BYTE [sizeof(STRUCT) + nPaddingSize];

Mais tarde, porém, a memória é liberada usando um delete chamar:

delete pStruct;

Essa mistura de array new [] e não-matriz delete causaria um vazamento de memória ou dependeria do compilador?Seria melhor alterar este código para usar malloc e free em vez de?

Foi útil?

Solução

Tecnicamente, acredito que isso poderia causar um problema com alocadores incompatíveis, embora na prática não conheça nenhum compilador que não fizesse a coisa certa com este exemplo.

Mais importante se STRUCT onde ter (ou receber) um destruidor, ele invocaria o destruidor sem ter invocado o construtor correspondente.

Claro, se você sabe de onde veio o pStruct, por que não simplesmente lançá-lo em delete para corresponder à alocação:

delete [] (BYTE*) pStruct;

Outras dicas

Eu pessoalmente acho que seria melhor você usar std::vector para gerenciar sua memória, então você não precisa do delete.

std::vector<BYTE> backing(sizeof(STRUCT) + nPaddingSize);
STRUCT* pStruct = (STRUCT*)(&backing[0]);

Depois que o apoio sai do escopo, seu pStruct não é mais válido.

Ou você pode usar:

boost::scoped_array<BYTE> backing(new BYTE[sizeof(STRUCT) + nPaddingSize]);
STRUCT* pStruct = (STRUCT*)backing.get();

Ou boost::shared_array se você precisar transferir a propriedade.

Sim, isso causará um vazamento de memória.

Veja isto, exceto em C++ Gotchas: http://www.informit.com/articles/article.aspx?p=30642 por quê.

Raymond Chen tem uma explicação de como o vetor new e delete diferem das versões escalares nos bastidores do compilador Microsoft ...Aqui:http://blogs.msdn.com/oldnewthing/archive/2004/02/03/66660.aspx

IMHO você deve corrigir a exclusão para:

delete [] pStruct;

em vez de mudar para malloc/free, até porque é uma mudança mais simples de fazer sem cometer erros ;)

E, claro, o mais simples de fazer a alteração que mostro acima está errado devido à conversão na alocação original, deveria ser

delete [] reinterpret_cast<BYTE *>(pStruct);

então, acho que provavelmente é tão fácil mudar para malloc/free afinal ;)

O comportamento do código é indefinido.Você pode ter sorte (ou não) e pode funcionar com o seu compilador, mas na verdade esse não é o código correto.Há dois problemas com isso:

  1. O delete deve ser uma matriz delete [].
  2. O delete deve ser chamado em um ponteiro para o mesmo tipo que o tipo alocado.

Então, para estar totalmente correto, você quer fazer algo assim:

delete [] (BYTE*)(pStruct);

O padrão C++ afirma claramente:

delete-expression:
             ::opt delete cast-expression
             ::opt delete [ ] cast-expression

A primeira alternativa é para objetos não array e a segunda é para arrays.O operando deve ter um tipo ponteiro, ou um tipo de classe com uma única função de conversão (12.3.2) para um tipo ponteiro.O resultado tem tipo void.

Na primeira alternativa (objeto delete), o valor do operando de delete deve ser um ponteiro para um objeto não array [...] Caso contrário, o comportamento é indefinido.

O valor do operando em delete pStruct é um ponteiro para uma matriz de char, independente do seu tipo estático (STRUCT*).Portanto, qualquer discussão sobre vazamentos de memória é inútil, porque o código está mal formado e um compilador C++ não é necessário para produzir um executável sensato neste caso.

Poderia vazar memória, não poderia, ou poderia fazer qualquer coisa até travar seu sistema.Na verdade, uma implementação C++ com a qual testei seu código aborta a execução do programa no ponto da expressão de exclusão.

Conforme destacado em outras postagens:

1) Chamadas para new/delete alocam memória e podem chamar construtores/destruidores (C++ '03 5.3.4/5.3.5)

2) Misturar versões array/não array de new e delete é um comportamento indefinido.(C++ '03 5.3.5/4)

Olhando a fonte, parece que alguém fez uma pesquisa e substituiu por malloc e free e o acima é o resultado.C++ tem um substituto direto para essas funções, que é chamar as funções de alocação para new e delete diretamente:

STRUCT* pStruct = (STRUCT*)::operator new (sizeof(STRUCT) + nPaddingSize);
// ...
pStruct->~STRUCT ();  // Call STRUCT destructor
::operator delete (pStruct);

Se o construtor de STRUCT for chamado, você poderá considerar alocar a memória e usar o posicionamento new:

BYTE * pByteData = new BYTE[sizeof(STRUCT) + nPaddingSize];
STRUCT * pStruct = new (pByteData) STRUCT ();
// ...
pStruct->~STRUCT ();
delete[] pByteData;

@eric - Obrigado pelos comentários.Você continua dizendo algo que me deixa louco:

Essas bibliotecas de tempo de execução lidam com o chamadas de gerenciamento de memória para o sistema operacional em um Sintaxe consistente independente do SO e essas bibliotecas de tempo de execução são Responsável por fazer malloc e novos trabalhar consistentemente entre sistemas operacionais, como Linux, Windows, Solaris, AIX, etc....

Isso não é verdade.O escritor do compilador fornece a implementação das bibliotecas std, por exemplo, e eles são absolutamente livres para implementá-las em um sistema operacional dependente caminho.Eles são livres, por exemplo, para fazer uma chamada gigante para malloc e então gerenciar a memória dentro do bloco da maneira que desejarem.

A compatibilidade é fornecida porque a API do std, etc.é o mesmo - não porque todas as bibliotecas de tempo de execução giram e chamam exatamente as mesmas chamadas de sistema operacional.

Os vários usos possíveis das palavras-chave new e delete parecem criar bastante confusão.Sempre há dois estágios na construção de objetos dinâmicos em C++:a alocação da memória bruta e a construção do novo objeto na área de memória alocada.Do outro lado do tempo de vida do objeto, há a destruição do objeto e a desalocação do local da memória onde o objeto residia.

Freqüentemente, essas duas etapas são executadas por uma única instrução C++.

MyObject* ObjPtr = new MyObject;

//...

delete MyObject;

Em vez do acima, você pode usar as funções de alocação de memória bruta C++ operator new e operator delete e construção explícita (via colocação new) e destruição para executar as etapas equivalentes.

void* MemoryPtr = ::operator new( sizeof(MyObject) );
MyObject* ObjPtr = new (MemoryPtr) MyObject;

// ...

ObjPtr->~MyObject();
::operator delete( MemoryPtr );

Observe como não há conversão envolvida e apenas um tipo de objeto é construído na área de memória alocada.Usando algo como new char[N] como forma de alocar memória bruta é tecnicamente incorreto, pois, logicamente, char objetos são criados na memória recém-alocada.Não conheço nenhuma situação em que isso não 'simplesmente funcione', mas confunda a distinção entre alocação de memória bruta e criação de objetos, por isso desaconselho isso.

Neste caso específico, não há ganho em separar as duas etapas do delete mas você precisa controlar manualmente a alocação inicial.O código acima funciona no cenário 'tudo funcionando', mas irá vazar a memória bruta no caso em que o construtor de MyObject lança uma exceção.Embora isso possa ser detectado e resolvido com um manipulador de exceção no ponto de alocação, provavelmente é mais fácil fornecer um operador new personalizado para que a construção completa possa ser tratada por uma expressão new de posicionamento.

class MyObject
{
    void* operator new( std::size_t rqsize, std::size_t padding )
    {
        return ::operator new( rqsize + padding );
    }

    // Usual (non-placement) delete
    // We need to define this as our placement operator delete
    // function happens to have one of the allowed signatures for
    // a non-placement operator delete
    void operator delete( void* p )
    {
        ::operator delete( p );
    }

    // Placement operator delete
    void operator delete( void* p, std::size_t )
    {
        ::operator delete( p );
    }
};

Existem alguns pontos sutis aqui.Definimos um novo posicionamento de classe para que possamos alocar memória suficiente para a instância da classe, além de algum preenchimento especificável pelo usuário.Como fazemos isso, precisamos fornecer uma exclusão de posicionamento correspondente para que, se a alocação de memória for bem-sucedida, mas a construção falhar, a memória alocada seja automaticamente desalocada.Infelizmente, a assinatura para nossa exclusão de posicionamento corresponde a uma das duas assinaturas permitidas para exclusão de não posicionamento, portanto, precisamos fornecer a outra forma de exclusão de posicionamento para que nossa exclusão de posicionamento real seja tratada como uma exclusão de posicionamento.(Poderíamos ter contornado isso adicionando um parâmetro fictício extra ao nosso posicionamento novo e exclusão de posicionamento, mas isso exigiria trabalho extra em todos os sites de chamada.)

// Called in one step like so:
MyObject* ObjectPtr = new (padding) MyObject;

Usando uma única nova expressão, agora temos a garantia de que a memória não vazará se qualquer parte da nova expressão for lançada.

No outro extremo da vida útil do objeto, como definimos o operador delete (mesmo que não o tivéssemos, a memória do objeto veio originalmente do operador global new em qualquer caso), a seguir está a maneira correta de destruir o objeto criado dinamicamente .

delete ObjectPtr;

Resumo!

  1. Não procure moldes! operator new e operator delete lidar com memória bruta, a colocação de novos pode construir objetos na memória bruta.Um elenco explícito de um void* para um ponteiro de objeto geralmente é um sinal de algo logicamente errado, mesmo que 'simplesmente funcione'.

  2. Ignoramos completamente new[] e delete[].Esses objetos de tamanho variável não funcionarão em arrays em nenhum caso.

  3. A colocação new permite que uma nova expressão não vaze, a nova expressão ainda é avaliada como um ponteiro para um objeto que precisa ser destruído e memória que precisa ser desalocada.O uso de algum tipo de ponteiro inteligente pode ajudar a prevenir outros tipos de vazamento.Do lado positivo, deixamos claro delete seja a maneira correta de fazer isso para que a maioria dos ponteiros inteligentes padrão funcionem.

Se você realmente precisa fazer esse tipo de coisa, você provavelmente deveria ligar para a operadora new diretamente:

STRUCT* pStruct = operator new(sizeof(STRUCT) + nPaddingSize);

Acredito que chamá-lo dessa forma evita chamar construtores/destruidores.

No momento não posso votar, mas resposta de limão fatiado é preferível A resposta de Rob Walker, já que o problema não tem nada a ver com alocadores ou se o STRUCT possui ou não um destruidor.

Observe também que o código de exemplo não resulta necessariamente em vazamento de memória - é um comportamento indefinido.Praticamente tudo pode acontecer (desde nada de ruim até um acidente muito, muito distante).

O código de exemplo resulta em comportamento indefinido, puro e simples.A resposta do slicedlime é direta e direta (com a ressalva de que a palavra 'vetor' deve ser alterada para 'array', já que vetores são uma coisa STL).

Esse tipo de coisa é abordado muito bem nas Perguntas frequentes sobre C++ (Seções 16.12, 16.13 e 16.14):

http://www.parashift.com/c++-faq-lite/freestore-mgmt.html#faq-16.12

É uma exclusão de array ([]) a que você está se referindo, não uma exclusão de vetor.Um vetor é std::vector e cuida da exclusão de seus elementos.

Você poderia retornar para um BYTE * e excluir:

delete[] (BYTE*)pStruct;

Sim, pode, já que você está alocando com new[] mas desalocando com delelte, sim, malloc/free é mais seguro aqui, mas em c++ você não deve usá-los, pois eles não suportam (des)construtores.

Além disso, seu código chamará o desconstrutor, mas não o construtor.Para algumas estruturas isso pode causar um vazamento de memória (se o construtor alocou mais memória, por exemplo, para uma string)

Melhor seria fazê-lo corretamente, pois isso também chamará corretamente quaisquer construtores e desconstrutores

STRUCT* pStruct = new STRUCT;
...
delete pStruct;

É sempre melhor manter a aquisição/liberação de qualquer recurso o mais equilibrada possível.Embora vazando ou não seja difícil dizer neste caso.Depende da implementação da (des) alocação de vetor pelo compilador.

BYTE * pBytes = new BYTE [sizeof(STRUCT) + nPaddingSize];

STRUCT* pStruct = reinterpret_cast< STRUCT* > ( pBytes ) ;

 // do stuff with pStruct

delete [] pBytes ;

Len:o problema é que pStruct é um STRUCT*, mas a memória alocada é na verdade um BYTE[] de tamanho desconhecido.Portanto, delete[] pStruct não desalocará toda a memória alocada.

Você está misturando maneiras de fazer as coisas em C e C++.Por que alocar mais do que o tamanho de uma STRUCT?Por que não apenas "nova ESTRUTURA"?Se você precisar fazer isso, talvez seja mais claro usar malloc e free nesse caso, já que você ou outros programadores poderão ter menos probabilidade de fazer suposições sobre os tipos e tamanhos dos objetos alocados.

@Matt Cruikshank Você deve prestar atenção e ler o que eu escrevi novamente, porque eu nunca sugeri não chamar delete[] e apenas deixar o sistema operacional limpar.E você está errado sobre as bibliotecas de tempo de execução C++ que gerenciam o heap.Se fosse esse o caso, o C++ não seria portátil como é hoje e um aplicativo travado nunca seria limpo pelo sistema operacional.(reconhecendo que existem tempos de execução específicos do sistema operacional que fazem o C/C++ parecer não portátil).Eu desafio você a encontrar stdlib.h nas fontes do Linux em kernel.org.A nova palavra-chave em C++, na verdade, está se comunicando com as mesmas rotinas de gerenciamento de memória que malloc.

As bibliotecas de tempo de execução C++ fazem chamadas de sistema do sistema operacional e é o sistema operacional que gerencia os heaps.Você está parcialmente correto ao dizer que as bibliotecas de tempo de execução indicam quando liberar a memória; no entanto, elas não percorrem nenhuma tabela heap diretamente.Em outras palavras, o tempo de execução ao qual você vincula não adiciona código ao seu aplicativo para percorrer os heaps para alocar ou desalocar.Este é o caso do Windows, Linux, Solaris, AIX, etc...É também a razão pela qual você não encontrará malloc em nenhuma fonte de kernel do Linux nem encontrará stdlib.h na fonte do Linux.Entenda que esses sistemas operacionais modernos possuem gerenciadores de memória virtual, o que complica um pouco mais as coisas.

Você já se perguntou por que você pode fazer uma chamada para malloc para obter 2G de RAM em uma caixa de 1G e ainda receber de volta um ponteiro de memória válido?

O gerenciamento de memória em processadores x86 é gerenciado no espaço do Kernel usando três tabelas.PAM (Tabela de Alocação de Páginas), PD (Diretórios de Páginas) e PT (Tabelas de Páginas).Isso está no nível de hardware do qual estou falando.Uma das coisas que o gerenciador de memória do sistema operacional faz, e não o seu aplicativo C++, é descobrir quanta memória física está instalada na caixa durante a inicialização com a ajuda de chamadas do BIOS.O sistema operacional também lida com exceções, como quando você tenta acessar a memória, seu aplicativo também não tem direitos.(Falha de proteção geral GPF).

Pode ser que estejamos dizendo a mesma coisa, Matt, mas acho que você pode estar confundindo um pouco a funcionalidade subjacente.Eu uso para manter um compilador C/C++ para viver...

@ericmayo - caramba.Bem, experimentando o VS2005, não consigo obter um vazamento honesto da exclusão escalar na memória feita pelo vetor novo.Acho que o comportamento do compilador é "indefinido" aqui, é a melhor defesa que posso reunir.

Você tem que admitir, porém, é uma péssima prática fazer o que o autor da postagem original disse.

Se esse fosse o caso, o C++ não ser portátil como é hoje e um Travando o aplicativo nunca ficaria limpo pelo sistema operacional.

Essa lógica realmente não se sustenta, no entanto.Minha afirmação é que o tempo de execução de um compilador pode gerenciar a memória dentro dos blocos de memória que o sistema operacional retorna para ele.É assim que a maioria das máquinas virtuais funciona; portanto, seu argumento contra a portabilidade nesse caso não faz muito sentido.

@Matt Cruikshank

"Bem, experimentando o VS2005, não consigo obter um vazamento honesto da exclusão escalar na memória feita pelo vetor novo.Acho que o comportamento do compilador é "indefinido" aqui, é a melhor defesa que posso reunir."

Não concordo que seja um comportamento do compilador ou mesmo um problema do compilador.A palavra-chave 'new' é compilada e vinculada, como você apontou, a bibliotecas em tempo de execução.Essas bibliotecas de tempo de execução lidam com as chamadas de gerenciamento de memória para o sistema operacional em uma sintaxe consistente independente do sistema operacional e essas bibliotecas de tempo de execução são responsáveis ​​por fazer com que malloc e new funcionem consistentemente entre sistemas operacionais como Linux, Windows, Solaris, AIX, etc. .Esta é a razão pela qual mencionei o argumento da portabilidade;uma tentativa de provar que o tempo de execução também não gerencia a memória.

O sistema operacional gerencia a memória.

As bibliotecas de tempo de execução fazem interface com o sistema operacional.No Windows, estas são as DLLs do gerenciador de memória virtual.É por isso que stdlib.h é implementado nas bibliotecas GLIB-C e não na fonte do kernel Linux;se GLIB-C for usado em outros sistemas operacionais, é a implementação de alterações no malloc para fazer as chamadas corretas do sistema operacional.Em VS, Borland, etc.você nunca encontrará nenhuma biblioteca fornecida com seus compiladores que realmente gerencie a memória.Você encontrará, no entanto, definições específicas do sistema operacional para malloc.

Como temos o código-fonte do Linux, você pode ver como o malloc é implementado lá.Você verá que malloc é realmente implementado no compilador GCC que, por sua vez, basicamente faz duas chamadas de sistema Linux no kernel para alocar memória.Nunca, o próprio malloc, gerenciando realmente a memória!

E não tire isso de mim.Leia o código-fonte do sistema operacional Linux ou veja o que K&R diz sobre isso...Aqui está um link em PDF para o K&R em C.

http://www.oberon2005.ru/paper/kr_c.pdf

Veja perto do final da página 149:“Chamadas para malloc e free podem ocorrer em qualquer ordem;malloc chama sobre o sistema operacional para obter mais memória conforme necessário.Essas rotinas ilustram algumas das considerações envolvidas na escrita de código dependente de máquina de uma forma relativamente independente de máquina, e também mostram uma aplicação na vida real de estruturas, uniões e typedef."

"Você tem que admitir, é uma péssima prática fazer o que o autor da postagem original disse."

Ah, eu não discordo disso.O que quero dizer é que o código do postador original não conduzia a um vazamento de memória.Isso é tudo que eu estava dizendo.Não falei sobre o lado das melhores práticas.Como o código está chamando delete, a memória está sendo liberada.

Concordo, em sua defesa, se o código do autor da postagem original nunca saiu ou nunca chegou à chamada de exclusão, que o código poderia ter um vazamento de memória, mas já que ele afirma que mais tarde vê a exclusão sendo chamada."Mais tarde, porém, a memória é liberada usando uma chamada delete:"

Além disso, meu motivo para responder como respondi foi devido ao comentário do OP "estruturas de comprimento variável (TAPI), onde o tamanho da estrutura dependerá de strings de comprimento variável"

Esse comentário parecia que ele estava questionando a natureza dinâmica das alocações feitas em relação ao elenco e, consequentemente, se perguntando se isso causaria um vazamento de memória.Eu estava lendo nas entrelinhas, se você quiser;).

Além das excelentes respostas acima, gostaria também de acrescentar:

Se o seu código rodar no Linux ou se você puder compilá-lo no Linux, sugiro executá-lo Valgrind.É uma excelente ferramenta, entre a miríade de avisos úteis que produz, também avisa quando você aloca memória como um array e depois a libera como um não-array (e vice-versa).

Use o operador new e delete:

struct STRUCT
{
  void *operator new (size_t)
  {
    return new char [sizeof(STRUCT) + nPaddingSize];
  }

  void operator delete (void *memory)
  {
    delete [] reinterpret_cast <char *> (memory);
  }
};

void main()
{
  STRUCT *s = new STRUCT;
  delete s;
}

Acho que não há vazamento de memória.

STRUCT* pStruct = (STRUCT*)new BYTE [sizeof(STRUCT) + nPaddingSize];

Isso é traduzido em uma chamada de alocação de memória dentro do sistema operacional, na qual um ponteiro para essa memória é retornado.No momento em que a memória é alocada, o tamanho do sizeof(STRUCT) e o tamanho de nPaddingSize seria conhecido para atender a quaisquer solicitações de alocação de memória no sistema operacional subjacente.

Assim, a memória alocada é “registrada” nas tabelas globais de alocação de memória do sistema operacional.As tabelas de memória são indexadas por seus ponteiros.Portanto, na chamada correspondente para excluir, toda a memória alocada originalmente está livre.(a fragmentação da memória também é um assunto popular neste domínio).

Veja bem, o compilador C/C++ não está gerenciando a memória, o sistema operacional subjacente está.

Concordo que existem métodos mais limpos, mas o OP disse que este era um código legado.

Resumindo, não vejo vazamento de memória, pois a resposta aceita acredita que exista.

Rob Walker responder é bom.

Apenas uma pequena adição, se você não possui nenhum construtor e/ou disstrutores, então basicamente precisa alocar e liberar um pedaço de memória bruta, considere usar o par free/malloc.

ericmayo.myopenid.com está tão errado que alguém com reputação suficiente deveria desconsiderá-lo.

As bibliotecas de tempo de execução C ou C++ gerenciam o heap que é fornecido em blocos pelo sistema operacional, mais ou menos como você indicou, Eric.Mas isso é É responsabilidade do desenvolvedor indicar ao compilador quais chamadas de tempo de execução devem ser feitas para liberar memória e possivelmente destruir os objetos que estão lá.A exclusão de vetor (também conhecida como delete[]) é necessária neste caso, para que o tempo de execução do C++ deixe o heap em um estado válido.O fato de que quando o PROCESS termina, o sistema operacional é inteligente o suficiente para desalocar os blocos de memória subjacentes não é algo em que os desenvolvedores devam confiar.Isso seria como nunca chamar delete.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top