En C, ¿los frenos actúan como un marco de pila?

153

Si creo una variable dentro de un nuevo conjunto de llaves, ¿esa variable aparece en la pila en la llave de cierre o se cuelga hasta el final de la función? Por ejemplo:

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

¿Va da tomar memoria durante la code that takes a whilesección?

Claudiu
fuente
8
¿Te refieres a (1) según el Estándar, (2) práctica universal entre implementaciones, o (3) práctica común entre implementaciones?
David Thornley

Respuestas:

83

No, las llaves no actúan como un marco de pila. En C, las llaves solo denotan un ámbito de nomenclatura, pero nada se destruye ni se extrae nada de la pila cuando el control se sale de ella.

Como un programador que escribe código, a menudo puede pensarlo como si fuera un marco de pila. Los identificadores declarados dentro de las llaves solo son accesibles dentro de las llaves, por lo que desde el punto de vista del programador, es como si fueran empujados a la pila a medida que se declaran y luego se abren cuando se sale del alcance. Sin embargo, los compiladores no tienen que generar código que empuje / extraiga nada en la entrada / salida (y, en general, no lo hacen).

También tenga en cuenta que las variables locales pueden no utilizar ningún espacio de pila en absoluto: podrían mantenerse en registros de CPU o en alguna otra ubicación de almacenamiento auxiliar, o optimizarse por completo.

Entonces, la dmatriz, en teoría, podría consumir memoria para toda la función. Sin embargo, el compilador puede optimizarlo o compartir su memoria con otras variables locales cuyas vidas de uso no se superponen.

Kristopher Johnson
fuente
9
¿No es eso específico de la implementación?
avakar
54
En C ++, se llama al destructor de un objeto al final de su alcance. Si la memoria se recupera es un problema específico de implementación.
Kristopher Johnson
8
@ pm100: se llamará a los destructores. Eso no dice nada acerca de la memoria que ocuparon esos objetos.
Donal Fellows
9
El estándar C especifica que la vida útil de las variables automáticas declaradas en el bloque se extiende solo hasta que finaliza la ejecución del bloque. Así que, esencialmente aquellas variables automáticas no consiguen "destruidos" al final del bloque.
caf
3
@KristopherJohnson: si un método tuviera dos bloques separados, cada uno de los cuales declarara una matriz de 1Kbyte, y un tercer bloque que llamara un método anidado, un compilador sería libre de usar la misma memoria para ambas matrices, y / o colocar la matriz en la parte más superficial de la pila y mueva el puntero de la pila sobre ella llamando al método anidado. Tal comportamiento podría reducir en 2K la profundidad de la pila requerida para la llamada a la función.
supercat
39

El tiempo durante el cual la variable realmente está ocupando memoria obviamente depende del compilador (y muchos compiladores no ajustan el puntero de la pila cuando se ingresan y salen bloques internos dentro de las funciones).

Sin embargo, una pregunta estrechamente relacionada pero posiblemente más interesante es si el programa puede acceder a ese objeto interno fuera del alcance interno (pero dentro de la función que lo contiene), es decir:

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

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

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

   return;
}

(En otras palabras: ¿se le permite al compilador desasignar d, incluso si en la práctica la mayoría no lo hace?).

La respuesta es que el compilador está permitido cancelar la asignación d, y el acceso p[0]en el que el comentario indica es un comportamiento indefinido (el programa está no permitió el acceso a la parte exterior del objeto interno del ámbito interno). La parte relevante del estándar C es 6.2.4p5:

Para un objeto de este tipo [uno que tiene una duración de almacenamiento automático] que no tiene un tipo de matriz de longitud variable, su vida útil se extiende desde la entrada en el bloque con el que está asociado hasta que la ejecución de ese bloque termina de alguna manera . (Al ingresar un bloque cerrado o al invocar una función se suspende, pero no finaliza, la ejecución del bloque actual). Si el bloque se ingresa recursivamente, se crea una nueva instancia del objeto cada vez. El valor inicial del objeto es indeterminado. Si se especifica una inicialización para el objeto, se realiza cada vez que se alcanza la declaración en la ejecución del bloque; de lo contrario, el valor se vuelve indeterminado cada vez que se alcanza la declaración.

