O que o Visual Studio faz com um ponteiro excluído e por quê?

130

Um livro em C ++ que tenho lido afirma que, quando um ponteiro é excluído usando o deleteoperador, a memória no local para o qual está apontando é "liberada" e pode ser substituída. Ele também afirma que o ponteiro continuará apontando para o mesmo local até que seja reatribuído ou definido como NULL.

No Visual Studio 2012, no entanto; esse não parece ser o caso!

Exemplo:

#include <iostream>

using namespace std;

int main()
{
    int* ptr = new int;
    cout << "ptr = " << ptr << endl;
    delete ptr;
    cout << "ptr = " << ptr << endl;

    system("pause");

    return 0;
}

Ao compilar e executar este programa, obtenho a seguinte saída:

ptr = 0050BC10
ptr = 00008123
Press any key to continue....

Claramente, o endereço que o ponteiro está apontando muda quando a exclusão é chamada!

Por que isso está acontecendo? Isso tem algo a ver com o Visual Studio especificamente?

E se delete pode alterar o endereço para o qual está apontando, por que não excluir automaticamente define o ponteiro para em NULLvez de algum endereço aleatório?

tjwrona1992
fonte
4
Exclua um ponteiro, não significa que ele será definido como NULL, você precisa cuidar disso.
Matt
11
Eu sei disso, mas o livro que estou lendo especificamente diz que ainda conterá o mesmo endereço para o qual estava apontando antes da exclusão, mas o conteúdo desse endereço pode ser substituído.
tjwrona1992
6
@ tjwrona1992, sim, porque é isso que geralmente está acontecendo. O livro apenas lista os resultados mais prováveis, não a regra mais difícil.
Sergeya
5
@ tjwrona1992 Um livro em C ++ que tenho lido - e o nome do livro é ...?
PaulMcKenzie
4
@ tjwrona1992: Pode ser surpreendente, mas todo o uso do valor do ponteiro inválido é um comportamento indefinido, não apenas a desreferenciação. "Verificar para onde está apontando" está usando o valor de uma maneira não permitida.
Ben Voigt

Respostas:

175

Notei que o endereço armazenado ptrestava sempre sendo substituído por 00008123...

Isso parecia estranho, então eu pesquisei um pouco e encontrei esta postagem no blog da Microsoft contendo uma seção discutindo "Desinfecção automática de ponteiros ao excluir objetos C ++".

... verificações de NULL são uma construção de código comum, o que significa que uma verificação existente de NULL combinada com o uso de NULL como um valor de sanitização poderia esconder por sorte um problema genuíno de segurança de memória cuja causa raiz realmente precisa ser resolvida.

Por esse motivo, escolhemos 0x8123 como um valor de higienização - do ponto de vista do sistema operacional, ele está na mesma página de memória que o endereço zero (NULL), mas uma violação de acesso em 0x8123 será mais destacada para o desenvolvedor por precisar de atenção mais detalhada .

Ele não apenas explica o que o Visual Studio faz com o ponteiro após a exclusão, mas também responde por que eles escolheram NÃO configurá-lo NULLautomaticamente!


Este "recurso" está ativado como parte da configuração "Verificações SDL". Para ativar / desativar, vá para: PROJETO -> Propriedades -> Propriedades de configuração -> C / C ++ -> Geral -> Verificações SDL

Para confirmar isso:

Alterar essa configuração e executar novamente o mesmo código produz a seguinte saída:

ptr = 007CBC10
ptr = 007CBC10

"feature" está entre aspas porque, em um caso em que você tem dois ponteiros para o mesmo local, chamar delete excluirá apenas UM deles. O outro será deixado apontando para o local inválido ...


ATUALIZAR:

Após mais 5 anos de experiência em programação C ++, percebo que todo esse problema é basicamente um ponto discutível. Se você é um programador C ++ e ainda está usando newe deletegerenciando ponteiros brutos em vez de usar ponteiros inteligentes (que contornam todo esse problema), convém considerar uma mudança na carreira para se tornar um programador C. ;)

tjwrona1992
fonte
12
Essa é uma boa descoberta. Eu gostaria que o MS documentasse melhor o comportamento de depuração como este. Por exemplo, seria bom saber qual versão do compilador começou a implementar isso e quais opções ativam / desativam o comportamento.
Michael Burr
5
"do ponto de vista do sistema operacional, isso está na mesma página de memória que o endereço zero" - hein? O tamanho da página padrão (ignorando páginas grandes) no x86 ainda não é 4kb para Windows e Linux? Embora eu me lembre vagamente de algo sobre os primeiros 64kb de espaço de endereço no blog de Raymond Chen, na prática, tomo o mesmo resultado:
Voo
12
O @Voo windows reserva o primeiro (e último) valor de 64kB de RAM como espaço morto para captura. 0x8123 cai bem lá dentro
catraca anormal
7
Na verdade, ele não incentiva os maus hábitos, e não permitir que você pule definir o ponteiro para NULL - que é toda a razão eles estão usando 0x8123em vez de 0. O ponteiro ainda é inválido, mas causa uma exceção ao tentar desreferenciá-lo (bom) e não passa nas verificações NULL (também bom, porque é um erro não fazer isso). Onde é o lugar para maus hábitos? Realmente é apenas algo que ajuda você a depurar.
Luaan 29/10/2015
3
Bem, ele não pode definir os dois (todos), então esta é a segunda melhor opção. Se você não gostar, basta desativar as verificações SDL - acho-as bastante úteis, especialmente ao depurar o código de outra pessoa.
Luaan 29/10/2015
30

