Em C, chaves funcionam como um quadro de pilha?

153

Se eu criar uma variável dentro de um novo conjunto de chaves, essa variável saiu da pilha na chave de fechamento ou permanece até o final da função? Por exemplo:

void foo() {
   int c[100];
   {
       int d[200];
   }
   //code that takes a while
   return;
}

Vai docupar memória durante a code that takes a whileseção?

Claudiu
fonte
8
Você quer dizer (1) de acordo com o Padrão, (2) prática universal entre implementações ou (3) prática comum entre implementações?
David Thornley

Respostas:

83

Não, chaves não funcionam como um quadro de pilha. Em C, chaves apenas indicam um escopo de nomenclatura, mas nada é destruído e nada sai da pilha quando o controle passa dela.

Como um programador que escreve código, você pode pensar nele como se fosse um quadro de pilha. Os identificadores declarados entre chaves são acessíveis apenas entre chaves, portanto, do ponto de vista de um programador, é como se eles fossem empurrados para a pilha conforme declarados e, então, apareciam quando o escopo era encerrado. No entanto, os compiladores não precisam gerar código que empurra / abre alguma coisa na entrada / saída (e geralmente não).

Observe também que as variáveis ​​locais podem não usar nenhum espaço de pilha: elas podem ser mantidas nos registros da CPU ou em algum outro local de armazenamento auxiliar ou ser totalmente otimizadas.

Assim, o darray, em teoria, poderia consumir memória para toda a função. No entanto, o compilador pode otimizá-lo ou compartilhar sua memória com outras variáveis ​​locais cujas vidas úteis não se sobrepõem.

Kristopher Johnson
fonte
9
Isso não é específico da implementação?
Avakar
54
No C ++, o destruidor de um objeto é chamado no final de seu escopo. Se a memória é recuperada é um problema específico da implementação.
Kristopher Johnson
8
@ pm100: Os destruidores serão chamados. Isso não diz nada sobre a memória que esses objetos ocupavam.
Donal Fellows
9
O padrão C especifica que a vida útil das variáveis ​​automáticas declaradas no bloco se estende apenas até o término da execução do bloco. Então, basicamente essas variáveis automáticas que se "destruído" no final do bloco.
Caf
3
@KristopherJohnson: Se um método tivesse dois blocos separados, cada um declarando uma matriz de 1 KB e um terceiro bloco que chamasse um método aninhado, um compilador estaria livre para usar a mesma memória para ambas as matrizes e / ou para colocar a matriz na parte mais rasa da pilha e mova o ponteiro da pilha acima dele, chamando o método aninhado. Esse comportamento pode reduzir em 2K a profundidade da pilha necessária para a chamada de função.
supercat
39

O tempo durante o qual a variável está realmente ocupando memória é obviamente dependente do compilador (e muitos compiladores não ajustam o ponteiro da pilha quando os blocos internos são inseridos e encerrados nas funções).

No entanto, uma questão intimamente relacionada, mas possivelmente mais interessante, é se o programa tem permissão para acessar esse objeto interno fora do escopo interno (mas dentro da função que o contém), ou seja:

void foo() {
   int c[100];
   int *p;

   {
       int d[200];
       p = d;
   }

   /* Can I access p[0] here? */

   return;
}

(Em outras palavras: o compilador pode desalocar d, mesmo que na prática a maioria não o faça?).

A resposta é que o compilador é permitido retirar a atribuição d, e acessar p[0]onde o comentário indica é um comportamento indefinido (o programa é não permitido o acesso do exterior objeto interno do âmbito interno). A parte relevante do padrão C é 6.2.4p5:

Para um objeto desse tipo [aquele que possui duração de armazenamento automática] que não possui um tipo de matriz de comprimento variável, sua vida útil se estende desde a entrada no bloco ao qual está associado até a execução desse bloco terminar de alguma maneira . (A inserção de um bloco fechado ou a chamada de uma função suspende, mas não termina, a execução do bloco atual.) Se o bloco for inserido recursivamente, uma nova instância do objeto será criada a cada vez. O valor inicial do objeto é indeterminado. Se uma inicialização for especificada para o objeto, ela será executada sempre que a declaração for atingida na execução do bloco; caso contrário, o valor se tornará indeterminado toda vez que a declaração for atingida.

