Gestión de memoria C

90

Siempre he oído que en C tienes que vigilar realmente cómo gestionas la memoria. Y todavía estoy empezando a aprender C, pero hasta ahora, no he tenido que hacer ninguna actividad relacionada con la gestión de memoria. Siempre imaginé tener que liberar variables y hacer todo tipo de cosas feas. Pero este no parece ser el caso.

¿Alguien puede mostrarme (con ejemplos de código) un ejemplo de cuándo tendría que hacer algo de "administración de memoria"?

El.Anti.9
fuente
Buen lugar para aprender G4G
EsmaeelE

Respuestas:

230

Hay dos lugares donde las variables se pueden guardar en la memoria. Cuando creas una variable como esta:

int  a;
char c;
char d[16];

Las variables se crean en la " pila ". Las variables de pila se liberan automáticamente cuando salen del alcance (es decir, cuando el código ya no puede alcanzarlas). Es posible que las escuche llamar variables "automáticas", pero eso ha pasado de moda.

Muchos ejemplos para principiantes usarán solo variables de pila.

La pila es buena porque es automática, pero también tiene dos inconvenientes: (1) El compilador necesita saber de antemano qué tan grandes son las variables y (b) el espacio de la pila es algo limitado. Por ejemplo: en Windows, en la configuración predeterminada del vinculador de Microsoft, la pila se establece en 1 MB y no todo está disponible para sus variables.

Si no sabe en el momento de la compilación qué tan grande es su matriz, o si necesita una matriz o estructura grande, necesita el "plan B".

El plan B se llama " montón ". Por lo general, puede crear variables tan grandes como le permita el sistema operativo, pero debe hacerlo usted mismo. Publicaciones anteriores le mostraron una forma en que puede hacerlo, aunque hay otras formas:

int size;
// ...
// Set size to some value, based on information available at run-time. Then:
// ...
char *p = (char *)malloc(size);

(Tenga en cuenta que las variables en el montón no se manipulan directamente, sino mediante punteros)

Una vez que crea una variable de montón, el problema es que el compilador no puede saber cuándo ha terminado con ella, por lo que pierde la liberación automática. Ahí es donde entra la "liberación manual" a la que se refería. Su código ahora es responsable de decidir cuándo la variable ya no es necesaria y liberarla para que la memoria se pueda utilizar para otros fines. Para el caso anterior, con:

free(p);

Lo que hace que esta segunda opción sea un "negocio desagradable" es que no siempre es fácil saber cuándo ya no se necesita la variable. Olvidar liberar una variable cuando no la necesita hará que su programa consuma más memoria de la que necesita. Esta situación se denomina "fuga". La memoria "filtrada" no se puede utilizar para nada hasta que el programa finalice y el sistema operativo recupere todos sus recursos. Son posibles problemas aún más desagradables si libera una variable de montón por error antes que haya terminado con ella.

En C y C ++, usted es responsable de limpiar sus variables de montón como se muestra arriba. Sin embargo, hay lenguajes y entornos como Java y lenguajes .NET como C # que utilizan un enfoque diferente, donde el montón se limpia por sí solo. Este segundo método, llamado "recolección de basura", es mucho más fácil para el desarrollador, pero paga una penalización en gastos generales y rendimiento. Es un equilibrio.

(He pasado por alto muchos detalles para dar una respuesta más simple, pero con suerte más nivelada)

Euro Micelli
fuente
3
Si desea poner algo en la pila pero no sabe qué tan grande es en tiempo de compilación, alloca () puede ampliar el marco de la pila para hacer espacio. No hay freea (), todo el marco de la pila aparece cuando la función regresa. El uso de alloca () para grandes asignaciones es peligroso.
DGentry
1
Tal vez podría agregar una o dos oraciones sobre la ubicación de la memoria de las variables globales
Michael Käfer
En C nunca emitió el retorno de malloc(), su causa UB, (char *)malloc(size);consulte stackoverflow.com/questions/605845/…
EsmaeelE
17

He aquí un ejemplo. Suponga que tiene una función strdup () que duplica una cadena:

