Eu tenho o seguinte código.
#include <iostream>
int * foo()
{
int a = 5;
return &a;
}
int main()
{
int* p = foo();
std::cout << *p;
*p = 8;
std::cout << *p;
}
E o código está sendo executado sem exceções de tempo de execução!
A saída foi 58
Como pode ser? A memória de uma variável local não está inacessível fora de sua função?
c++
memory-management
local-variables
dangling-pointer
desconhecidos
fonte
fonte
address of local variable ‘a’ returned
; valgrind showsInvalid write of size 4 [...] Address 0xbefd7114 is just below the stack ptr
Respostas:
Você aluga um quarto de hotel. Você coloca um livro na gaveta superior da mesa de cabeceira e vai dormir. Você faz check-out na manhã seguinte, mas "esquece" de devolver sua chave. Você rouba a chave!
Uma semana depois, você volta ao hotel, não faz o check-in, entra no seu antigo quarto com a chave roubada e olha na gaveta. Seu livro ainda está lá. Surpreendente!
Como pode ser? O conteúdo de uma gaveta de quarto de hotel não está inacessível se você não alugou o quarto?
Bem, obviamente esse cenário pode acontecer no mundo real sem problemas. Não existe uma força misteriosa que faz com que seu livro desapareça quando você não está mais autorizado a estar na sala. Também não há uma força misteriosa que o impeça de entrar em uma sala com uma chave roubada.
A gerência do hotel não é obrigada a remover seu livro. Você não fez um contrato com eles, dizendo que, se você deixar as coisas para trás, elas irão rasgar para você. Se você entrar ilegalmente no seu quarto com uma chave roubada para recuperá-lo, a equipe de segurança do hotel não será obrigada a pegá-lo se escondendo. Você não fez um contrato com eles que dizia "se eu tentar entrar furtivamente no meu quarto" quarto mais tarde, você é obrigado a me parar. " Em vez disso, você assinou um contrato com eles que dizia "Prometo não voltar mais tarde para o meu quarto", um contrato que você quebrou .
Nesta situação, tudo pode acontecer . O livro pode estar lá - você teve sorte. O livro de outra pessoa pode estar lá e o seu pode estar no forno do hotel. Alguém pode estar lá quando você entra, rasgando seu livro em pedaços. O hotel poderia ter removido completamente a mesa e o livro e substituído por um guarda-roupa. Todo o hotel pode estar prestes a ser demolido e substituído por um estádio de futebol, e você morrerá em uma explosão enquanto estiver se esgueirando.
Você não sabe o que vai acontecer; quando você check-out do hotel e roubou uma chave para ilegalmente usar mais tarde, você deu-se o direito de viver em um mundo seguro previsível porque você escolheu para quebrar as regras do sistema.
C ++ não é uma linguagem segura . Ele alegremente permitirá que você quebre as regras do sistema. Se você tentar fazer algo ilegal e tolo, como voltar para uma sala em que não está autorizado a entrar e vasculhar uma mesa que talvez nem esteja mais lá, o C ++ não o impedirá. Linguagens mais seguras que o C ++ resolvem esse problema restringindo seu poder - tendo um controle muito mais rigoroso sobre as chaves, por exemplo.
ATUALIZAR
Santo Deus, esta resposta está recebendo muita atenção. (Não sei por que - considerei apenas uma pequena analogia "divertida", mas tanto faz.)
Eu pensei que poderia ser pertinente atualizar isso um pouco com mais alguns pensamentos técnicos.
Compiladores estão no negócio de gerar código que gerencia o armazenamento dos dados manipulados por esse programa. Existem várias maneiras diferentes de gerar código para gerenciar a memória, mas com o tempo duas técnicas básicas foram entrincheiradas.
O primeiro é ter algum tipo de área de armazenamento "de longa duração" em que a "vida útil" de cada byte no armazenamento - ou seja, o período em que ele é validamente associado a alguma variável de programa - não possa ser facilmente prevista com antecedência de tempo. O compilador gera chamadas para um "gerenciador de heap" que sabe como alocar dinamicamente o armazenamento quando necessário e recuperá-lo quando não for mais necessário.
O segundo método é ter uma área de armazenamento de "curta duração", onde a vida útil de cada byte é bem conhecida. Aqui, as vidas seguem um padrão de "aninhamento". A vida útil mais longa dessas variáveis de vida curta será alocada antes de qualquer outra variável de vida curta e será liberada por último. Variáveis de vida mais curta serão alocadas após as de vida mais longa e serão liberadas antes delas. O tempo de vida dessas variáveis de vida mais curta é "aninhado" dentro da vida das variáveis de vida mais longa.
Variáveis locais seguem o último padrão; Quando um método é inserido, suas variáveis locais ganham vida. Quando esse método chama outro método, as variáveis locais do novo método ganham vida. Eles estarão mortos antes que as variáveis locais do primeiro método estejam mortas. A ordem relativa do início e do fim da vida útil dos armazenamentos associados às variáveis locais pode ser calculada com antecedência.
Por esse motivo, as variáveis locais geralmente são geradas como armazenamento em uma estrutura de dados de "pilha", porque uma pilha tem a propriedade que a primeira coisa que for pressionada será a última que foi retirada.
É como se o hotel decidisse alugar apenas os quartos sequencialmente, e você não poderá fazer check-out até que todos com um número de quarto maior do que o seu.
Então, vamos pensar sobre a pilha. Em muitos sistemas operacionais, você obtém uma pilha por encadeamento e a pilha é alocada para ter um determinado tamanho fixo. Quando você chama um método, as coisas são colocadas na pilha. Se você passar um ponteiro para a pilha de volta do seu método, como o pôster original faz aqui, isso é apenas um ponteiro para o meio de algum bloco de memória de milhões de bytes totalmente válido. Em nossa analogia, você faz check-out do hotel; quando fizer isso, você acabou de sair da sala ocupada com o número mais alto. Se ninguém mais fizer o check-in depois de você e você voltar ilegalmente ao seu quarto, todas as suas coisas ainda estarão garantidas nesse hotel em particular .
Usamos pilhas para lojas temporárias porque são realmente baratas e fáceis. Uma implementação do C ++ não é necessária para usar uma pilha para armazenamento de locais; poderia usar a pilha. Não, porque isso tornaria o programa mais lento.
Uma implementação do C ++ não é necessária para deixar o lixo que você deixou na pilha intocado, para que você possa retornar posteriormente ilegalmente; é perfeitamente legal para o compilador gerar código que volta a zero tudo na "sala" que você acabou de desocupar. Não é porque, novamente, isso seria caro.
Uma implementação do C ++ não é necessária para garantir que, quando a pilha diminui logicamente, os endereços que costumavam ser válidos ainda sejam mapeados na memória. A implementação está autorizada a dizer ao sistema operacional "terminamos de usar esta página da pilha agora. Até que eu diga o contrário, emita uma exceção que destrói o processo se alguém tocar na página da pilha anteriormente válida". Novamente, as implementações não fazem isso porque são lentas e desnecessárias.
Em vez disso, as implementações permitem que você cometa erros e se livre dele. A maior parte do tempo. Até que um dia algo realmente terrível dê errado e o processo exploda.
Isso é problemático. Existem muitas regras e é muito fácil quebrá-las acidentalmente. Eu certamente tenho muitas vezes. E pior, o problema geralmente surge quando a memória é detectada como bilhões de nanossegundos corrompidos após a corrupção, quando é muito difícil descobrir quem estragou tudo.
Idiomas mais seguros para a memória resolvem esse problema restringindo seu poder. No C # "normal", simplesmente não há como pegar o endereço de um local e devolvê-lo ou armazená-lo para mais tarde. Você pode usar o endereço de um local, mas o idioma foi projetado de maneira inteligente, para que seja impossível usá-lo após o término da vida útil do local. Para pegar o endereço de um local e devolvê-lo, é necessário colocar o compilador em um modo "não seguro" especial e colocar a palavra "não seguro" em seu programa, para chamar a atenção para o fato de que você provavelmente está fazendo algo perigoso que poderia estar violando as regras.
Para leitura adicional:
E se o C # permitisse retornar referências? Coincidentemente, esse é o assunto da postagem de hoje:
https://ericlippert.com/2011/06/23/ref-returns-and-ref-locals/
Por que usamos pilhas para gerenciar a memória? Os tipos de valor em C # sempre são armazenados na pilha? Como a memória virtual funciona? E muitos mais tópicos sobre como o gerenciador de memória C # funciona. Muitos desses artigos também são pertinentes aos programadores de C ++:
https://ericlippert.com/tag/memory-management/
fonte
O que você está fazendo aqui é simplesmente ler e gravar na memória que costumava ser o endereço de
a
. Agora que você está forafoo
, é apenas um ponteiro para alguma área de memória aleatória. Acontece que no seu exemplo, essa área de memória existe e nada mais a está usando no momento. Você não quebra nada continuando a usá-lo, e nada mais o substituiu ainda. Portanto, o5
ainda está lá. Em um programa real, essa memória seria reutilizada quase imediatamente e você quebraria alguma coisa fazendo isso (embora os sintomas possam não aparecer até muito mais tarde!)Ao retornar
foo
, você diz ao sistema operacional que não está mais usando essa memória e pode ser transferida para outra coisa. Se você tiver sorte e ele nunca for reatribuído, e o sistema operacional não o pegar novamente, então você se safará da mentira. As chances são de que você acabe escrevendo sobre o que mais acabar com esse endereço.Agora, se você está se perguntando por que o compilador não reclama, provavelmente é porque
foo
foi eliminado pela otimização. Normalmente, ele avisa sobre esse tipo de coisa. C supõe que você saiba o que está fazendo e, tecnicamente, não violou o escopo aqui (não há referência aa
si mesmo fora defoo
), apenas regras de acesso à memória, que apenas acionam um aviso em vez de um erro.Em resumo: isso geralmente não funciona, mas às vezes funciona por acaso.
fonte
Porque o espaço de armazenamento ainda não estava pisado. Não conte com esse comportamento.
fonte
Uma pequena adição a todas as respostas:
se você fizer algo assim:
a saída provavelmente será: 7
Isso ocorre porque, após retornar de foo (), a pilha é liberada e reutilizada pelo boo (). Se você desmontar o executável, verá claramente.
fonte
boo
reutiliza afoo
pilha? não são pilhas de função separados uns dos outros, também eu recebo lixo executar este código no Visual Studio 2015foo()
, existe e desceboo()
.Foo()
eBoo()
ambos entram com o ponteiro da pilha no mesmo local. No entanto, esse não é um comportamento que deve ser considerado. Outras 'coisas' (como interrupções, ou o sistema operacional) pode usar a pilha entre a chamada deboo()
efoo()
, modificando o seu conteúdo ...No C ++, você pode acessar qualquer endereço, mas isso não significa que você deva . O endereço que você está acessando não é mais válido. Ele funciona porque nada mais mexidos a memória após foo retornou, mas poderia falhar em muitas circunstâncias. Tente analisar seu programa com Valgrind , ou até mesmo compilá-lo otimizado, e veja ...
fonte
Você nunca lança uma exceção C ++ acessando memória inválida. Você está apenas dando um exemplo da idéia geral de referenciar um local de memória arbitrário. Eu poderia fazer o mesmo assim:
Aqui, estou simplesmente tratando 123456 como o endereço de um duplo e escrevendo nele. Qualquer coisa pode acontecer:
q
pode realmente ser um endereço válido de um duplo, por exemplodouble p; q = &p;
.q
pode apontar para algum lugar dentro da memória alocada e eu apenas sobrescrito 8 bytes lá.q
aponta para fora da memória alocada e o gerenciador de memória do sistema operacional envia um sinal de falha de segmentação ao meu programa, fazendo com que o tempo de execução o encerre.Como você o configura, é um pouco mais razoável que o endereço retornado aponte para uma área válida da memória, pois provavelmente estará um pouco mais abaixo na pilha, mas ainda é um local inválido que você não pode acessar em um moda determinista.
Ninguém verificará automaticamente a validade semântica de endereços de memória como esse durante a execução normal do programa. No entanto, como um depurador de memória
valgrind
fará isso com prazer, você deve executar o programa através dele e testemunhar os erros.fonte
4) I win the lottery
Você compilou seu programa com o otimizador ativado? A
foo()
função é bastante simples e pode ter sido incorporada ou substituída no código resultante.Mas eu concordo com Mark B que o comportamento resultante é indefinido.
fonte
5
alteração será feita ...Seu problema não tem nada a ver com o escopo . No código que você mostra, a função
main
não vê os nomes na funçãofoo
, então você não pode acessara
diretamente foo com esse nome forafoo
.O problema que você está tendo é por que o programa não sinaliza um erro ao fazer referência a memória ilegal. Isso ocorre porque os padrões C ++ não especificam um limite muito claro entre memória ilegal e memória legal. Fazer referência a algo na pilha exibida às vezes causa erro e às vezes não. Depende. Não conte com esse comportamento. Suponha que sempre resultará em erro ao programar, mas assuma que nunca sinalizará erro ao depurar.
fonte
Você está apenas retornando um endereço de memória, é permitido, mas provavelmente um erro.
Sim, se você tentar desreferenciar esse endereço de memória, terá um comportamento indefinido.
fonte
cout
.*a
aponta para a memória não alocada (liberada). Mesmo que você não faça isso, ainda é perigoso (e provavelmente falso).Esse é um comportamento indefinido clássico que foi discutido aqui há dois dias - pesquise um pouco no site. Em poucas palavras, você teve sorte, mas tudo poderia ter acontecido e seu código está fazendo acesso inválido à memória.
fonte
Esse comportamento é indefinido, como Alex apontou - na verdade, a maioria dos compiladores alertará contra isso, porque é uma maneira fácil de obter falhas.
Para um exemplo do tipo de comportamento assustador que você provavelmente obtém, tente este exemplo:
Isso exibe "y = 123", mas seus resultados podem variar (realmente!). Seu ponteiro está incomodando outras variáveis locais não relacionadas.
fonte
Preste atenção a todos os avisos. Não apenas resolva erros.
O GCC mostra este aviso
Este é o poder do C ++. Você deve se preocupar com a memória. Com o
-Werror
sinalizador, esse aviso se torna um erro e agora você precisa depurá-lo.fonte
Funciona porque a pilha não foi alterada (ainda) desde que a foi colocada lá. Chame algumas outras funções (que também estão chamando outras funções) antes de acessar
a
novamente e você provavelmente não terá mais tanta sorte ... ;-)fonte
Você realmente invocou um comportamento indefinido.
Retornando o endereço de uma obra temporária, mas como os temporários são destruídos no final de uma função, os resultados do acesso a elas serão indefinidos.
Então você não modificou,
a
mas a localização da memória ondea
antes estava. Essa diferença é muito semelhante à diferença entre travamento e não travamento.fonte
Nas implementações típicas do compilador, você pode pensar no código como "imprima o valor do bloco de memória com o endereço que costumava ser ocupado por um". Além disso, se você adicionar uma nova chamada de função a uma função que consista em um local
int
, é uma boa chance de o valora
(ou o endereço de memóriaa
usado para apontar) mudar. Isso acontece porque a pilha será substituída por um novo quadro contendo dados diferentes.No entanto, esse é um comportamento indefinido e você não deve confiar nele para funcionar!
fonte
a
, o ponteiro mantinha o endereço dea
. Embora a Norma não exija que as implementações definam o comportamento dos endereços após o término da vida útil de seu destino, também reconhece que em algumas plataformas o UB é processado de maneira documentada, característica do ambiente. Embora o endereço de uma variável local geralmente não seja de muita utilidade depois de sair do escopo, alguns outros tipos de endereços ainda podem ser significativos após a vida útil de seus respectivos destinos.realloc
comparar um ponteiro passado com o valor de retorno, nem permitir que ponteiros para endereços dentro do bloco antigo sejam ajustados para apontar para o novo, algumas implementações o fazem , e o código que explora esse recurso pode ser mais eficiente do que o código que tem que evitar qualquer ação - até comparações - envolvendo ponteiros para a alocação atribuídarealloc
.Pode, porque
a
é uma variável alocada temporariamente pelo tempo de vida de seu escopo (foo
função). Depois que você retornar,foo
a memória fica livre e pode ser substituída.O que você está fazendo é descrito como comportamento indefinido . O resultado não pode ser previsto.
fonte
As coisas com a saída correta do console (?) Podem mudar drasticamente se você usar :: printf, mas não cout. Você pode brincar com o depurador no código abaixo (testado em x86, 32 bits, MSVisual Studio):
fonte
Depois de retornar de uma função, todos os identificadores são destruídos em vez dos valores mantidos em um local de memória e não podemos localizar os valores sem ter um identificador. Mas esse local ainda contém o valor armazenado pela função anterior.
Portanto, aqui a função
foo()
está retornando o endereço dea
ea
é destruída após retornar seu endereço. E você pode acessar o valor modificado através desse endereço retornado.Deixe-me dar um exemplo do mundo real:
Suponha que um homem esconda dinheiro em um local e informe a localização. Depois de algum tempo, o homem que havia lhe dito a localização do dinheiro morre. Mas você ainda tem acesso a esse dinheiro oculto.
fonte
É uma maneira 'suja' de usar endereços de memória. Quando você retorna um endereço (ponteiro), não sabe se ele pertence ao escopo local de uma função. É apenas um endereço. Agora que você chamou a função 'foo', esse endereço (local da memória) de 'a' já estava alocado na memória endereçável (com segurança, pelo menos por enquanto) do seu aplicativo (processo). Depois que a função 'foo' retorna, o endereço de 'a' pode ser considerado 'sujo', mas está lá, não é limpo, nem é perturbado / modificado por expressões em outra parte do programa (neste caso específico, pelo menos). O compilador AC / C ++ não impede você de acesso 'sujo' (pode avisá-lo, se você se importa).
fonte
Seu código é muito arriscado. Você está criando uma variável local (que é considerada destruída após o término da função) e retorna o endereço de memória dessa variável depois que ela é destruída.
Isso significa que o endereço de memória pode ser válido ou não e seu código estará vulnerável a possíveis problemas de endereço de memória (por exemplo, falha de segmentação).
Isso significa que você está fazendo uma coisa muito ruim, porque está passando um endereço de memória para um ponteiro que não é confiável.
Considere este exemplo e teste-o:
Ao contrário do seu exemplo, com este exemplo você é:
fonte
new
.new
. Você está ensinando-os a usarnew
. Mas você não deve usarnew
.new
em 2019 (a menos que esteja escrevendo código de biblioteca) e também não ensine os novatos a fazer isso! Felicidades.