Pregunta

Siempre he escuchado que en C tienes que observar realmente cómo administras la memoria. Y todavía estoy empezando a aprender C, pero hasta ahora no he tenido que hacer ningún tipo de gestión de la memoria en relación con las actividades. Siempre me imaginé que tenía que liberar variables y hacer todo tipo de cosas feas. Pero este no parece ser el caso.

¿Puede alguien mostrarme (con ejemplos de código) un ejemplo de cuándo deberías hacer algo de " administración de memoria " ?

¿Fue útil?

Solución

Hay dos lugares donde las variables se pueden poner 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 la pila se liberan automáticamente cuando salen del ámbito (es decir, cuando el código ya no puede alcanzarlas). Podrías escucharlos llamados " automático " variables, pero eso ha pasado de moda.

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

La pila está bien 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 para el vinculador de Microsoft, la pila se establece en 1 MB, y no todo está disponible para sus variables.

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

El plan B se llama el " montón " Por lo general, puede crear variables tan grandes como el sistema operativo le permita, pero debe hacerlo usted mismo. Las 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 a través de punteros)

Una vez que creas una variable de pila, el problema es que el compilador no puede decir cuándo terminaste con ella, por lo que pierdes la liberación automática. Ahí es donde " liberación manual " a la que se refería entra. Su código ahora es responsable de decidir cuándo ya no se necesita la variable, y liberarla para que la memoria pueda tomarse para otros fines. Para el caso anterior, con:

free(p);

¿Qué hace que esta segunda opción "negocios desagradables" es que no siempre es fácil saber cuándo ya no se necesita la variable. Si olvida liberar una variable cuando no la necesita, su programa consumirá más memoria de la que necesita. Esta situación se denomina " fuga " ;. El " filtrado " la memoria no se puede usar para nada hasta que su programa finalice y el sistema operativo recupere todos sus recursos. Incluso es posible que surjan problemas más desagradables si libera una variable del montón por error antes de que realmente haya terminado con ella.

En C y C ++, usted es responsable de limpiar las variables del montón como se muestra arriba. Sin embargo, hay lenguajes y entornos como Java y .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 usted paga una multa en gastos generales y rendimiento. Es un equilibrio.

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

Otros consejos

Aquí hay un ejemplo. Supongamos 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 el puntero en el primer bloque de memoria cuando llamó a strdup la 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 agotado 11 gigas de memoria (posiblemente más, dependiendo de su administrador de memoria) y si no se ha bloqueado, es probable que el proceso se esté ejecutando muy lentamente.

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

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

¡Espero que este ejemplo ayude!

Tienes que hacer " gestión de memoria " cuando desee usar memoria en el montón en lugar de la pila. Si no sabe qué tan grande es hacer una matriz hasta el tiempo de ejecución, entonces tiene que usar el montón. Por ejemplo, es posible que desee almacenar algo en una cadena, pero no sabe 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

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

En C, la responsabilidad de garantizar que los punteros apuntan a la memoria que posee es solo suya. Esto requiere un enfoque organizado y disciplinado, a menos que abandone los punteros, lo que hace que sea difícil escribir una C efectiva.

Las respuestas publicadas hasta la fecha se concentran en las asignaciones de variables automáticas (de pila) y de pila. El uso de la asignación de la pila genera una memoria conveniente y administrada automáticamente, pero en algunas circunstancias (grandes memorias intermedias, algoritmos recursivos) puede conducir al terrible problema del desbordamiento de la pila. Saber exactamente cuánta memoria puede asignar en la pila depende mucho 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 forma segura.

La asignación del montón es menos inherente al lenguaje. Básicamente es 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 ('gratis'). Suena simple, pero está asociado con un dolor de programador no dicho. Los problemas son simples (liberar la misma memoria dos veces o nada [pérdidas de memoria], no asignar suficiente memoria [desbordamiento del búfer], etc.) pero difíciles de evitar y depurar. Un enfoque altamente disciplinado es absolutamente obligatorio en la práctica, pero, por supuesto, el lenguaje no lo exige realmente.

Me gustaría mencionar otro tipo de asignación de memoria que ha sido ignorada por otras publicaciones. Es posible asignar variables de forma estática al declararlas fuera de cualquier función. Creo que, en general, este tipo de asignación tiene una mala reputación porque es usado por 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 de espagueti. El método de asignación estática se puede utilizar simplemente para evitar algunos de los escollos del montón y los métodos de asignación automática. Algunos programadores de C se sorprenden al saber que se han construido grandes y sofisticados programas de C integrados y juegos sin ningún uso de asignación de montón.