char *strdup(char *src)
{
    char * dest;
    dest = malloc(strlen(src) + 1);
    if (dest == NULL)
        abort();
    strcpy(dest, src);
    return dest;
}

Y lo llamas así:

main()
{
    char *s;
    s = strdup("hello");
    printf("%s\n", s);
    s = strdup("world");
    printf("%s\n", s);
}

Puede ver que el programa funciona, pero ha asignado memoria (a través de malloc) sin liberarla. Ha perdido su puntero al primer bloque de memoria cuando llamó a strdup por segunda vez.

Esto no es gran cosa para esta pequeña cantidad de memoria, pero considere el caso:

for (i = 0; i < 1000000000; ++i)  /* billion times */
    s = strdup("hello world");    /* 11 bytes */

Ahora ha usado hasta 11 gigabytes de memoria (posiblemente más, dependiendo de su administrador de memoria) y si no se ha bloqueado, su proceso probablemente se esté ejecutando bastante lento.

Para solucionarlo, debe llamar a free () para todo lo que se obtiene con malloc () después de terminar de usarlo:

s = strdup("hello");
free(s);  /* now not leaking memory! */
s = strdup("world");
...

¡Espero que este ejemplo ayude!

Mark Harrison
fuente
Me gusta más esta respuesta. Pero tengo una pequeña pregunta al margen. Esperaría que algo como esto se resolviera con bibliotecas, ¿no hay una biblioteca que imite de cerca los tipos de datos básicos y les agregue funciones de liberación de memoria para que cuando las variables se usen también se liberen automáticamente?
Lorenzo
Ninguno que forme parte del estándar. Si ingresa a C ++, obtendrá cadenas y contenedores que administran automáticamente la memoria.
Mark Harrison
Ya veo, ¿hay algunas bibliotecas de terceros? ¿Podrías nombrarlos?
Lorenzo
9

Tienes que hacer "administración de memoria" cuando quieras usar memoria en el montón en lugar de en la pila. Si no sabe qué tan grande hacer una matriz hasta el tiempo de ejecución, entonces debe usar el montón. Por ejemplo, es posible que desee almacenar algo en una cadena, pero no sepa qué tan grande será su contenido hasta que se ejecute el programa. En ese caso, escribirías algo como esto:

 char *string = malloc(stringlength); // stringlength is the number of bytes to allocate

 // Do something with the string...

 free(string); // Free the allocated memory
Jeremy Ruten
fuente
5

Creo que la forma más concisa de responder a la pregunta es considerar el papel del puntero en C. El puntero es un mecanismo liviano pero poderoso que le brinda una inmensa libertad a costa de una inmensa capacidad para dispararse en el pie.

En C, la responsabilidad de asegurarse de que sus indicadores apunten a la memoria que posee es suya y solo suya. Esto requiere un enfoque organizado y disciplinado, a menos que se olvide de los consejos, lo que dificulta escribir una C.

Las respuestas publicadas hasta la fecha se concentran en asignaciones automáticas (pila) y variables de pila. El uso de la asignación de pila genera una memoria conveniente y administrada automáticamente, pero en algunas circunstancias (búferes grandes, algoritmos recursivos) puede conducir al horrendo problema del desbordamiento de pila. Saber exactamente cuánta memoria puede asignar en la pila depende en gran medida del sistema. En algunos escenarios integrados, unas pocas docenas de bytes pueden ser su límite, en algunos escenarios de escritorio puede usar megabytes de manera segura.

La asignación de montón es menos inherente al idioma. Es básicamente un conjunto de llamadas a la biblioteca que le otorga la propiedad de un bloque de memoria de un tamaño determinado hasta que esté listo para devolverlo ('liberarlo'). Suena simple, pero está asociado con un dolor incalculable del programador. Los problemas son simples (liberar la misma memoria dos veces, o nada en absoluto [pérdidas de memoria], no asignar suficiente memoria [desbordamiento del búfer], etc.) pero difíciles de evitar y depurar. Un enfoque sumamente disciplinado es absolutamente obligatorio en la práctica, pero, por supuesto, el idioma no lo exige.

