A memória de uma variável local pode ser acessada fora de seu escopo?

1029

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?

desconhecidos
fonte
14
isso nem será compilado como está; se você corrigir os negócios não-formados, o gcc ainda avisará address of local variable ‘a’ returned; valgrind showsInvalid write of size 4 [...] Address 0xbefd7114 is just below the stack ptr
sehe
76
@Serge: Quando eu era jovem, trabalhei em algum código complicado de anel zero, executado no sistema operacional Netware, que envolvia a movimentação inteligente do ponteiro da pilha de uma maneira que não era exatamente sancionada pelo sistema operacional. Eu saberia quando cometi um erro, porque muitas vezes a pilha acabava sobrepondo-se à memória da tela e eu podia ver os bytes sendo escritos diretamente no visor. Você não pode se safar desse tipo de coisa hoje em dia.
Eric Lippert
23
ri muito. Eu precisava ler a pergunta e algumas respostas antes mesmo de entender onde está o problema. Isso é realmente uma pergunta sobre o escopo de acesso da variável? Você nem usa 'a' fora de sua função. E isso é tudo o que há para isso. Jogar algumas referências de memória é um tópico totalmente diferente do escopo variável.
21711 Erikbwork
10
Resposta idiota não significa pergunta idiota. Muitas das perguntas duvidosas que as pessoas propuseram aqui são perguntas completamente diferentes que se referem ao mesmo sintoma subjacente ... mas o questionador sabe como saber que elas devem permanecer abertas. Fechei um idiota mais velho e o fundei nessa questão, que deve permanecer aberta porque tem uma resposta muito boa.
Joel Spolsky
16
@ Joel: Se a resposta aqui for boa, ela deve ser mesclada com perguntas mais antigas , das quais isso é uma bobagem, e não o contrário. E esta questão é de fato um embuste das outras questões propostas aqui e depois algumas (embora algumas das propostas sejam mais adequadas que outras). Note que acho que a resposta de Eric é boa. (Na verdade, eu sinalizado esta pergunta para fundir as respostas para uma das questões mais velhos, a fim de salvar as perguntas mais velhos.)
SBI

Respostas:

4801

Como pode ser? A memória de uma variável local não está inacessível fora de sua função?

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:

Eric Lippert
fonte
56
@muntoo: Infelizmente, não é como se o sistema operacional soasse uma sirene de aviso antes de encerrar ou desalocar uma página de memória virtual. Se você está mexendo com essa memória quando não a possui mais, o sistema operacional está perfeitamente dentro de seus direitos de interromper todo o processo ao tocar em uma página desalocada. Estrondo!
precisa
83
@Kyle: Apenas hotéis seguros fazem isso. Os hotéis inseguros obtêm ganhos de lucro mensuráveis ​​por não perder tempo com as chaves de programação.
Alexander Torstling
498
@cyberguijarro: Que o C ++ não é seguro para a memória é simplesmente um fato. Não está "atacando" nada. Se eu tivesse dito, por exemplo, "C ++ é uma mistura horrível de recursos subespecíficos e excessivamente complexos empilhados sobre um modelo de memória frágil e perigoso e sou grato todos os dias por não trabalhar mais nele por minha própria sanidade", isso bashing C ++. Salientar que não é seguro para a memória está explicando por que o pôster original está vendo esse problema; está respondendo à pergunta, não editorializando.
Eric Lippert
50
A rigor, a analogia deve mencionar que a recepcionista do hotel ficou muito feliz por você levar a chave com você. "Oh, você se importa se eu levar essa chave comigo?" "Vá em frente. Por que eu me importaria? Eu só trabalho aqui". Não se torna ilegal até você tentar usá-lo.
Phsquared
140
Por favor, pelo menos considere escrever um livro um dia. Eu o compraria mesmo que fosse apenas uma coleção de postagens de blog revisadas e expandidas, e tenho certeza que muitas pessoas também. Mas um livro com suas idéias originais sobre vários assuntos relacionados à programação seria uma ótima leitura. Sei que é incrivelmente difícil encontrar tempo para isso, mas considere escrever um.
Dyppl
276

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á fora foo, é 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, o 5ainda 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 foofoi 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 a asi mesmo fora de foo), 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.

Rena
fonte
152

Porque o espaço de armazenamento ainda não estava pisado. Não conte com esse comportamento.

