Manejo de Memoria Una introducción informal Universidad Técnica Federicto Santa Marı́a José Luis Canepa 17 de septiembre de 2009 Índice 1. Introducción 2 2. Entendiendo como se almacena 2.1. Solo hay Bytes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.2. La magia de memcpy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.3. Los structs no se quedan fuera . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 2 2 3 3. Los malditos punteros 3.1. Los punteros son números sobrevalorados . . . . . . . . . . . . . . . . . . . . . . . . . . 3.2. ¿Qué hace C con los punteros? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 4 4 4. Function Stack y Scopes 4.1. Callstack . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.2. Scope . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.3. Scope y Punteros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 4 5 5 5. Memoria dinámica 5.1. Malloc . . . . . . . . . . . . . 5.2. Free . . . . . . . . . . . . . . 5.3. Malloc, punteros y funciones 5.4. Otros errores de Stack . . . . 6 6 7 7 8 . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1. Introducción Para poder programar de manera adecuada las tareas de Estructura de Datos que demanden manejo de memoria intensivo (Listas, Pilas, Colas, Árboles, ...) y frecuente, se debe entender un claro entendimiento de como se maneja la memoria, y en nuestro caso particular, de como se tiene que implementar en C. Después de leer esta guı́a, usted deberı́a ser capaz de poder manejar todos los conceptos relacionados al manejo de memoria y punteros en C. Además de apreciar principios básicos, como “scope” de variables. 2. 2.1. Entendiendo como se almacena Solo hay Bytes Cuando ustedes utilizan ints, floats, chars o cualquier otro tipo de datos, incluyendo structs caseros, todos estos quedan en algún espacio de memoria. El hecho es, los nombres como enteros y floats son gracias del compilador, en la memoria, no existe ninguna forma de distinguir cual es cual, en efecto, se podrı́a hacer casting de un tipo de variable a otra completamente distinta (e.g char[4] a int). Y esto se debe a que, al final de cuentas, todo esta almacenado en Bytes. Para hacerse una idea del tamaño de cada uno: long int long int long puntero puntero Tipo char int (32 bit) (64 bit) long int float double (32 bit) (64 bit) Tamaño (Bytes) 1 4 4 8 8 4 8 4 8 Cuadro 1: Tamaños de primitivos de C. 2.2. La magia de memcpy En efecto, se puede utilizar memcpy() para escribir una dirección de memoria y se podrı́a rellenar simplemente con cualquier cosa, siempre y cuando se conozcan los tamaños de cada uno de ellos. #include <stdio.h> #include <stdlib.h> #include <string.h> int main(int argc, char **argv) { char string[16]; int array[4] = {0x616c6f48, 0x6e754d20, 0x00006f64}; 2 memcpy(string, array, 11); printf("%s", string); return 0; } Este código simplemente copia el contenido del arreglo de números (en hexadecimal) en uno de strings. Obviamente, los números se eligieron de manera que produjera el conocido ”Hola Mundo”. En la memoria esto ser verı́a ası́: ints char(hex) char(dec) char(ascii) corregido 0x61 97 a H 0x616c6f48 0x6c 0x6f 108 111 l o o l 0x48 72 H a 0x6e754d20 0x75 0x4d 117 77 u M M u 0x6e 110 n 0x20 32 n 0x00 0 0 d 0x00006f64 0x00 0x6f 0 111 0 d o 0 0x64 100 o 0 Cuadro 2: Hola Mundo en hexadecimal, nótese que mi computador guarda cada 4 bytes al revés. C se encargarı́a de esto automáticamente Como se puede apreciar en el ejemplo, memcpy reemplazó el contenido de memoria directamente sobre el string (procurando invertir en el caso de mi computador) para reemplazar el contenido individual de cada “celda”. 2.3. Los structs no se quedan fuera ¿Para que podrı́a servirnos esto? Piensen en un struct que contenga struct X { int x; char str[3]; char *x; }; ¿Cuál serı́a el largo de este struct? Al consultar la tabla de arriba, sabemos el tamaño individual de cada uno de los tipos dentro de este struct, y es tan simple como que cada cosa esta continua, una al lado de la otra. sizeof (structX) = sizeof (int) + 3 ∗ sizeof (char) + sizeof (char∗) = 4 + 3 + 4 = 11 (1) Y no solo eso, sino que se preserva el orden, si se hiciera un casteo como en el caso anterior, cada uno de estos datos serı́a reemplazado ints char struct 0x???????? ? ? ? ? int 0x???????? ? ? ? ? char[3] 0x???????? ? ? ? ? char* ? Conste que memcpy no es la única función que hace esto. fread es un buen ejemplo de otra función que pisa memoria directamente. 3 3. Los malditos punteros 3.1. Los punteros son números sobrevalorados Por si fuera poco, los punteros siempre han sido números, con una cualidad distinta a los demás. En vez de utilizar sus 4 Bytes (u ocho en 64-bit) para almacenar un número que nos importe, almacenan una “dirección de memoria”. Esto es lo que siempre confunde a todos los que ven los punteros por primera vez, para hacerlo mas simple, tomemos un arreglo como el siguiente: int x[4] 128 34 0 -15 El nombre x queda almacenado en una tabla interna en nuestro espacio de memoria, que asocia el sı́mbolo (x) a una dirección de memoria. Podemos ver esta dirección de memoria con printf: printf("%p", x); Y vemos que guarda un valor hexadecimal bastante feo. Pero como vimos antes, los hexadecimales se usan, simplemente para ahorrar tener que escribir un número gigante (232 ). O sea, simplemente, es un número. 3.2. ¿Qué hace C con los punteros? Entonces, si miramos la tabla anterior, ¿Cómo podrı́amos encontrar el elemento 2? Tenemos un puntero base (x) y sabemos cuanto miden cada una de esas celdas (son ints, ası́ que debiesen ser 4 Bytes). Entonces, para poder llegar a un elemento i dentro del arreglo, el compilador, al leer esto: x[i]; Internamente está haciendo lo siguiente: ∗(x + i · sizeof (int)) (2) En otras palabras, puede determinar, por simple aritmética, donde se encuentra el elemento i dentro del arreglo. Es por esto que les dijeron que los arreglos, en el fondo, eran punteros al primer elemento. int x[4] 128 x 34 x+4 0 x+8 -15 x+12 Con esto vemos, los punteros son simples y malditos números. Pronto volveremos a saber mas de ellos, pero primero, hay que entender algo importante. 4. 4.1. Function Stack y Scopes Callstack ¿Recuerdan el stack? ¿Esa estructura de datos que pone una cosa sobre otra? ¿“Una pila”? La gran pregunta que todo el mundo se hace al verla por primera vez es: ¿Cuando m**** podrı́a ser útil? Bueno, C lo usa, no solo eso, Assembly lo usa. Para ponerlo de manera simple, todo lo que haces en C que no sea memoria dinámica, esta en un stack. Este se llama el stack de llamadas (Callstack). “¿Para qué? ” Bueno, una función debe recordar quien fue la que la llamo para poder volver al lugar que le corresponde después de haber terminado. Cuando una función termina de llamarse, vuelve a donde terminó. Todos hemos visto esto en acción. f(g(h(i(j())))) No tiene mucho sentido indagar en esto, porque después se pone mas complicado. Lo importante es que, el hecho de que todo esté en un stack, causa que exista... 4 4.2. Scope El “scope” se podrı́a traducir directamente como “ámbito”, que no esta muy alejado de lo que en verdad significa. El hecho de que las variables estén siendo guardada en el stack, significa que cuando se desapila una función, todas las variables con ellas se van. Es decir, las variables viven siempre y cuando su función viva. void func(int x, int y) { int z = 3; } Todas las variables de la función anterior mueren una vez que se acaba la función, no solo z, sino también x e y mueren. 4.3. Scope y Punteros “¿Pero y los punteros? ¡Siempre he visto que se comportan distinto! ” void func(int *x) { *x += 10; // Cambiamos el contenido de x } int main() { int x = 3; func(&x); return 0; } Ya habı́amos dicho que los punteros son también números. Los punteros entran dentro de la función, indican el espacio de memoria donde esta la variable y le sumamos 10 al valor contenida en ella. Ocurre que int *x también muere como cualquier otra p*** variable. En efecto, ¿qué pasa si quiséramos cambiar la dirección a la que apunta un puntero? void func(int *x) { x += 4; // Cambiamos la dirreción (direccion+4), esto no perdura } int main() { int x = 3; func(&x); return 0; } 5 ¿Esto funcionarı́a? Para ser sinceros, sı́ y no. Efectivamente desplazarı́amos el puntero, pero, una vez que termina la función, este vuelve a su normalidad, porque es una variable local. Ası́ es, los punteros se pasan por valor, solo que ese valor representa una referencia a otra cosa (en este caso a un “3” llamado x). “¿Cómo hago una función que cambie la dirección que apunta un puntero? ” Sencillo, dijimos que un puntero pasa una referencia a algo, entonces, lo que quieremos es cambiar esa referencia. En Castellano: Quieremos la referencia de la referencia. void func(int **x) { *x += 4; // Cambiamos la dirreción (direccion+4), esto perdura } Aquı́ todo se pone raro. **x es un “puntero al puntero”, si lo pusieramos, tablı́sticamente: Sı́mbolo x *x **x ¿Dónde está? 0x00000000 0x00000004 0x00000008 ¿Qué guarda? 3 0x00000000 0x00000004 (Obviamente, no podemos usar las direcciones 0x00000000 (¡segfault!), pero es por motivos de ejemplo). Entonces, al pasar un puntero, solamente le estamos dando la dirección por valor a la función, y la función ahora sabe que en dicha dirección puede hacer cosas. 5. Memoria dinámica 5.1. Malloc Luego de ver la sección sobre el stack, estarás preguntandote “¡Oh Todopoderoso Can, ¿Cómo puedo hacer que mis variables no mueran al terminar la función?!”. Y a esto respondo, malloc. Y ese dı́a, todos se regocijaron. El stack no es lo único que hay, hay otra sección del espacio del programa conocido como el “Heap” (Montı́culo). Es otra estructura de datos interna, que se encarga de mantener todas las cosas que metan en un elegante árbol. tipo *puntero = malloc(<tama~ no en bytes>); Supongo que recuerdas lo de que, al fin y al cabo, todo son Bytes. Perfectamente podemos hacer int *puntero = malloc(4); Porque sabemos que los enteros miden 4 Bytes, pero esto no le gustará mucho al compilador, prefiere que nos vayamos a la segura y usemos sizeof() int *puntero = malloc(sizeof(int)); ¡Pero hay más! ¿Recuerdan que los punteros y los arreglos son básicamente lo mismo? ¿Y que [] realiza una simple operación aritmética? Bueno, gcc (el compilador) es muy amigable, y nos permite declarar un arreglo casi automágicamente int *array = malloc(sizeof(int)*10); array[3] = 5; El compilador notó que estabamos encajando todo en un int, y decidió que cuando se usara el operador [] sobre array, harı́a un salto, tal como si fuera un arreglo normal de siempre. 6 5.2. Free Ah, pero no todo es feliz en el mundo del malloc. Dado que no hay un stack que indique cuando hay que matar las funciones, debemos matarlas nosotros. En efecto, esta es la razón por la que varios programas usan cantidades cerdas de memoria (¿Firefox?); a veces se les olvida soltar memoria. void perdida() { int *array = malloc(sizeof(int)*10); } En 3 lı́neas ya botamos 40 Bytes de nuestro programa a la basura. Ya no sabemos donde está, ni con quien se junta, porque perdimos su referencia. En efecto, esto puede ocurrir, casi inadvertidamente: void perdida() { int *array = malloc(sizeof(int)); // lalal operaciones // Otro pedido de memoria array = malloc(sizeof(int)); // mas operaciones free(array); } Pedimos un espacio de memoria, luego pedimos otro y borramos este último. El primero que fue pedido, pero jamás fue soltado. Otros lenguajes de alto nivel (Python, Java, ...) tienen recolectores de basura, que al detectar que una variable ha sido abandonada, las sueltan. Ni C, ni C++ tienen esto (D sı́). 5.3. Malloc, punteros y funciones Entonces tenemos funciones, punteros y malloc. ¿Qué pasa si los usamos todos? void magmamix(int *reservame, int size) { reservame = malloc(sizeof(int)*size); } Ah, que linda función. Lastima que este completamente mal, ¿Viste el problema? Si lo viste, felicitaciones, anda por una galleta. Aquı́ esta la versión correcta: void magmamix(int **reservame, int size) { *reservame = malloc(sizeof(int)*size); } Y ahora los que no lo vieron antes dirán: “Aaaaaaaaah...”. Estabamos tratando de cambiar la dirección de un puntero local, cuando debı́amos cambiar la dirección a la que apunta el puntero, vı́a otro puntero (Anda a leer el capı́tulo anterior, por pelotudo). Para los que todavı́a le tienen pánico a la idea de los punteros dobles, podemos usar otra cosa: 7 int* retorno(int size) { int *reservame = malloc(sizeof(int)*size); return reservame; } Si haz estado poniendo atención, los punteros son valores, y pueden ser devueltos tal cual, especialmente para este tipo de situaciones. Todo buen ejemplo, tiene un contra-ejemplo: int* retorno(int size) { int reservame[size]; return reservame; } Si no sabes a estas alturas por qué esta mal, deberı́as leer el capı́tulo sobre el stack. Para los que no han puesto atención: reservame es una variable local, tan pronto como la función se acabe, este estará despejado, por lo tanto, el puntero que retornemos apuntará a un espacio inválido. 5.4. Otros errores de Stack Entonces, ya entendemos que: 1. No hay que retornar punteros a variables de stack (locales). 2. Los punteros son números, y son pasados por valor. 3. No puedes cambiar hacia dónde apunta un puntero, sino que necesitas un puntero a puntero. 4. Si quieres memoria que perdure, malloc es tu amigo. 5. Pero si malloc es tu amigo, necesitas su hermano free. ¿Qué se nos queda? ¿Qué pasa si hacemos esto? typedef struct T_NODE { char title[150]; char author[100]; char album[100]; struct T_NODE *next; }node; node* new_node() { node* nnode = malloc(sizeof(node)); char title[150] = {"No hay titulo."}; nnode->title = title; 8 nnode->next = NULL; return nnode; } int main() { node *x = new_node(); return 0; } (¿Parece familiar?) Pareciera estar todo en orden... Ah pero miren nada mas: char title[150] = {"No hay titulo."}; nnode->title = title; Esto es una variable local, y no solo eso, el usar el operador “=” implica que estamos copiando el puntero de una variable local a una guardada de manera dinámica. node* new_node() { node* nnode = malloc(sizeof(node)); // Ahora si funciona strcpy(nnode->title, "No hay cancion"); nnode->next = NULL; return nnode; } Ası́ es, siempre que hagas referencia a una variable local, todo es bueno hasta que se acaba la función, y eso incluye structs. Inclusive si el struct hubiese estado definido ası́: typedef struct T_NODE { char *title; char author[100]; char album[100]; struct T_NODE *next; }node; Si quisieramos tener un tı́tulo de largo dinámico habrı́a que... node* new_node() { node* nnode = malloc(sizeof(node)); char* title = malloc(sizeof(char)*16); strcpy(title, "No hay titulo."); nnode->title = title; 9 nnode->next = NULL; return nnode; } Y con esto, tenemos todos los posibles casos cubiertos. 10