coste y flete
fuente
Como alguien que aprende cómo funciona el alcance y la memoria en C y C ++ después de años de usar lenguajes de nivel superior, encuentro esta respuesta más precisa y útil que la aceptada.
Chris
20

Su pregunta no es lo suficientemente clara como para ser respondida sin ambigüedades.

Por un lado, los compiladores normalmente no realizan ninguna asignación de asignación de memoria local para ámbitos de bloque anidados. La memoria local normalmente se asigna solo una vez en la entrada de la función y se libera en la salida de la función.

Por otro lado, cuando finaliza la vida útil de un objeto local, la memoria ocupada por ese objeto puede reutilizarse para otro objeto local más tarde. Por ejemplo, en este código

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

ambas matrices generalmente ocuparán la misma área de memoria, lo que significa que la cantidad total de almacenamiento local que necesita la función fooes lo que sea necesario para la mayor de las dos matrices, no para las dos al mismo tiempo.

dUsted debe decidir si este último califica para continuar ocupando la memoria hasta el final de la función en el contexto de su pregunta.

Hormiga
fuente
6

Depende de la implementación. Escribí un programa corto para probar lo que hace gcc 4.3.4, y asigna todo el espacio de la pila a la vez al comienzo de la función. Puede examinar el ensamblaje que produce gcc utilizando el indicador -S.

Daniel Stutzbach
fuente
3

No, d [] no estará en la pila por el resto de la rutina. Pero alloca () es diferente.

Editar: Kristopher Johnson (y Simon y Daniel) tienen razón , y mi respuesta inicial fue incorrecta . Con gcc 4.3.4.on CYGWIN, el código:

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

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

da:

_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

¡Vive y aprende! Y una prueba rápida parece mostrar que AndreyT también tiene razón sobre las asignaciones múltiples.

Se agregó mucho más tarde : la prueba anterior muestra que la documentación de gcc no es del todo correcta. Durante años ha dicho (énfasis agregado):

"El espacio para una matriz de longitud variable se desasigna tan pronto como finaliza el alcance del nombre de la matriz ".

Joseph Quinsey
fuente
Compilar con la optimización deshabilitada no necesariamente muestra lo que obtendrá en el código optimizado. En este caso, el comportamiento es el mismo (asignar al inicio de la función, y solo libre al salir de la función): godbolt.org/g/M112AQ . Pero el no-cygwin gcc no llama a una allocafunción. Estoy realmente sorprendido de que cygwin gcc haría eso. Ni siquiera es una matriz de longitud variable, así que IDK por qué lo mencionas.
Peter Cordes
2

Podrían. Puede que no. La respuesta que creo que realmente necesitas es: nunca asumas nada. Los compiladores modernos hacen todo tipo de arquitectura y magia específica de implementación. Escriba su código de manera simple y legible para los humanos y deje que el compilador haga lo bueno. Si intenta codificar alrededor del compilador, está buscando problemas, y los problemas que generalmente tiene en estas situaciones suelen ser terriblemente sutiles y difíciles de diagnosticar.

usuario19666
fuente
1

Su variable dgeneralmente no se saca de la pila. Las llaves no indican un marco de pila. De lo contrario, no podría hacer algo como esto:

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

Si las llaves causan un verdadero empuje / pop de pila (como lo haría una llamada a la función), entonces el código anterior no se compilaría porque el código dentro de las llaves no podría acceder a la variable varque vive fuera de las llaves (como un sub- La función no puede acceder directamente a las variables en la función de llamada). Sabemos que este no es el caso.