caf
fonte
Como alguém que aprende como o escopo e a memória funcionam em C e C ++ após anos de uso de linguagens de nível superior, acho essa resposta mais precisa e útil do que a aceita.
10248 Chris
20

Sua pergunta não é clara o suficiente para ser respondida sem ambiguidade.

Por um lado, os compiladores normalmente não fazem nenhuma alocação-desalocação de memória local para escopos de blocos aninhados. A memória local é normalmente alocada apenas uma vez na entrada da função e liberada na saída da função.

Por outro lado, quando a vida útil de um objeto local termina, a memória ocupada por esse objeto pode ser reutilizada posteriormente para outro objeto local. Por exemplo, neste código

void foo()
{
  {
    int d[100];
  }
  {
    double e[20];
  }
}

as duas matrizes geralmente ocupam a mesma área de memória, o que significa que a quantidade total de armazenamento local necessária para a função fooé o que for necessário para a maior das duas matrizes, não para as duas ao mesmo tempo.

Se este último se qualifica como dcontinuando a ocupar memória até o final da função no contexto de sua pergunta, é sua decisão.

Formiga
fonte
6

É dependente da implementação. Eu escrevi um programa curto para testar o que o gcc 4.3.4 faz e aloca todo o espaço da pilha de uma só vez no início da função. Você pode examinar a montagem que o gcc produz usando o sinalizador -S.

Daniel Stutzbach
fonte
3

Não, d [] não estará na pilha pelo restante da rotina. Mas alloca () é diferente.

Edit: Kristopher Johnson (e Simon e Daniel) estão certos , e minha resposta inicial estava errada . Com o gcc 4.3.4.em CYGWIN, o código:

void foo(int[]);
void bar(void);
void foobar(int); 

void foobar(int flag) {
    if (flag) {
        int big[100000000];
        foo(big);
    }
    bar();
}

dá:

_foobar:
    pushl   %ebp
    movl    %esp, %ebp
    movl    $400000008, %eax
    call    __alloca
    cmpl    $0, 8(%ebp)
    je      L2
    leal    -400000000(%ebp), %eax
    movl    %eax, (%esp)
    call    _foo
L2:
    call    _bar
    leave
    ret

Viva e aprenda! E um teste rápido parece mostrar que o AndreyT também está correto sobre várias alocações.

Adicionado muito mais tarde : O teste acima mostra que a documentação do gcc não está correta. Durante anos, ele disse (grifo nosso):

"O espaço para uma matriz de comprimento variável é desalocado assim que o escopo do nome da matriz termina ."

Joseph Quinsey
fonte
Compilar com a otimização desativada não mostra necessariamente o que você obterá no código otimizado. Nesse caso, o comportamento é o mesmo (alocado no início da função e somente gratuito ao sair da função): godbolt.org/g/M112AQ . Mas o gcc não cygwin não chama uma allocafunção. Estou realmente surpreso que o cygwin gcc faria isso. Não é nem mesmo uma matriz de comprimento variável, então IDK por que você traz isso à tona.
Peter Cordes
2

Eles podem. Eles podem não. A resposta que você realmente precisa é: nunca assuma nada. Compiladores modernos executam todos os tipos de arquitetura e mágica específica de implementação. Escreva seu código de forma simples e legível para humanos e deixe o compilador fazer as coisas boas. Se você tentar codificar em torno do compilador, precisará de problemas - e o problema que geralmente ocorre nessas situações geralmente é terrivelmente sutil e difícil de diagnosticar.

user19666
fonte
1

Sua variável dnormalmente não é removida da pilha. Chaves entre dentes não indicam um quadro de pilha. Caso contrário, você não seria capaz de fazer algo assim:

char var = getch();
    {
        char next_var = var + 1;
        use_variable(next_char);
    }

Se chaves entre chaves causassem um push / pop verdadeiro da pilha (como faria uma chamada de função), o código acima não seria compilado porque o código dentro das chaves não seria capaz de acessar a variável varque vive fora das chaves (como uma sub- A função não pode acessar diretamente variáveis ​​na função de chamada). Sabemos que esse não é o caso.

Aparelhos encaracolados são simplesmente usados ​​para escopo. O compilador tratará qualquer acesso à variável "interna" de fora dos colchetes como inválido e poderá reutilizar essa memória para outra coisa (isso depende da implementação). No entanto, ele não pode ser retirado da pilha até que a função de fechamento retorne.