Me gustaría mencionar otro tipo de asignación de memoria que otras publicaciones han ignorado. Es posible asignar variables estáticamente declarándolas fuera de cualquier función. Creo que, en general, este tipo de asignación tiene mala reputación porque lo utilizan variables globales. Sin embargo, no hay nada que diga que la única forma de usar la memoria asignada de esta manera es como una variable global indisciplinada en un lío de código espagueti. El método de asignación estática se puede utilizar simplemente para evitar algunos de los errores del montón y los métodos de asignación automática. Algunos programadores de C se sorprenden al saber que los grandes y sofisticados programas embebidos y de juegos en C se han construido sin ningún uso de asignación de montón.

Bill Forster
fuente
4

Aquí hay algunas respuestas excelentes sobre cómo asignar y liberar memoria, y en mi opinión, el lado más desafiante de usar C es asegurarse de que la única memoria que usa es la memoria que ha asignado; si esto no se hace correctamente, lo que finaliza está el primo de este sitio, un desbordamiento de búfer, y es posible que esté sobrescribiendo la memoria que está siendo utilizada por otra aplicación, con resultados muy impredecibles.

Un ejemplo:

int main() {
    char* myString = (char*)malloc(5*sizeof(char));
    myString = "abcd";
}

En este punto, ha asignado 5 bytes para myString y lo ha llenado con "abcd \ 0" (las cadenas terminan en nulo - \ 0). Si su asignación de cadena fue

myString = "abcde";

Estaría asignando "abcde" en los 5 bytes que ha asignado a su programa, y ​​el carácter nulo final se colocaría al final de esto, una parte de la memoria que no ha sido asignada para su uso y podría ser gratis, pero también podría estar siendo utilizado por otra aplicación: esta es la parte crítica de la administración de la memoria, donde un error tendrá consecuencias impredecibles (y a veces irrepetibles).

Chris BC
fuente
Aquí asigna 5 bytes. Suéltelo asignando un puntero. Cualquier intento de liberar este puntero conduce a un comportamiento indefinido. Nota C-Strings no sobrecargan el operador = no hay copia.
Martin York
Sin embargo, realmente depende del malloc que estés usando. Muchos operadores malloc se alinean en 8 bytes. Entonces, si este malloc está usando un sistema de encabezado / pie de página, malloc reservaría 5 + 4 * 2 (4 bytes tanto para el encabezado como para el pie de página). Eso sería 13 bytes, y malloc solo le daría 3 bytes adicionales para la alineación. No estoy diciendo que sea una buena idea usar esto, porque solo funcionará en sistemas cuyo malloc funciona así, pero al menos es importante saber por qué hacer algo mal podría funcionar.
Kodai
Loki: He editado la respuesta para usar en strcpy()lugar de =; Supongo que esa fue la intención de Chris BC.
echristopherson
Creo que la protección de la memoria del hardware de las plataformas modernas evita que los procesos del espacio de usuario sobrescriban los espacios de direcciones de otros procesos; en su lugar, obtendría una falla de segmentación. Pero eso no es parte de C per se.
echristopherson
4

Una cosa para recordar es siempre inicializar sus punteros a NULL, ya que un puntero no inicializado puede contener una dirección de memoria válida pseudoaleatoria que puede hacer que los errores de puntero sigan adelante silenciosamente. Al hacer que un puntero se inicialice con NULL, siempre puede detectar si está utilizando este puntero sin inicializarlo. La razón es que los sistemas operativos "conectan" la dirección virtual 0x00000000 a excepciones de protección general para atrapar el uso del puntero nulo.

Hernán
fuente
2

También es posible que desee utilizar la asignación de memoria dinámica cuando necesite definir una matriz enorme, digamos int [10000]. No puede simplemente ponerlo en la pila porque entonces, hm ... obtendrá un desbordamiento de la pila.

Otro buen ejemplo sería la implementación de una estructura de datos, digamos una lista enlazada o un árbol binario. No tengo un código de muestra para pegar aquí, pero puedes buscarlo en Google fácilmente.

Sarga
fuente
2