Las llaves se utilizan simplemente para determinar el alcance. El compilador tratará cualquier acceso a la variable "interna" desde fuera de los corchetes como no válidos, y puede reutilizar esa memoria para otra cosa (esto depende de la implementación). Sin embargo, no se puede quitar de la pila hasta que vuelva la función de cierre.

Actualización: Esto es lo que la especificación C tiene que decir. Con respecto a los objetos con duración de almacenamiento automático (sección 6.4.2):

Para un objeto que no tiene un tipo de matriz de longitud variable, su vida útil se extiende desde la entrada en el bloque con el que está asociado hasta que la ejecución de ese bloque termina de todos modos.

La misma sección define el término "vida útil" como (el énfasis es mío):

La vida útil de un objeto es la parte de la ejecución del programa durante la cual se garantiza que el almacenamiento estará reservado para él. Existe un objeto, tiene una dirección constante y conserva su último valor almacenado durante toda su vida útil. Si se hace referencia a un objeto fuera de su vida útil, el comportamiento es indefinido.

La palabra clave aquí es, por supuesto, 'garantizada'. Una vez que abandona el alcance del conjunto interno de llaves, la vida útil de la matriz ha terminado. El almacenamiento puede o no estar asignado (el compilador puede reutilizar el espacio para otra cosa), pero cualquier intento de acceder a la matriz invoca un comportamiento indefinido y produce resultados impredecibles.

La especificación C no tiene noción de marcos de pila. Solo habla de cómo se comportará el programa resultante y deja los detalles de la implementación al compilador (después de todo, la implementación se vería bastante diferente en una CPU sin pila que en una CPU con una pila de hardware). No hay nada en la especificación C que indique dónde terminará o no un marco de pila. La única forma real de saber es compilar el código en su compilador / plataforma particular y examinar el ensamblaje resultante. El conjunto actual de opciones de optimización de su compilador probablemente también jugará un papel en esto.

Si desea asegurarse de que la matriz dya no consume memoria mientras se ejecuta el código, puede convertir el código entre llaves en una función separada o explícitamente mallocy freela memoria en lugar de utilizar el almacenamiento automático.

bta
fuente
1
"Si los corchetes causaron un empuje / pop de pila, entonces el código anterior no se compilaría porque el código dentro de los corchetes no podría acceder a la variable var que vive fuera de los corchetes" , esto simplemente no es cierto. El compilador siempre puede recordar la distancia desde el puntero de la pila / marco, y usarlo para hacer referencia a variables externas. También, véase la respuesta de Joseph como un ejemplo de llaves que hacen causa una pila push / pop.
George
@ george: el comportamiento que describe, así como el ejemplo de Joseph, depende del compilador y la plataforma que esté utilizando. Por ejemplo, compilar el mismo código para un objetivo MIPS produce resultados completamente diferentes. Estaba hablando puramente desde el punto de vista de la especificación C (ya que el OP no especificó un compilador u objetivo). Editaré la respuesta y agregaré más detalles.
bta
0

Creo que está fuera de alcance, pero no se saca de la pila hasta que la función regrese. Por lo tanto, seguirá ocupando memoria en la pila hasta que se complete la función, pero no será accesible después de la primera llave de cierre.

Simón
fuente
3
Sin garantías Una vez que se cierra el alcance, el compilador ya no realiza un seguimiento de esa memoria (o al menos no se requiere que ...) y bien puede reutilizarla. Esta es la razón por la cual tocar la memoria anteriormente ocupada por una variable fuera de alcance es un comportamiento indefinido. Cuidado con los demonios nasales y advertencias similares.
dmckee --- gatito ex moderador
0

Ya se ha dado mucha información sobre el estándar que indica que, de hecho, es específico de la implementación.

Entonces, un experimento podría ser de interés. Si probamos el siguiente 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 gcc obtenemos aquí dos veces la misma dirección: Coliro

Pero si probamos el siguiente 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 gcc obtenemos aquí dos direcciones diferentes: Coliro

Entonces, no puedes estar realmente seguro de lo que está sucediendo.

Mi-he
fuente