Você vê os efeitos colaterais da /sdlopção de compilação. Ativado por padrão para projetos do VS2015, ele permite verificações de segurança adicionais além daquelas fornecidas por / gs. Use a configuração Projeto> Propriedades> C / C ++> Geral> Verificações SDL para alterá-la.

Citando o artigo MSDN :

  • Executa a limpeza limitada do ponteiro. Em expressões que não envolvem desreferências e em tipos que não possuem destruidor definido pelo usuário, as referências de ponteiro são definidas para um endereço inválido após uma chamada para exclusão. Isso ajuda a impedir a reutilização de referências de ponteiro obsoletas.

Lembre-se de que definir ponteiros excluídos para NULL é uma prática ruim quando você usa o MSVC. Isso anula a ajuda que você obtém da Debug Heap e da opção / sdl. Você não pode mais detectar chamadas gratuitas de exclusão / exclusão no seu programa.

Hans Passant
fonte
1
Confirmado. Depois de desativar esse recurso, o ponteiro não é mais redirecionado. Obrigado por fornecer a configuração real que a modifica!
Tjwrona1992 27/10/2015
Hans, ainda é considerado uma má prática definir ponteiros excluídos como NULL em um caso em que você tem dois ponteiros apontando para o mesmo local? Quando você deleteescolhe, o Visual Studio deixará o segundo ponteiro apontando para o local original, que agora é inválido.
tjwrona1992
1
Muito claro para mim que tipo de mágica você espera que aconteça, definindo o ponteiro como NULL. Esse outro ponteiro não é, portanto, não resolve nada, você ainda precisa do alocador de depuração para encontrar o bug.
Hans Passant
3
O VS não limpa os ponteiros. Isso os corrompe. Portanto, seu programa falhará quando você os usar de qualquer maneira. O alocador de depuração faz a mesma coisa com a memória heap. O grande problema com o NULL, ele não é corrompido o suficiente. Caso contrário, uma estratégia comum, google "0xdeadbeef".
Hans Passant
1
Definir o ponteiro como NULL ainda é muito melhor do que deixá-lo apontando para o endereço anterior, que agora é inválido. Tentar gravar em um ponteiro NULL não corromperá nenhum dado e provavelmente travará o programa. Tentar reutilizar o ponteiro nesse ponto pode até não travar o programa, mas apenas produzir resultados imprevisíveis!
tjwrona1992
19

Ele também afirma que o ponteiro continuará apontando para o mesmo local até que seja reatribuído ou definido como NULL.

Essa é definitivamente uma informação enganosa.

Claramente, o endereço que o ponteiro está apontando muda quando a exclusão é chamada!

Por que isso está acontecendo? Isso tem algo a ver com o Visual Studio especificamente?

Isso está claramente dentro das especificações de idioma. ptrnão é válido após a chamada para delete. Usandoptr após deleted foi motivo de comportamento indefinido. Não faça isso. O ambiente de tempo de execução é livre para fazer o que quiser ptrapós a chamada delete.

E se delete pode alterar o endereço para o qual está apontando, por que não excluir automaticamente define o ponteiro para NULL em vez de algum endereço aleatório ???

Alterar o valor do ponteiro para qualquer valor antigo está dentro da especificação do idioma. Quanto a alterá-lo para NULL, eu diria que isso seria ruim. O programa se comportaria de maneira mais sensata se o valor do ponteiro fosse definido como NULL. No entanto, isso ocultará o problema. Quando o programa é compilado com diferentes configurações de otimização ou transportado para um ambiente diferente, o problema provavelmente aparecerá no momento mais inoportuno.

R Sahu
fonte
1
Não acredito que responda à pergunta da OP.
Sergeya
Discordo mesmo após a edição. Configurá-lo como NULL não ocultará o problema - na verdade, o EXPIRARIA em mais casos do que sem isso. Há uma razão pela qual implementações normais não fazem isso, e a razão é diferente.
Sergeya
4
@SergeyA, a maioria das implementações não faz isso por uma questão de eficiência. No entanto, se uma implementação decidir configurá-lo, é melhor configurá-lo para algo que não seja NULL. Isso revelaria os problemas mais cedo do que se estivesse definido como NULL. Está definido como NULL, chamar deleteduas vezes o ponteiro não causaria problemas. Definitivamente isso não é bom.
R Sahu 27/10
Não, não a eficiência - pelo menos, não é a principal preocupação.
SergeyA
7
@SergeyA Definir um ponteiro para um valor que não esteja, NULLmas também definitivamente fora do espaço de endereço do processo, exporá mais casos do que as duas alternativas. Deixá-lo pendurado não causará necessariamente um segfault se for usado após ser liberado; configurá-lo para NULLnão causará um segfault se for deleted novamente.
Blacklight Shining
10
delete ptr;
cout << "ptr = " << ptr << endl;

