Manejo de Memoria Una introducción informal Universidad

Anuncio
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
Descargar