(Estoy escribiendo porque siento que las respuestas hasta ahora no son del todo acertadas).

La razón por la que vale la pena mencionar la administración de la memoria es cuando tiene un problema / solución que requiere que cree estructuras complejas. (Si sus programas fallan si asigna mucho espacio en la pila a la vez, eso es un error). Por lo general, la primera estructura de datos que necesitará aprender es algún tipo de lista . Aquí hay uno solo enlazado, fuera de mi cabeza:

typedef struct listelem { struct listelem *next; void *data;} listelem;

listelem * create(void * data)
{
   listelem *p = calloc(1, sizeof(listelem));
   if(p) p->data = data;
   return p;
}

listelem * delete(listelem * p)
{
   listelem next = p->next;
   free(p);
   return next;
}

void deleteall(listelem * p)
{
  while(p) p = delete(p);
}

void foreach(listelem * p, void (*fun)(void *data) )
{
  for( ; p != NULL; p = p->next) fun(p->data);
}

listelem * merge(listelem *p, listelem *q)
{
  while(p != NULL && p->next != NULL) p = p->next;
  if(p) {
    p->next = q;
    return p;
  } else
    return q;
}

Naturalmente, le gustaría algunas otras funciones, pero básicamente, para esto es para lo que necesita la administración de memoria. Debo señalar que hay una serie de trucos que son posibles con la gestión de memoria "manual", por ejemplo,

  • Usando el hecho de que malloc está garantizado (por el estándar del lenguaje) para devolver un puntero divisible por 4,
  • asignar espacio adicional para algún propósito siniestro propio,
  • creando grupos de memoria s ..

Consiga un buen depurador ... ¡ Buena suerte!

Anders Eurenius
fuente
El aprendizaje de las estructuras de datos es el siguiente paso clave para comprender la gestión de la memoria. Aprender los algoritmos para ejecutar adecuadamente estas estructuras le mostrará los métodos adecuados para superar estos obstáculos. Es por eso que encontrará estructuras de datos y algoritmos que se enseñan en los mismos cursos.
aj.toulan
0

@ Euro Micelli

Un aspecto negativo que se debe agregar es que los punteros a la pila ya no son válidos cuando la función regresa, por lo que no puede devolver un puntero a una variable de pila desde una función. Este es un error común y una de las principales razones por las que no puede funcionar solo con variables de pila. Si su función necesita devolver un puntero, entonces tiene que hacer malloc y ocuparse de la gestión de la memoria.

Jonathan Branam
fuente
0

@ Ted Percival :
... no necesitas lanzar el valor de retorno de malloc ().

Estás en lo cierto, por supuesto. Creo que siempre ha sido así, aunque no tengo una copia de K&R para comprobar.

No me gustan muchas de las conversiones implícitas en C, así que tiendo a usar moldes para hacer que la "magia" sea más visible. A veces ayuda a la legibilidad, a veces no, ya veces hace que el compilador detecte un error silencioso. Aún así, no tengo una opinión firme sobre esto, de una forma u otra.

Esto es especialmente probable si su compilador comprende los comentarios de estilo C ++.

Sí ... me atrapaste allí. Paso mucho más tiempo en C ++ que en C. Gracias por darte cuenta.

Euro Micelli
fuente
@echristopherson, gracias. Tiene razón, pero tenga en cuenta que esta Q / A fue de agosto de 2008, antes de que Stack Overflow estuviera incluso en Beta pública. En aquel entonces, todavía estábamos averiguando cómo debería funcionar el sitio. El formato de esta Pregunta / Respuesta no debe verse necesariamente como un modelo de cómo utilizar SO. ¡Gracias!
Euro Micelli
Ah, gracias por señalar eso, no me di cuenta de que ese aspecto del sitio todavía estaba cambiando.
echristopherson
0

En C, en realidad tienes dos opciones diferentes. Uno, puede dejar que el sistema administre la memoria por usted. Alternativamente, puede hacerlo usted mismo. En general, querrá ceñirse al primero el mayor tiempo posible. Sin embargo, la memoria administrada automáticamente en C es extremadamente limitada y necesitará administrar la memoria manualmente en muchos casos, como:

