Stack, Static y Heap en C ++

160

He buscado, pero no he entendido muy bien estos tres conceptos. ¿Cuándo debo usar la asignación dinámica (en el montón) y cuál es su verdadera ventaja? ¿Cuáles son los problemas de static y stack? ¿Podría escribir una aplicación completa sin asignar variables en el montón?

Escuché que otros idiomas incorporan un "recolector de basura" para que no tenga que preocuparse por la memoria. ¿Qué hace el recolector de basura?

¿Qué podrías hacer manipulando la memoria por ti mismo que no podrías hacer usando este recolector de basura?

Una vez alguien me dijo eso con esta declaración:

int * asafe=new int;

Tengo un "puntero a un puntero". Qué significa eso? Es diferente de:

asafe=new int;

?

Hai
fuente
Hubo una pregunta muy similar hace algún tiempo: ¿Qué y dónde están la pila y el montón? Hay algunas respuestas realmente buenas a esa pregunta que deberían arrojar algo de luz sobre la suya.
Scott Saad el
Posible duplicado de ¿Qué y dónde están la pila y el montón?
Swati Garg

Respuestas:

223

Se hizo una pregunta similar , pero no se hizo sobre estadísticas.

Resumen de lo que son la memoria estática, el montón y la pila:

  • Una variable estática es básicamente una variable global, incluso si no puede acceder a ella globalmente. Por lo general, hay una dirección que está en el ejecutable mismo. Solo hay una copia para todo el programa. No importa cuántas veces ingrese a una llamada de función (o clase) (¡y en cuántos hilos!) La variable se refiere a la misma ubicación de memoria.

  • El montón es un montón de memoria que se puede usar dinámicamente. Si desea 4 kb para un objeto, el asignador dinámico buscará en su lista de espacio libre en el montón, seleccionará un fragmento de 4 kb y se lo dará. En general, el asignador de memoria dinámica (malloc, new, etc.) comienza al final de la memoria y funciona al revés.

  • Explicar cómo una pila crece y se contrae está un poco fuera del alcance de esta respuesta, pero es suficiente decir que siempre agrega y elimina solo desde el final. Las pilas generalmente comienzan altas y crecen a direcciones más bajas. Se queda sin memoria cuando la pila se encuentra con el asignador dinámico en algún lugar en el medio (pero se refiere a la memoria física y virtual y la fragmentación). Múltiples hilos requerirán múltiples pilas (el proceso generalmente reserva un tamaño mínimo para la pila).

Cuándo querrías usar cada uno:

  • Las estáticas / globales son útiles para la memoria que sabes que siempre necesitarás y sabes que nunca quieres desasignar. (Por cierto, se puede pensar que los entornos integrados tienen solo memoria estática ... la pila y el montón son parte de un espacio de direcciones conocido compartido por un tercer tipo de memoria: el código del programa. Los programas a menudo realizan una asignación dinámica fuera de su memoria estática cuando necesitan cosas como listas enlazadas. Sin embargo, la memoria estática en sí misma (el búfer) no está "asignada", sino que otros objetos se asignan fuera de la memoria mantenida por el búfer para este propósito. Puede hacer esto en juegos no integrados, y los juegos de consola con frecuencia evitarán los mecanismos de memoria dinámica incorporados a favor de controlar estrictamente el proceso de asignación mediante el uso de buffers de tamaños preestablecidos para todas las asignaciones).

  • Las variables de pila son útiles para cuando sabes que mientras la función esté dentro del alcance (en algún lugar de la pila), querrás que las variables permanezcan. Las pilas son buenas para las variables que necesita para el código donde se encuentran, pero que no se necesitan fuera de ese código. También son realmente agradables para cuando accede a un recurso, como un archivo, y desea que el recurso desaparezca automáticamente cuando deja ese código.

  • Las asignaciones de almacenamiento dinámico (memoria asignada dinámicamente) son útiles cuando desea ser más flexible que lo anterior. Con frecuencia, se llama a una función para responder a un evento (el usuario hace clic en el botón "crear cuadro"). La respuesta adecuada puede requerir la asignación de un nuevo objeto (un nuevo objeto Box) que debería permanecer mucho tiempo después de que se salga de la función, por lo que no puede estar en la pila. Pero no sabe cuántos cuadros desea al inicio del programa, por lo que no puede ser estático.

