Unidad 8 1 Computación Avanzada Punteros. Dentro de la memoria de la computadora cada dato almacenado ocupa una o más celdas contiguas de memoria. El número de celdas de memoria requeridas para almacenar un dato depende de su tipo. Si 'v' es una variable que representa un determinado dato, el compilador automáticamente asigna celdas de memoria para ese dato dependiendo de su tipo. El dato puede ser accedido si conocemos su localización ( la dirección ) de la primera celda de memoria. La dirección de 'v' se determina mediante &v, donde & es un operador monario, llamado operador dirección, que proporciona la dirección del operando. Si la dirección de 'v' se le asigna a otra variable pv = &v; esta nueva variable es un puntero a 'v': representa la dirección de 'v' y no su valor. Los punteros son direcciones. El dato representado por 'v' puede ser entonces accedido mediante *pv, donde * es un operador monario, llamado operador indirección ( u operador de “dereferencia” ), que opera sólo sobre una variable puntero. En C, los punteros son una herramienta fundamental para construir programas. Sin ellos se podría hacer realmente poca cosa. Examinemos unos ejemplos que muestran donde los punteros son necesarios: 1.- Paso de parámetros por referencia. Ya hablamos de ésto en funciones. 2.- Declaración de arreglos dinámicos. Imaginemos que estamos haciendo un procesador de imágenes, y que tenemos una función que va a hacer cierto tratamiento. Recibe por parámetro el nombre del fichero a utilizar, y lo lee del disco, lo modifica, y lo guarda otra vez. Nuestro problema es que no sabemos de antemano lo que va a medir la foto, y por lo tanto, si no existieran los arreglos dinámicos, deberíamos declarar un buffer lo suficientemente grande como para que cupiese la foto más enorme que tuviéramos. ¿Qué estamos haciendo si actuamos de este modo? Si la foto es pequeña, estamos usando demasiada memoria, que puede hacer incluso que se agoten los recursos del ordenador. Si la foto es más grande de lo que habíamos previsto, nuestro programa no será capaz de tratarla, aunque la computadora tenga recursos suficientes. Unidad 8 2 Computación Avanzada 3.- Acceso a memoria física. C es el lenguaje comúnmente utilizado a la hora de programar módulos de sistemas operativos, drivers, etc. Este tipo de programas deben acceder con frecuencia al hardware de la maquina, para poder leer datos del exterior (movimientos de un ratón, temperatura de un sensor) o bien para escribir sobre ellos (dibujar en una pantalla, mover electroválvulas). Una de las maneras de hacer todo esto consiste en “mapear” los dispositivos de entrada/salida sobre un espacio real de memoria. De este modo, cuando se escribe en cierta posición de memoria lo que se está haciendo realmente es actuar sobre una salida, y cuando se lee de cierta posición de memoria se está leyendo una entrada. Por ejemplo, en un PC, la memoria de la pantalla en modo texto se encuentra en la posición 0B8000000H (también conocido como el par segmento:dirección B800:0000 aunque ésto no funcionará en Windows, solo en MS-DOS, ya que Windows protege a la máquina contra los programas de usuario bajo una capa de memoria virtual). A partir de esta posición, cada dos bytes codifican una letra de pantalla: el primer byte corresponde al código ASCII de la misma, y el segundo byte corresponde al código del color a aplicarle. Declaración de punteros Los punteros, como cualquier otra variable, deben ser declarados antes de ser usados. Cuando una variable puntero es definida, el nombre de la variable debe ir precedida por un *. Este identifica que la variable es un puntero. El tipo de dato que aparece en la declaración se refiere al tipo de objeto del puntero, el tipo de dato que se almacena en la dirección representada por el puntero, en vez del puntero mismo. Así, una declaración de puntero general es: tipo_dato *ptvar; donde ptvar es la variable puntero y tipo_dato el tipo de dato apuntado por el puntero. Dentro de la declaración, una variable puntero puede ser inicializada asignándole la dirección de otra variable que por supuesto debe estar previamente declarada. Los operadores de punteros. Los operadores monarios & y * son miembros del mismo grupo de precedencia que los otros operadores monarios: -,++,--,!,sizeof. Hay que recordar que este grupo de Unidad 8 3 Computación Avanzada operadores tiene mayor precedencia que el de los operadores aritméticos y la asociaciones de los operadores monarios es de derecha a izquierda. El operador dirección (&) sólo puede actuar sobre variables ordinarias o elementos simples de un arreglo. El operador indirección (*) sólo puede actuar sobre operandos que sean punteros. Las variables puntero pueden apuntar a variables numéricas o de caracteres, arreglos, funciones, otras variables puntero y también pueden apuntar a otros tipos de estructuras de datos más complejos. Por tanto a una variable puntero se le puede asignar la dirección de una variable ordinaria. También se le puede asignar la dirección de otra variable puntero, siempre que ambas apunten al mismo tipo de dato. Mostremos lo que hemos hablado hasta ahora con un ejemplo: #include<stdio.h> main(){ int v1, v2; int *pv; v1 = 555; // Asigna 555 a v1 pv = &v1; // pv recibe la dirección de v1. v2 = *pv; // Asigno a v2 el “contenido” de la posición de memoria pv. printf(“%i”,v2); // Imprime 555; return 0; } Esto hace algo como: Direcciones Contenido 0x00F0 v1 = 555 0x00F1 v1 0x00F2 v2 = 555 0x00F3 v2 Unidad 8 4 Computación Avanzada Direcciones Contenido ... 0X#### pv = 0x00F0 0X####+1 donde la dirección '####' no es realmente de importancia. Aquí se observa cómo los punteros contienen direcciones de memoria. 'v1' es una variable que contiene el valor de 555. 'pv' es un puntero ( almacenado en cualquier dirección #### que realmente no nos importa en este caso ) que contiene la dirección de memoria de 'v1', o sea, 0x00F0. Luego a 'v2' se le asigna el contenido de 'pv' ( El contenido de la localidad de memoria 0x00F0 que es 555 ). Por último el programa imprime la variable 'v2'. ¡¡¡ Fácil !!!, ¿ no es cierto ?, pues ese es el concepto detrás de los punteros. OJO: Es muy peligroso trabajar con punteros si éstos no han sido asignados a ninguna posición de variable con anterioridad. Esto se ve bien ya que si no ha sido asignada una dirección, el puntero podría contener una dirección importante que ya está siendo usada y podría variar su valor. Para evitar este tipo de conflictos se puede asignar a los punteros un valor predeterminado o inicializarlos a NULO ( NULL ). Esto nos indicaría que al puntero aun no se le ha asignado una dirección y por lo tanto no podrá ser usada. Ej: int *p=NULL; Expresiones con punteros. En general, las expresiones que involucran punteros se ajustan a las mismas reglas que cualquier otra expresión de C. Esto quiere decir que si tenemos: int x; int *p1, *p2; podemos hacer: p1 = &x; p2 = p1; ó hacer operaciones simples con punteros ó incluso comparaciones. P. ej.: Unidad 8 5 Computación Avanzada if (p1 == p2) printf(“Ambos apuntan al mismo sitio\n”); p2 = p1 +1; // Ésto es aritmética de punteros. p2--; p1++; Pero no se permiten otras operaciones aritméticas con punteros. Así una variable puntero no puede ser multiplicada por una constante, dos punteros no pueden sumarse, etc. Aritmética de punteros. Existen sólo dos operaciones aritméticas que pueden hacerse con punteros: la suma y la resta. En particular, un valor entero puede ser sumado o restado a una variable puntero, pero el resultado de la operación debe ser interpretado con cuidado. Supongamos que px es una variable puntero que representa la dirección de una variable x. Se pueden escribir expresiones como ++px, --px, (px+3). Cada expresión representa una dirección localizada a cierta distancia de la posición original representada por px. La distancia exacta será el producto de la cantidad entera por el número de bytes que ocupa cada elemento al que apunta px. Es por eso que NO ES LO MISMO UN PUNTERO A CHAR QUE UN PUNTERO A INT. Ambos punteros contendrán una dirección de memoria pero, por ejemplo, en el siguiente caso: char ch='a'; char *pch=&ch; // Asignamos al puntero pch la dirección de ch. // Supongamos que la dirección es 1000 pch++; // Incrementa pch a 1001 ya que los tipo char ocupan 1 byte. Sí, es cierto, no se ve complicaciones ya que pch contiene el valor que esperábamos. En cambio: int x=10; Unidad 8 6 Computación Avanzada int *px=&x; // Asignamos al puntero px la dirección de x. // Supongamos que la dirección es 1000 px++; // Incrementa px a 1002 ( no a 1001 ) ya que los tipo int ocupan 2 bytes. Ahora sí, px tendrá 1002, NO 1001 como quizá se esperaba. Esto es debido a que “toda la aritmética de punteros es relativa al tipo base”. Esto es de gran utilidad para manejar arreglos ( entre otras utilidades ) pero este punto lo estudiaremos más adelante. Las operaciones que se pueden realizar sobre punteros: 1. A una variable puntero se le puede asignar la dirección de una variable ordinaria (pv=&v). 2. A una variable puntero se le puede asignar el valor de otra variable puntero siempre que ambas apunten al mismo tipo de dato. 3. A una variable puntero se le puede asignar el valor nulo (NULL). 4. Una cantidad entera puede ser sumada o restada a una variable puntero. 5. Una variable puntero puede ser restada de otra con tal de que ambas apunten a elementos del mismo arreglo. ( OJO: este caso es muy especial ). 6. Dos variables puntero pueden ser comparadas siempre que ambas apunten a datos del mismo tipo. 7. Por último, como ya se comentó, no se permiten otras operaciones aritméticas con punteros. Así una variable puntero no puede ser multiplicada por una constante, dos punteros no pueden sumarse, etc. Funciones de asignación dinámica. Una vez compilados, todos los programas en C organizan la memoria de la computadora en cuatro regiones que contienen: el código del programa ( en lenguaje máquina ), los datos globales, la pila ( o Stack ) a través del cual se pasan argumentos y donde residen las variables locales y el montón ( o Heap ) que es el área de memoria libre que es gestionada por las funciones de asignación dinámica de C. Existen dos funciones básicas para trabajar con asignaciones dinámicas. Estas son: void *malloc(num_bytes); void free(void *p); Unidad 8 7 Computación Avanzada La función malloc() asigna “num_bytes” número de bytes de memoria y devuelve un puntero void ( sin tipo predeterminado. Si se desea que sea de algún tipo, debe usarse la expresión “(tipo *)” antes del malloc(). Ej: (int *)malloc(128); ) del comienzo de dicha memoria y free() devuelve al Heap la memoria previamente asignada para que pueda ser reutilizada. Ambas funciones están en el archivo de cabecera “stdlib.h”. Como comentamos en el punto “Declaración de arreglos dinámicos”, ésto es muy útil para todos esos casos en los que _NO_ sabemos con antelación de cual tamaño será la variable que vamos a usar. Supongamos el caso en que tenemos un programa que leerá caracteres de un archivo. Nosotros no sabemos con antelación de que tamaño será ese archivo. Así que podríamos crear un arreglo que contenga espacio suficiente como para almacenar el mayor arreglo posible... quizá 100.000 caracteres pero ésto es increíblemente ineficiente ya que la memoria se estaría usando aunque no fuese necesaria ( quizá para leer un archivo de tan sólo 4 bytes, por lo que nos quitaría memoria para otras operaciones realmente necesarias ). Mejor es que usáramos una asignación dinámica con el malloc(). Así, supongamos que leemos un archivo y este contiene 500 bytes, entonces nos bastaría hacer algo como: char *p; unsigned long int i; // leemos el archivo. Asignamos en i el número de bytes que contiene ( 500 bytes ). p=(char *)malloc(i); Después de la asignación, p apunta al primero de los 500 bytes de la memoria libre que ha sido reservada por el malloc(). Luego de usarlo, y si ya no es más necesaria su existencia, podemos devolver esa memoria al Heap (el Heap es región que queda disponible para las solicitudes de memoria dinámica al sistema operativo) para ser reutilizada. Esto se hace: free(p); Eso sí, como el Heap no es infinito, siempre que se asigne memoria es necesario antes de usarla comprobar el valor devuelto por malloc() y así asegurarse que no es nulo. Un puntero nulo indicará un error en la asignación... p. ej. que no hay memoria suficiente. Si se utiliza un puntero nulo el programa fallará. Por lo tanto, siempre es conveniente hacer algo como: Unidad 8 8 Computación Avanzada if((p=malloc(100)==NULL) { printf(“No hay memoria\n”); exit(1); } También existe una función muy importante en C llamada sizeof(tipo). Esta función retorna la cantidad de bytes que ocupa una variable de tipo “tipo” lo que asegura un buen funcionamiento del programa sin importar el compilador lo que a su vez le da gran portabilidad al código además del hecho de que así no tenemos que memorizar los tamaños de todos los tipos ni calcular con anterioridad el tamaño de un tipo de dato creado. Supongamos que necesitamos espacio para guardar 10 variables de tipo double, podemos hacer: p = malloc(10*sizeof(double));