a. Desea que la variable sobreviva a las funciones y no quiere tener una variable global. ex:

par de estructuras {
   int val;
   par de estructuras * siguiente;
}

par de estructuras * par_nuevo (int val) {
   par de estructuras * np = malloc (tamaño de (par de estructuras));
   np-> val = val;
   np-> siguiente = NULL;
   return np;
}

si. desea tener memoria asignada dinámicamente. El ejemplo más común es una matriz sin longitud fija:

int * my_special_array;
my_special_array = malloc (sizeof (int) * number_of_element);
para (i = 0; i

C. Quieres hacer algo REALMENTE sucio. Por ejemplo, me gustaría que una estructura representara muchos tipos de datos y no me gusta la unión (la unión se ve tan desordenada):

estructura de datos { int data_type; long data_in_mem; }; estructura animal {/ * algo * /}; estructura persona {/ * alguna otra cosa * /}; estructura animal * read_animal (); estructura persona * read_person (); / * En principal * / muestra de datos de estructura; sampe.data_type = input_type; switch (input_type) { caso DATA_PERSON: sample.data_in_mem = read_person (); descanso; caso DATA_ANIMAL: sample.data_in_mem = read_animal (); defecto: printf ("¡Oh hoh! Te lo advierto, eso de nuevo y voy a segmentar tu SO"); }

Mira, un valor largo es suficiente para contener CUALQUIER COSA. Solo recuerda liberarlo, o te arrepentirás. Este es uno de mis trucos favoritos para divertirme en C: D.

Sin embargo, en general, querrá mantenerse alejado de sus trucos favoritos (T___T). Romperás tu sistema operativo, tarde o temprano, si los usas con demasiada frecuencia. Siempre que no use * alloc y free, es seguro decir que todavía es virgen y que el código aún se ve bien.

magia
fuente
"Mira, un valor largo es suficiente para contener CUALQUIER COSA" -: / de qué estás hablando, en la mayoría de los sistemas un valor largo es de 4 bytes, exactamente lo mismo que un int. La única razón por la que se ajusta a los punteros aquí es porque el tamaño de long resulta ser el mismo que el tamaño del puntero. Sin embargo, debería utilizar void *.
Score_Under el
-2

Por supuesto. Si crea un objeto que existe fuera del alcance en el que lo usa. Aquí hay un ejemplo artificial (tenga en cuenta que mi sintaxis estará apagada; mi C está oxidada, pero este ejemplo aún ilustrará el concepto):

class MyClass
{
   SomeOtherClass *myObject;

   public MyClass()
   {
      //The object is created when the class is constructed
      myObject = (SomeOtherClass*)malloc(sizeof(myObject));
   }

   public ~MyClass()
   {
      //The class is destructed
      //If you don't free the object here, you leak memory
      free(myObject);
   }

   public void SomeMemberFunction()
   {
      //Some use of the object
      myObject->SomeOperation();
   }


};

En este ejemplo, estoy usando un objeto de tipo SomeOtherClass durante la vida de MyClass. El objeto SomeOtherClass se usa en varias funciones, por lo que he asignado dinámicamente la memoria: el objeto SomeOtherClass se crea cuando se crea MyClass, se usa varias veces durante la vida útil del objeto y luego se libera una vez que se libera MyClass.

Obviamente, si este fuera un código real, no habría ninguna razón (aparte del posible consumo de memoria de la pila) para crear myObject de esta manera, pero este tipo de creación / destrucción de objetos se vuelve útil cuando tiene muchos objetos y desea controlarlos con precisión. cuando se crean y destruyen (para que su aplicación no consuma 1 GB de RAM durante toda su vida útil, por ejemplo), y en un entorno de ventana, esto es bastante obligatorio, ya que los objetos que crea (botones, por ejemplo) , debe existir fuera del alcance de cualquier función en particular (o incluso de clase).

El Pitufo
fuente
1
Je, sí, eso es C ++, ¿no? Es increíble que hayan tardado cinco meses en llamarme.
TheSmurf