Recolección de basura

Últimamente he escuchado mucho acerca de cuán geniales son los recolectores de basura, por lo que tal vez un poco de voz disidente sería útil.

La recolección de basura es un mecanismo maravilloso para cuando el rendimiento no es un gran problema. Escuché que los GC se están volviendo mejores y más sofisticados, pero el hecho es que puede verse obligado a aceptar una penalización de rendimiento (dependiendo del caso de uso). Y si eres flojo, aún puede no funcionar correctamente. En el mejor de los casos, los recolectores de basura se dan cuenta de que su memoria desaparece cuando se da cuenta de que no hay más referencias (consulte el recuento de referencias) Pero, si tiene un objeto que se refiere a sí mismo (posiblemente al referirse a otro objeto que se refiere de nuevo), entonces el conteo de referencias por sí solo no indicará que la memoria se puede eliminar. En este caso, el GC necesita mirar la sopa de referencia completa y determinar si hay islas a las que solo se hace referencia. Sin pensarlo, supongo que se trata de una operación O (n ^ 2), pero sea lo que sea, puede empeorar si te preocupa el rendimiento. (Editar: Martin B señala que es O (n) para algoritmos razonablemente eficientes. Eso sigue siendo O (n) demasiado si le preocupa el rendimiento y puede desasignar en tiempo constante sin recolección de basura).

Personalmente, cuando escucho a la gente decir que C ++ no tiene recolección de basura, mi mente lo etiqueta como una característica de C ++, pero probablemente soy una minoría. Probablemente lo más difícil para la gente de aprender sobre la programación en C y C ++ son los punteros y cómo manejar correctamente sus asignaciones de memoria dinámica. Algunos otros idiomas, como Python, serían horribles sin GC, por lo que creo que todo se reduce a lo que quieres de un idioma. Si desea un rendimiento confiable, entonces C ++ sin recolección de basura es lo único en este lado de Fortran en lo que puedo pensar. Si desea facilidad de uso y ruedas de entrenamiento (para evitar que se caiga sin requerir que aprenda la administración de memoria "adecuada"), elija algo con un GC. Incluso si sabe cómo administrar bien la memoria, le ahorrará tiempo que puede dedicar a optimizar otro código. Realmente ya no hay una gran penalización de rendimiento, pero si realmente necesita un rendimiento confiable (y la capacidad de saber exactamente qué está sucediendo, cuándo, debajo de las sábanas), me quedaría con C ++. Hay una razón por la que todos los motores de juegos principales de los que he oído hablar están en C ++ (si no en C o ensamblaje). Python, et al. Están bien para los scripts, pero no son el motor principal del juego.

mercados
fuente
No es realmente relevante para la pregunta original (o para mucho, en realidad), pero tienes las ubicaciones de la pila y el montón al revés. Por lo general , la pila crece hacia abajo y el montón crece (aunque un montón en realidad no "crecer", por lo que esta es una enorme simplificación excesiva) ...
P papá
No creo que esta pregunta sea similar o incluso duplicada de la otra pregunta. este es específicamente sobre C ++ y lo que quiso decir es casi con certeza las tres duraciones de almacenamiento existentes en C ++. Puede tener un objeto dinámico asignado en la memoria estática bien, por ejemplo, sobrecarga op new.
Johannes Schaub - litb
77
Su tratamiento despectivo de la recolección de basura fue un poco menos que útil.
P Daddy
9
A menudo, la recolección de basura es hoy en día mejor que la liberación manual de memoria porque ocurre cuando hay poco trabajo por hacer, en lugar de liberar memoria que puede ocurrir justo cuando el rendimiento podría usarse de otra manera.
Georg Schölly
3
Solo un pequeño comentario: la recolección de basura no tiene una complejidad O (n ^ 2) (eso, de hecho, sería desastroso para el rendimiento). El tiempo necesario para un ciclo de recolección de basura es proporcional al tamaño del montón; consulte hpl.hp.com/personal/Hans_Boehm/gc/complexity.html .
Martin B
54

Lo siguiente, por supuesto, no es del todo preciso. Tómelo con un grano de sal cuando lo lea :)