msw
fonte
1
Cara, essa foi a espera mais longa por um comentário desde então: "O que é verdade? Disse brincando Pilatos". Talvez fosse a Bíblia de Gideon naquela gaveta do hotel. E o que aconteceu com eles, afinal? Observe que eles não estão mais presentes, pelo menos em Londres. Acho que, de acordo com a legislação sobre Igualdades, você precisaria de uma biblioteca de folhetos religiosos.
Rob Kent
Eu poderia jurar que escrevi há muito tempo, mas apareceu recentemente e descobriu que minha resposta não estava lá. Agora eu tenho que descobrir suas alusões acima, pois espero que eu me divirta quando eu fizer>. <
msw
1
Haha Francis Bacon, um dos maiores ensaístas da Grã-Bretanha, a quem algumas pessoas suspeitam ter escrito as peças de Shakespeare, porque não pode aceitar que um garoto da escola de gramática do país, filho de um glover, possa ser um gênio. Esse é o sistema de aulas de inglês. Jesus disse: 'Eu sou a verdade'. oregonstate.edu/instruct/phl302/texts/bacon/bacon_essays.html
Rob Kent
84

Uma pequena adição a todas as respostas:

se você fizer algo assim:

#include<stdio.h>
#include <stdlib.h>
int * foo(){
    int a = 5;
    return &a;
}
void boo(){
    int a = 7;

}
int main(){
    int * p = foo();
    boo();
    printf("%d\n",*p);
}

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.

Michael
fonte
2
Exemplo simples, mas excelente, para entender a teoria subjacente da pilha. Apenas uma adição de teste, declarando "int a = 5;" em foo () como "estático int a = 5;" pode ser usado para entender o escopo e o tempo de vida de uma variável estática.
controle
15
-1 "para provavelmente será 7 ". O compilador pode registrar um in boo. Pode removê-lo porque é desnecessário. Há uma boa chance de que * p não seja 5 , mas isso não significa que haja uma razão particularmente boa para provavelmente ser 7 .
Matt
2
Isso é chamado de comportamento indefinido!
Francis Cugler 27/03
por que e como booreutiliza a foopilha? não são pilhas de função separados uns dos outros, também eu recebo lixo executar este código no Visual Studio 2015
ampawd
1
@ampawd tem quase um ano, mas não, as "pilhas de funções" não são separadas uma da outra. Um CONTEXTO tem uma pilha. Esse contexto usa sua pilha para inserir main, depois desce foo(), existe e desce boo(). Foo()e Boo()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 de boo()e foo(), modificando o seu conteúdo ...
Russ Schultz
72

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 ...

Charles Brunet
fonte
5
Você provavelmente quer dizer que pode tentar acessar qualquer endereço. Porque a maioria dos sistemas operacionais hoje não permite que nenhum programa acesse nenhum endereço; existem toneladas de salvaguardas para proteger o espaço de endereço. É por isso que não haverá outro LOADLIN.EXE por aí.
v010dya
67

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:

unsigned int q = 123456;

*(double*)(q) = 1.2;

Aqui, estou simplesmente tratando 123456 como o endereço de um duplo e escrevendo nele. Qualquer coisa pode acontecer:

  1. qpode realmente ser um endereço válido de um duplo, por exemplo double p; q = &p;.
  2. q pode apontar para algum lugar dentro da memória alocada e eu apenas sobrescrito 8 bytes lá.
  3. 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.
  4. Você ganha na loteria.

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 valgrindfará isso com prazer, você deve executar o programa através dele e testemunhar os erros.

Kerrek SB
fonte
9
Eu só vou escrever um programa agora que continua a execução deste programa de modo que4) I win the lottery
Aidiakapi
29

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.

gastush
fonte
Essa é a minha aposta. O otimizador descartou a chamada de função.
Erik Aronesty
9
Isso não é necessário. Como nenhuma nova função é chamada após foo (), o quadro de pilha local das funções simplesmente ainda não foi sobrescrito. Adicione outra invocação de função após foo (), e a 5alteração será feita ...
Tomas
Executei o programa com o GCC 4.8, substituindo cout por printf (e incluindo o stdio). Avisa com razão "aviso: o endereço da variável local 'a' retornou [-Wreturn-local-addr]". Saídas 58 sem otimização e 08 com -O3. Estranhamente, P possui um endereço, mesmo que seu valor seja 0. Eu esperava NULL (0) como endereço.
Kevinf #
23

Seu problema não tem nada a ver com o escopo . No código que você mostra, a função mainnão vê os nomes na função foo, então você não pode acessar adiretamente foo com esse nome fora foo.

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.

Chang Peng
fonte
Lembro-me de uma cópia antiga do Turbo C Programming para a IBM , com a qual costumava brincar quando, como manipular diretamente a memória gráfica e o layout da memória de vídeo em modo texto da IBM, foi descrito em detalhes. Obviamente, o sistema em que o código funcionava definia claramente o que significava escrever para esses endereços, desde que você não se preocupasse com a portabilidade para outros sistemas, tudo estava bem. IIRC, indicadores para anular eram um tema comum nesse livro.
a CVn
@ Michael Kjörling: Claro! As pessoas gostam de fazer algum trabalho sujo de vez em quando;)
Chang Peng
18

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.

int * ref () {

 int tmp = 100;
 return &tmp;
}