Aquí hay algunas respuestas geniales sobre cómo asignar y liberar memoria, y en mi opinión, el lado más desafiante de usar C es garantizar que la única memoria que use sea la memoria que ha asignado, si no se hace correctamente lo que terminas es el primo de este sitio, un desbordamiento de búfer, y es posible que estés sobrescribiendo la memoria que está utilizando 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 un nulo - \ 0). Si su asignación de cadena fue

myString = "abcde";

Usted estaría asignando " abcde " en los 5 bytes que ha asignado a su programa, y ??el carácter nulo final se colocará al final de este - una parte de la memoria que no se ha asignado para su uso y podría ser gratuita, pero igualmente 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 (ya veces irrepetibles).

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 en silencio. 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 " cable " la dirección virtual 0x00000000 a excepciones de protección general para interceptar el uso del puntero nulo.

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

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

(Estoy escribiendo porque siento que las respuestas hasta ahora no están del todo bien).

La razón por la que debe mencionarse la administración de 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 de una vez, es un error). Por lo general, la primera estructura de datos que necesitará aprender es una especie de list . Aquí hay un enlace único, de la parte superior 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 tener algunas otras funciones, pero básicamente, para eso necesita la administración de memoria. Debo señalar que hay varios trucos posibles con " manual " gestión de memoria, por ejemplo,

  • El hecho de que malloc esté garantizado (según el estándar de idioma) para devolver un puntero divisible por 4,
  • asignar espacio adicional para un propósito siniestro propio,
  • creando grupo de memoria s ..

Consigue un buen depurador ... ¡Buena suerte!

@ 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 arreglárselas solo con las variables de la pila. Si su función necesita devolver un puntero, entonces tiene que malloc y tratar con la administración de la memoria.

  

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

Tienes razón, por supuesto. Creo que siempre ha sido así, aunque no tengo una copia de K & amp; R para verificar.

No me gustan mucho las conversiones implícitas en C, así que tiendo a usar conversiones para hacer "magia" más visible. Algunas veces ayuda a la legibilidad, otras no, y otras veces causa que el compilador atrape un error silencioso. Aún así, no tengo una opinión firme sobre esto, de una manera u otra.

  

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

Sí ... me atrapaste allí. Pasé mucho más tiempo en C ++ que en C. Gracias por notarlo.

En C, en realidad tienes dos opciones diferentes. Uno, puedes dejar que el sistema administre la memoria por ti. Alternativamente, puedes hacerlo solo. En general, querrá atenerse a lo primero tanto como sea posible. Sin embargo, la memoria administrada automáticamente en C es extremadamente limitada y en muchos casos necesitará administrar la memoria manualmente, como por ejemplo:

a. Desea que la variable sobreviva a las funciones y no desea tener una variable global. ej .:

struct pair{
   int val;
   struct pair *next;
}

struct pair* new_pair(int val){
   struct pair* np = malloc(sizeof(struct pair));
   np->val = val;
   np->next = NULL;
   return np;
}

b. quieres 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);
for(i=0; i

c. You want to do something REALLY dirty. For example, I would want a struct to represent many kind of data and I don't like union (union looks soooo messy):

struct data{ int data_type; long data_in_mem; }; struct animal{/*something*/}; struct person{/*some other thing*/}; struct animal* read_animal(); struct person* read_person(); /*In main*/ struct data sample; sampe.data_type = input_type; switch(input_type){ case DATA_PERSON: sample.data_in_mem = read_person(); break; case DATA_ANIMAL: sample.data_in_mem = read_animal(); default: printf("Oh hoh! I warn you, that again and I will seg fault your OS"); }

Vea, 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ás alejarte de tus trucos favoritos (T___T). Romperá su sistema operativo, tarde o temprano, si los usa con demasiada frecuencia. Siempre y cuando no uses * alloc y free, es seguro decir que sigues siendo virgen y que el código aún se ve bien.

Claro. Si creas un objeto que existe fuera del ámbito en el que lo usas. Aquí hay un ejemplo artificial (ten en cuenta que mi sintaxis estará desactivada; 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 útil 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 para controlar con precisión cuándo se crean y destruyen (para que su aplicación no absorba 1 GB de RAM durante toda su vida útil, por ejemplo), y en un entorno con ventanas, esto es prácticamente obligatorio, como objetos que crea (botones , digamos), deben existir fuera del alcance de cualquier función particular (o incluso de la clase).

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top