Em geral, mesmo os valores de leitura (como você fez acima, observe: isso é diferente da exclusão de referência) de ponteiros inválidos (o ponteiro se torna inválido, por exemplo, quando você o deletefaz) como comportamento definido pela implementação. Isso foi introduzido no CWG # 1438 . Veja também aqui .

Observe que antes que os valores de leitura de ponteiros inválidos fossem um comportamento indefinido, o que você tem acima seria um comportamento indefinido, o que significa que qualquer coisa poderia acontecer.

giorgim
fonte
3
Também é relevante a citação de [basic.stc.dynamic.deallocation]: "Se o argumento fornecido para uma função de desalocação na biblioteca padrão for um ponteiro que não seja o valor nulo do ponteiro, a função de desalocação deverá desalocar o armazenamento referenciado pelo ponteiro, invalidando todos os ponteiros referentes a qualquer parte do armazenamento desalocado "e a regra na [conv.lval](seção 4.1) que diz que ler (lvalue-> conversão de valor) qualquer valor de ponteiro inválido é um comportamento definido pela implementação.
Ben Voigt
Até o UB pode ser implementado de uma maneira específica por um fornecedor específico, de forma que seja confiável, pelo menos para esse compilador. Se a Microsoft tivesse decidido implementar seu recurso de limpeza de ponteiro antes do CWG # 1438, isso não tornaria esse recurso mais ou menos confiável e, em particular, simplesmente não é verdade que "algo poderia acontecer" se esse recurso estivesse ativado , independentemente do que o padrão diz.
Kyle Strand
@KyleStrand: Basicamente, dei definição de UB ( blog.regehr.org/archives/213 ).
Giorgim
1
Para a maioria da comunidade C ++ no SO, "tudo pode acontecer" é considerado literalmente demais . Eu acho isso ridículo . Entendo a definição de UB, mas também entendo que os compiladores são apenas peças de software implementadas por pessoas reais, e se essas pessoas implementam o compilador para que ele se comporte de certa maneira, é assim que o compilador se comportará , independentemente do que o padrão diz .
Kyle Strand
1

Acredito que você esteja executando algum tipo de modo de depuração, e o VS está tentando apontar o ponteiro para algum local conhecido, para que outras tentativas de desreferenciação possam ser rastreadas e relatadas. Tente compilar / executar o mesmo programa no modo de lançamento.

Normalmente, os ponteiros não são trocados deletepor motivos de eficiência e para evitar dar uma falsa idéia de segurança. Definir o ponteiro de exclusão para um valor predefinido não será bom na maioria dos cenários complexos, pois o ponteiro que está sendo excluído provavelmente será apenas um dos vários que apontam para esse local.

Por uma questão de fato, quanto mais eu penso sobre isso, mais eu acho que o VS é o culpado ao fazê-lo, como de costume. E se o ponteiro for const? Ainda vai mudar isso?

SergeyA
fonte
Sim, até indicadores constantes são redirecionados para este misterioso 8123!
tjwrona1992
Há outra pedra no VS :) Hoje de manhã alguém perguntou por que eles deveriam usar o g ++ em vez do VS. Aqui vai.
Sergeya
7
@SergeyA, mas, por outro lado, recusar a marcação do ponteiro excluído mostra por segfault que você tentou remover a marcação de um ponteiro excluído e não será igual a NULL. No outro caso, ele só trava se a página também for liberada (o que é muito improvável). Falhe mais rápido; resolver mais cedo.
ratchet freak
1
@ratchetfreak "Falhar rápido, resolver mais cedo" é um mantra muito valioso, mas "Falhar rápido destruindo as principais evidências forenses" não inicia um mantra tão valioso. Em casos simples, pode ser conveniente, mas em casos mais complicados (aqueles em que precisamos mais de ajuda), apagar informações valiosas diminui minhas ferramentas disponíveis para resolver o problema.
Cort Ammon
2
@ tjwrona1992: A Microsoft está fazendo a coisa certa aqui na minha opinião. Desinfetar um ponteiro é melhor do que não fazer nenhum. E se isso causar um problema na depuração, coloque um ponto de interrupção antes da chamada de exclusão incorreta. As probabilidades são de que, sem algo assim, você nunca perceberia o problema. E se você tem uma solução melhor para localizar esses erros, use-o e por que se importa com o que a Microsoft faz?
Zan Lynx
0

Após excluir o ponteiro, a memória para a qual ele aponta ainda pode ser válida. Para manifestar esse erro, o valor do ponteiro é definido como um valor óbvio. Isso realmente ajuda no processo de depuração. Se o valor tiver sido definido como NULL, ele pode nunca aparecer como um bug potencial no fluxo do programa. Portanto, pode ocultar um erro quando você testar mais tarde NULL.

Outro ponto é que algum otimizador de tempo de execução pode verificar esse valor e alterar seus resultados.

Em tempos anteriores, o MS definiu o valor como 0xcfffffff.

Karsten
fonte