int main () {

 int * a = ref();
 //Up until this point there is defined results
 //You can even print the address returned
 // but yes probably a bug

 cout << *a << endl;//Undefined results
}
Brian R. Bondy
fonte
Eu discordo: há um problema antes da cout. *aaponta para a memória não alocada (liberada). Mesmo que você não faça isso, ainda é perigoso (e provavelmente falso).
ereOn
@OnOn: Esclareci mais o que eu quis dizer com problema, mas não, não é perigoso em termos de código c ++ válido. Mas é perigoso em termos de provável erro do usuário e fará algo ruim. Talvez, por exemplo, você esteja tentando ver como a pilha cresce e se preocupa apenas com o valor do endereço e nunca o desreferirá.
Brian R. Bondy
18

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.

Kerrek SB
fonte
18

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:

int *a()
{
   int x = 5;
   return &x;
}

void b( int *c )
{
   int y = 29;
   *c = 123;
   cout << "y=" << y << endl;
}

int main()
{
   b( a() );
   return 0;
}

Isso exibe "y = 123", mas seus resultados podem variar (realmente!). Seu ponteiro está incomodando outras variáveis ​​locais não relacionadas.

AHelps
fonte
18

Preste atenção a todos os avisos. Não apenas resolva erros.
O GCC mostra este aviso

aviso: endereço da variável local 'a' retornado

Este é o poder do C ++. Você deve se preocupar com a memória. Com o -Werrorsinalizador, esse aviso se torna um erro e agora você precisa depurá-lo.

sam
fonte
17

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 anovamente e você provavelmente não terá mais tanta sorte ... ;-)

Adrian Grigore
fonte
16

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, amas a localização da memória onde aantes estava. Essa diferença é muito semelhante à diferença entre travamento e não travamento.

Alexander Gessler
fonte
14

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 valor a(ou o endereço de memória ausado 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!

Larsmoa
fonte
3
"imprima o valor do bloco de memória com o endereço que costumava ser ocupado por um" não está certo. Isso faz parecer que seu código tem algum significado bem definido, o que não é o caso. Você está certo que provavelmente é assim que a maioria dos compiladores o implementaria.
Brennan Vincent
@BrennanVincent: Enquanto o armazenamento estava ocupado a, o ponteiro mantinha o endereço de a. 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.
Supercat 26/06
@BrennanVincent: Por exemplo, embora a Norma possa não exigir que as implementações permitam realloccomparar 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ída realloc.
Supercat 26/06
14

Pode, porque aé uma variável alocada temporariamente pelo tempo de vida de seu escopo ( foofunção). Depois que você retornar, fooa 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.

littleadv
fonte
12

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):

char* foo() 
{
  char buf[10];
  ::strcpy(buf, "TEST”);
  return buf;
}

int main() 
{
  char* s = foo();    //place breakpoint & check 's' varialbe here
  ::printf("%s\n", s); 
}
Mykola
fonte
5

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 de ae aé 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.

Ghulam Moinul Quadir
fonte
4

É 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).

Ayub
fonte
1

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:

int * foo()
{
   int *x = new int;
   *x = 5;
   return x;
}

int main()
{
    int* p = foo();
    std::cout << *p << "\n"; //better to put a new-line in the output, IMO
    *p = 8;
    std::cout << *p;
    delete p;
    return 0;
}

Ao contrário do seu exemplo, com este exemplo você é:

  • alocando memória para int em uma função local
  • esse endereço de memória ainda é válido também quando a função expira (não é excluída por ninguém)
  • o endereço da memória é confiável (esse bloco de memória não é considerado livre, portanto, não será substituído até que seja excluído)
  • o endereço de memória deve ser excluído quando não for usado. (veja a exclusão no final do programa)
Nobun
fonte
Você adicionou algo ainda não coberto pelas respostas existentes? E por favor não use ponteiros brutos / new.
Lightness Races em órbita
1
O autor da pergunta usou ponteiros brutos. Eu fiz um exemplo que refletia exatamente o exemplo que ele fez para permitir que ele visse a diferença entre ponteiro não confiável e confiável. Na verdade, há outra resposta semelhante à minha, mas ela usa strcpy que, IMHO, poderia ser menos claro para um codificador iniciante do que o meu exemplo que usa new.
Nobun
Eles não usaram new. Você está ensinando-os a usar new. Mas você não deve usar new.
Lightness Races em órbita
Então, na sua opinião, é melhor passar um endereço para uma variável local que é destruída em uma função do que realmente alocar memória? Isso não faz sentido. É importante entender o conceito de alocar e desalocar memória, principalmente se você estiver perguntando sobre ponteiros (o solicitante não usou ponteiros novos, mas usou ponteiros).
Nobun
Quando eu disse isso? Não, é melhor usar ponteiros inteligentes para indicar adequadamente a propriedade do recurso referenciado. Não use newem 2019 (a menos que esteja escrevendo código de biblioteca) e também não ensine os novatos a fazer isso! Felicidades.
Lightness Races em órbita