Bueno, las tres cosas a las que se refiere son la duración del almacenamiento automático, estático y dinámico , que tiene algo que ver con la duración de los objetos y cuándo comienzan.


Duración de almacenamiento automático

Utiliza el tiempo de almacenamiento automático de breve duración y pequeñas de datos, que solamente se necesita a nivel local dentro de algún bloque:

if(some condition) {
    int a[3]; // array a has automatic storage duration
    fill_it(a);
    print_it(a);
}

La vida útil finaliza tan pronto como salgamos del bloque, y comienza tan pronto como se define el objeto. Son el tipo más simple de duración de almacenamiento y son mucho más rápidos que en la duración de almacenamiento dinámico en particular.


Duración de almacenamiento estático

Utiliza la duración del almacenamiento estático para las variables libres, a las que puede acceder cualquier código en todo momento, si su alcance permite dicho uso (alcance del espacio de nombres), y para las variables locales que necesitan extender su vida útil a través de la salida de su alcance (alcance local), y para las variables miembro que deben ser compartidas por todos los objetos de su clase (alcance de clases). Su vida útil depende del alcance en el que se encuentren. Pueden tener un alcance de espacio de nombres y un alcance local y de clase . Lo que es cierto acerca de ambos es que, una vez que comienza su vida, la vida termina al final del programa . Aquí hay dos ejemplos:

// static storage duration. in global namespace scope
string globalA; 
int main() {
    foo();
    foo();
}

void foo() {
    // static storage duration. in local scope
    static string localA;
    localA += "ab"
    cout << localA;
}

El programa se imprime abababporque localAno se destruye al salir de su bloque. Puede decir que los objetos que tienen alcance local comienzan su vida útil cuando el control alcanza su definición . Porque localAsucede cuando se ingresa el cuerpo de la función. Para los objetos en el ámbito del espacio de nombres, la vida comienza al inicio del programa . Lo mismo es cierto para los objetos estáticos de alcance de clase:

class A {
    static string classScopeA;
};

string A::classScopeA;

A a, b; &a.classScopeA == &b.classScopeA == &A::classScopeA;

Como puede ver, classScopeAno está vinculado a objetos particulares de su clase, sino a la clase misma. La dirección de los tres nombres anteriores es la misma, y ​​todos denotan el mismo objeto. Hay una regla especial sobre cuándo y cómo se inicializan los objetos estáticos, pero no nos preocupemos por eso ahora. Eso se entiende por el término fiasco de orden de inicialización estática .


Duración de almacenamiento dinámico

La última duración de almacenamiento es dinámica. Lo usa si desea que los objetos vivan en otra isla, y desea colocar punteros alrededor de ellos. También los usa si sus objetos son grandes y si desea crear matrices de tamaño que solo se conocen en tiempo de ejecución . Debido a esta flexibilidad, los objetos que tienen una duración de almacenamiento dinámico son complicados y lentos de administrar. Los objetos que tienen esa duración dinámica comienzan su vida útil cuando ocurre una nueva invocación apropiada del operador:

int main() {
    // the object that s points to has dynamic storage 
    // duration
    string *s = new string;
    // pass a pointer pointing to the object around. 
    // the object itself isn't touched
    foo(s);
    delete s;
}

void foo(string *s) {
    cout << s->size();
}

Su vida útil finaliza solo cuando llamas a eliminar para ellos. Si olvida eso, esos objetos nunca terminan la vida. Y los objetos de clase que definen un constructor declarado por el usuario no tendrán sus destructores llamados. Los objetos que tienen una duración de almacenamiento dinámico requieren un manejo manual de su vida útil y recursos de memoria asociados. Existen bibliotecas para facilitar su uso. La recolección de basura explícita para objetos particulares se puede establecer utilizando un puntero inteligente:

int main() {
    shared_ptr<string> s(new string);
    foo(s);
}

void foo(shared_ptr<string> s) {
    cout << s->size();
}

No tiene que preocuparse por llamar a delete: el ptr compartido lo hace por usted, si el último puntero que hace referencia al objeto se sale del alcance. El ptr compartido tiene una duración de almacenamiento automática. Por lo tanto, su vida útil se administra automáticamente, lo que le permite verificar si debe eliminar el objeto dinámico señalado en su destructor. Para referencia de shared_ptr, consulte los documentos de impulso: http://www.boost.org/doc/libs/1_37_0/libs/smart_ptr/shared_ptr.htm