Atualização: Aqui está o que a especificação C tem a dizer. Em relação a objetos com duração de armazenamento automático (seção 6.4.2):

Para um objeto que não possui um tipo de matriz de comprimento variável, sua vida útil se estende desde a entrada no bloco ao qual está associado até a execução desse bloco terminar de qualquer maneira.

A mesma seção define o termo "tempo de vida" como (ênfase minha):

A vida útil de um objeto é a parte da execução do programa durante a qual o armazenamento é garantido como reservado para ele. Um objeto existe, tem um endereço constante e mantém seu último valor armazenado ao longo de sua vida útil. Se um objeto for referido fora de sua vida útil, o comportamento será indefinido.

A palavra-chave aqui é, obviamente, 'garantida'. Depois que você sai do escopo do conjunto interno de chaves, a vida útil da matriz termina. O armazenamento pode ou não ser ainda alocado para ele (seu compilador pode reutilizar o espaço para outra coisa), mas qualquer tentativa de acessar a matriz invoca um comportamento indefinido e produz resultados imprevisíveis.

A especificação C não tem noção de quadros de pilha. Ele fala apenas de como o programa resultante se comportará e deixa os detalhes da implementação para o compilador (afinal, a implementação seria bem diferente em uma CPU sem pilha do que em uma CPU com uma pilha de hardware). Não há nada na especificação C que determine onde um quadro de pilha terminará ou não. A única maneira real de saber é compilar o código no seu compilador / plataforma específico e examinar o assembly resultante. O conjunto atual de opções de otimização do seu compilador provavelmente também desempenhará um papel nisso.

Se você deseja garantir que a matriz dnão consuma mais memória enquanto o código está em execução, é possível converter o código entre chaves em uma função separada ou explicitamente malloce freea memória em vez de usar o armazenamento automático.

bta
fonte
1
"Se chaves entre chaves causassem um push / pop da pilha, o código acima não seria compilado porque o código dentro das chaves não seria capaz de acessar a variável var que vive fora das chaves" - isso simplesmente não é verdade. O compilador sempre pode lembrar a distância do ponteiro da pilha / quadro e usá-lo para fazer referência a variáveis ​​externas. Além disso, veja a resposta de Joseph para um exemplo de chaves que fazer causa de um empurrão pilha / pop.
18740 George
@ george- O comportamento que você descreve, assim como o exemplo de Joseph, depende do compilador e da plataforma que você está usando. Por exemplo, compilar o mesmo código para um destino MIPS gera resultados completamente diferentes. Eu estava falando puramente do ponto de vista da especificação C (já que o OP não especificou um compilador ou destino). Vou editar a resposta e adicionar mais detalhes.
bta
0

Eu acredito que ele sai do escopo, mas não é extraído da pilha até que a função retorne. Portanto, ele continuará ocupando memória na pilha até que a função seja concluída, mas não acessível a jusante da primeira chave de fechamento.

simon
fonte
3
Sem garantias. Depois que o escopo é fechado, o compilador não está mais controlando essa memória (ou pelo menos não é necessário ...) e pode muito bem reutilizá-la. É por isso que tocar na memória anteriormente ocupada por uma variável fora do escopo é um comportamento indefinido. Cuidado com os demônios nasais e avisos semelhantes.
dmckee --- gatinho ex-moderador
0

Já foram fornecidas muitas informações sobre o padrão, indicando que ele é realmente específico da implementação.

Portanto, um experimento pode ser interessante. Se tentarmos o seguinte código:

#include <stdio.h>
int main() {
    int* x;
    int* y;
    {
        int a;
        x = &a;
        printf("%p\n", (void*) x);
    }
    {
        int b;
        y = &b;
        printf("%p\n", (void*) y);
    }
}

Usando o gcc, obtemos aqui duas vezes o mesmo endereço: Coliro

Mas se tentarmos o seguinte código:

#include <stdio.h>
int main() {
    int* x;
    int* y;
    {
        int a;
        x = &a;
    }
    {
        int b;
        y = &b;
    }
    printf("%p\n", (void*) x);
    printf("%p\n", (void*) y);
}

Usando o gcc, obtemos aqui dois endereços diferentes: Coliro

Então, você não pode ter certeza do que está acontecendo.

Mi-He
fonte