Johannes Schaub - litb
fuente
39

Se ha dicho elaboradamente, como "la respuesta corta":

  • variable estática (clase) de por
    vida = tiempo de ejecución del programa (1)
    visibilidad = determinada por modificadores de acceso (privado / protegido / público)

  • variable estática (alcance global)
    duración = tiempo de ejecución del programa (1)
    visibilidad = la unidad de compilación en la que se instancia (2)


  • vida útil de la variable del montón = definida por usted (nueva para eliminar)
    visibilidad = definida por usted (a lo que le asigne el puntero)


  • visibilidad de la variable de la pila = desde la declaración hasta que se salga del alcance
    vida útil = desde la declaración hasta que se salga del alcance de declaración


(1) más exactamente: desde la inicialización hasta la desinicialización de la unidad de compilación (es decir, archivo C / C ++). El orden de inicialización de las unidades de compilación no está definido por el estándar.

(2) Cuidado: si crea una instancia de una variable estática en un encabezado, cada unidad de compilación obtiene su propia copia.

Peterchen
fuente
5

Estoy seguro de que uno de los pedantes obtendrá una respuesta mejor en breve, pero la diferencia principal es la velocidad y el tamaño.

Apilar

Dramáticamente más rápido de asignar. Se realiza en O (1) ya que se asigna al configurar el marco de la pila para que sea esencialmente libre. El inconveniente es que si te quedas sin espacio de pila, estás deshuesado. Puede ajustar el tamaño de la pila, pero IIRC tiene ~ 2 MB para jugar. Además, tan pronto como salga de la función, todo en la pila se borrará. Por lo tanto, puede ser problemático consultarlo más adelante. (Los punteros para apilar objetos asignados conducen a errores).

Montón

Dramáticamente más lento para asignar. Pero tienes GB para jugar y señalar.

Recolector de basura

El recolector de basura es un código que se ejecuta en segundo plano y libera memoria. Cuando asigna memoria en el montón, es muy fácil olvidarse de liberarla, lo que se conoce como pérdida de memoria. Con el tiempo, la memoria que consume su aplicación crece y crece hasta que se bloquea. Tener un recolector de basura periódicamente libera la memoria que ya no necesita ayuda a eliminar esta clase de errores. Por supuesto, esto tiene un precio, ya que el recolector de basura ralentiza las cosas.

Chris Smith
fuente
3

¿Cuáles son los problemas de static y stack?

El problema con la asignación "estática" es que la asignación se realiza en tiempo de compilación: no se puede usar para asignar un número variable de datos, cuyo número no se conoce hasta el tiempo de ejecución.

El problema con la asignación en la "pila" es que la asignación se destruye tan pronto como la subrutina que hace la asignación regresa.

¿Podría escribir una aplicación completa sin asignar variables en el montón?

Quizás, pero no una aplicación grande, normal y no trivial (pero los llamados programas "integrados" podrían escribirse sin el montón, utilizando un subconjunto de C ++).

¿Qué hace el recolector de basura?

Sigue observando sus datos ("marcar y barrer") para detectar cuándo su aplicación ya no hace referencia a ellos. Esto es conveniente para la aplicación, porque la aplicación no necesita desasignar los datos ... pero el recolector de basura puede ser computacionalmente costoso.

Los recolectores de basura no son una característica habitual de la programación en C ++.

¿Qué podrías hacer manipulando la memoria por ti mismo que no podrías hacer usando este recolector de basura?

Aprenda los mecanismos de C ++ para la desasignación de memoria determinista:

  • 'static': nunca desasignado
  • 'stack': tan pronto como la variable "salga del alcance"
  • 'montón': cuando se elimina el puntero (eliminado explícitamente por la aplicación, o implícitamente eliminado dentro de una u otra subrutina)
ChrisW
fuente
1

La asignación de memoria de pila (variables de función, variables locales) puede ser problemática cuando su pila es demasiado "profunda" y desborda la memoria disponible para las asignaciones de pila. El montón es para objetos a los que se debe acceder desde múltiples subprocesos o durante todo el ciclo de vida del programa. Puede escribir un programa completo sin usar el montón.

Puede perder memoria con bastante facilidad sin un recolector de basura, pero también puede dictar cuándo se liberan los objetos y la memoria. Me he encontrado con problemas con Java cuando ejecuta el GC y tengo un proceso en tiempo real, porque el GC es un hilo exclusivo (nada más puede ejecutarse). Entonces, si el rendimiento es crítico y puede garantizar que no haya objetos filtrados, no usar un GC es muy útil. De lo contrario, solo te hace odiar la vida cuando tu aplicación consume memoria y tienes que rastrear la fuente de una fuga.

Rob Elsner
fuente
1

¿Qué sucede si su programa no sabe por adelantado cuánta memoria asignar (por lo tanto, no puede usar variables de pila)? Digamos que las listas enlazadas, las listas pueden crecer sin saber por adelantado cuál es su tamaño. Por lo tanto, la asignación en un montón tiene sentido para una lista vinculada cuando no se sabe cuántos elementos se insertarán en ella.

kal
fuente
0

Una ventaja de GC en algunas situaciones es una molestia en otras; confiar en GC alienta a no pensar mucho en ello. En teoría, espera hasta el período 'inactivo' o hasta que sea absolutamente necesario, cuando robará el ancho de banda y provocará una latencia de respuesta en su aplicación.

Pero no tienes que 'no pensar en eso'. Al igual que con todo lo demás en aplicaciones multiproceso, cuando puede ceder, puede ceder. Entonces, por ejemplo, en .Net, es posible solicitar un GC; Al hacer esto, en lugar de un GC de ejecución más larga y menos frecuente, puede tener un GC de ejecución más corta y más frecuente, y extender la latencia asociada con esta sobrecarga.

Pero esto derrota la atracción principal de GC que parece estar "alentada a no tener que pensar mucho en ella porque es automática".

Si se expuso por primera vez a la programación antes de que GC se generalizara y se sintiera cómodo con malloc / free y new / delete, incluso podría darse el caso de que GC sea un poco molesto y / o desconfíe (como uno podría desconfiar de ' optimización ', que ha tenido un historial a cuadros.) Muchas aplicaciones toleran latencia aleatoria. Pero para las aplicaciones que no lo hacen, donde la latencia aleatoria es menos aceptable, una reacción común es evitar los entornos de GC y avanzar en la dirección de código puramente no administrado (o Dios no lo quiera, un arte de larga muerte, lenguaje ensamblador).

Hace un tiempo tuve un estudiante de verano aquí, un niño interno e inteligente, que fue destetado en GC; estaba tan entusiasmado con la superiortia de GC que incluso cuando programaba en C / C ++ no administrado se negaba a seguir el modelo malloc / free new / delete porque, entre comillas, "no debería tener que hacer esto en un lenguaje de programación moderno". ¿Y sabes? Para aplicaciones pequeñas y de ejecución corta, de hecho puede salirse con la suya, pero no para aplicaciones de alto rendimiento.

frediano
fuente
0

Stack es una memoria asignada por el compilador, cuando compilamos el programa, en el compilador predeterminado asigna algo de memoria del sistema operativo (podemos cambiar la configuración de la configuración del compilador en su IDE) y el sistema operativo es el que le proporciona la memoria, depende en muchas memorias disponibles en el sistema y en muchas otras cosas, y al apilar la memoria se asigna cuando declaramos una variable que copian (ref como formales) esas variables se insertan en la pila y siguen algunas convenciones de nomenclatura por defecto su CDECL en los estudios visuales ej .: notación infija: c = a + b; el empuje de la pila se realiza de EMPUJE de derecha a izquierda, b para apilar, operador, a para apilar y resultado de esos i, ec para apilar. En notación previa: = + cabina Aquí todas las variables se empujan para apilarse primero (de derecha a izquierda) y luego se realiza la operación. Esta memoria asignada por el compilador es fija. Supongamos que se asigna 1 MB de memoria a nuestra aplicación, digamos que las variables usan 700 kb de memoria (todas las variables locales se envían a la pila a menos que se asignen dinámicamente), por lo que la memoria restante de 324 kb se asigna al montón. Y esta pila tiene menos tiempo de vida, cuando el alcance de la función termina, estas pilas se borran.

raj
fuente