Tema 13 Punteros Índice 1.− Introducción 2.− Operaciones 3.− Aritmética de punteros 4.− Punteros y vectores/matrices 5.− Punteros y estructuras 6.− Asignación dinámica de memoria 7.− Punteros y funciones: − Paso por referencia − Devolución de punteros − Vectores/matrices como parámetros − Punteros a función Tema 13 Punteros Pág 1 Introducción Los punteros son uno de los tipos de datos más temidos de C. Sin embargo, también son los más versátiles y los que mayor potencia proporcionan. Su uso permite el paso de argumentos por referencia, la construcción de estructuras dinámicas de datos y la mejora en la velocidad de ejecución de algunos algoritmos. Sin embargo, mal usados pueden producir errores graves, difíciles de detectar y, en ocasiones, los peores errores posibles: los no reproducibles. Un puntero es una variable que contiene una dirección de memoria. En la mayoría de los sistemas operativos actuales, se suele tratar de un número entero de cuatro u ocho bytes (32 o 64 bits). Recordemos de temas anteriores que todo objeto de un programa se debe situar en memoria principal (ya sea en la pila, en el montón, en el segmento de código o en el de datos). Cada objeto del programa tiene, por tanto, una posición de memoria (aquella en la que comienza a almacenarse) que lo identifica. Los punteros se usan para almacenar posiciones (direcciones) de memoria de otros objetos del programa. Un puntero puede considerarse como una referencia a otro objeto del Tema 13 Punteros Pág 2 programa (una variable, una función...). Los punteros son variables, exactamente igual que las demás. Se declaran junto con el resto, usando la siguiente sintaxis: <tipo> *<nombre>; Donde <nombre> es el nombre de la variable puntero y tipo es el tipo de objeto cuya dirección se va a almacenar. Es importante que se sepa a qué va a apuntar un puntero, por motivos que veremos luego. Ejemplos: int *punt; /* puntero a entero */ float *v, *w, *z; /* punts. a real */ struct coche *a, *b; /* punts. a estructura */ void *pos; /* puntero genérico */ Los punteros a void son un tipo especial de puntero que no están asociados a un tipo particular. Una vez declarado, a un puntero se le pueden asignar valores, se pueden realizar operaciones con él, se puede usar su valor en expresiones... Tema 13 Punteros Pág 3 Operaciones Sobre una variable puntero se pueden realizar una serie de operaciones que veremos a continuación: − Dirección: el operador de dirección no es realmente uno que se aplique sobre las variables puntero (normalmente), sino sobre otros tipos de variable. Éste operador, representado con el símbolo ampersand (&) obtiene la dirección de memoria de la variable a la que precede. Así, si la variable entera w está almacenada en la posición de memoria 32012, la operación int *punt; punt= &w; asignará el valor 32012 a la variable punt. − Indirección: el operador de desreferenciación o de indirección sí se aplica a valores de tipo puntero. Éste operador se representa por un asterisco (*) y devuelve un valor del tipo apuntado por el operando. Este valor es el contenido en la posición apuntada por el puntero. Así, en el siguiente código float *p; float q= 1.44; p= &q; print("%f\n", *p); Tema 13 Punteros Pág 4 el valor que se imprime es 1.44, ya que p apunta a la dirección de q. En general, si el puntero x apunta a un tipo de datos T, la expresión *x es de tipo T. − Incremento, decremento: los valores de tipo puntero se pueden incrementar y decrementar, siempre en valores enteros. Se admiten los operadores ’+’, ’−’, ’++’ y ’−−’. − Resta de punteros: se puede hallar la diferencia entre dos punteros, que es la distancia que separa las direcciones a las que apuntan en memoria. − Impresión: para mostrar el valor de un puntero se usa un especificador de formato especial en la instrucción printf: ’%p’. Únicamente se permiten estas operaciones entre punteros. Aritmética de punteros Las operaciones de incremento, decremento y resta de punteros tienen una semántica especial. Cuando un puntero p se incrementa o decrementa en una cantidad X, no se modifica la dirección apuntada en X bytes, sino en X elementos del tipo apuntado por p. Es decir, p se incrementa o decrementa en Tema 13 Punteros Pág 5 sizeof(T) bytes. Así, por ejemplo (suponiendo sizeof(char)==1 y sizeof(float)==4): char *p= 20232; float *q= 20232; p++; /* p vale 20233 */ q++; /* q vale 20236 */ p= p+5; /* p vale 20238 */ q= q+2; /* q vale 20246 */ La resta de punteros debe hacerse siempre entre punteros del mismo tipo (es decir, que apunten al mismo tipo de datos) y el resultado no es el número de bytes que los separan, sino el número de elementos. Punteros y vectores/matrices Hay que hacer unas cuantas consideraciones acerca de cómo entiende C los vectores: − Para C, un vector es un puntero que apunta a una zona de memoria reservada en tiempo de compilación. − El nombre de un vector es un puntero al inicio del mismo. − Cuando C analiza una expresión del tipo vector[indice] realmente la convierte a otra de tipo *(vector+indice) Tema 13 Punteros Pág 6 Y, cosa curiosa, la mayoría de los compiladores de C admiten las expresiones indice[vector] Un ejemplo que ilustra estos puntos: char cadena[80], *p; strcpy(cadena, "palabra"); p= cadena; /* el nombre es el puntero */ /* imprimen lo mismo */ printf("%c, %s\n", cadena[5], cadena ); printf("%c, %s\n", *(p+5), p ); En cuanto a las matrices, C las almacena en memoria disponiendo sus elementos por filas de forma consecutiva. Así, los elementos de una matriz de 3x4 estarían así: m00 m01 m02 m03 m10 m11 m12 m13 m20 m21 m22 m23 Por otra parte, pudiera parecer que una matriz de dos dimensiones es un vector de vectores (o de punteros, que es igual), y que C la almacena así: m0 m00 m01 m02 m03 m1 m2 m10 m11 m12 m13 m20 m21 m22 m23 Tema 13 Punteros Pág 7 Donde las filas podrían estar en zonas no contiguas de memoria. Sin embargo, no es así. Todos los elementos están seguidos en la memoria. Pero, como caso particular, las expresiones m[0], m[1] y m[2] sobre la matriz de nuestro ejemplo producen las direcciones donde comienzan las filas primera, segunda y tercera respectivamente. Es decir: int m[3][4], *fila, f, c; for(f=0; f<3; f++) { fila= m[f]; for(c= 0; c<4; c++) printf("%d\n", fila[c]); } Pero recordemos que esto no es porque m sea un vector de punteros a vectores, sino porque el compilador nos facilita la tarea permitiéndonos hablar de m[f]. Sabiendo que las matrices se almacenan de forma contigua, una forma bastante común de acceder a un elemento de una matriz bidimensional de FxC a partir de la posición de inicio de la matriz es mediante la fórmula siguiente: elemento(fila, columna)= *(puntero+(fila*C)+columna) Esto nos permitirá, como veremos más tarde, pasar matrices de cualquier tamaño a una función. Tema 13 Punteros Pág 8 Punteros y estructuras Un puntero puede estar asociados a cualquier tipo de datos, incluídas estructuras (y, por supuesto, punteros). Cuando se tiene un puntero a estructura, se puede acceder a los campos de la misma de dos formas diferentes: desreferenciando el puntero y aplicando el operador ’.’ o directamente sobre el puntero, aplicando el operador ’−>’: struct s { int a; float b; }; struct s var, *punt; punt= &var; var.a= 143; printf("%d, %d, %d\n", var.a, (*punt).a, punt−>a ); Debe notarse que, debido a que la precedencia del operador ’.’ es superior a la del operador ’*’, hay que usar paréntesis para desreferenciar antes de acceder al campo. Los punteros a estructuras son muy usados cuando se trata con estructuras dinámicas de datos, tal y como veremos en temas posteriores. Tema 13 Punteros Pág 9 Asignación dinámica de memoria Uno de los usos más comunes de los punteros es mantener referencias a zonas de memoria dinámica. Tal y como se vio en un tema anterior, existe una zona de la memoria asignada al programa que se utiliza para almacenamiento de datos dinámicos. La gestión de los datos dinámicos se realiza mediante llamadas al sistema que permiten reservar y liberar zonas de memoria. Estas llamadas suelen devolver o aceptar como parámetros valores de tipo puntero. Para la gestión de memoria dinámica en C se usan varias funciones, cuyos prototipos están declarados en el fichero de cabecera ’stdlib.h’. En primer lugar, la función void *malloc( int n ) permite reservar una zona de memoria dinámica de n bytes de longitud. La dirección de inicio de dicha zona se devuelve como resultado. Si no se consiguió reservar esa cantidad de bytes, se devuelve el valor NULL. Como se ve, el tipo de retorno de malloc es un puntero genérico (void *), que deberá ser convertido al tipo adecuado según lo que se vaya a almacenar en la zona reservada. Tema 13 Punteros Pág 10 Un ejemplo del uso de malloc para reservar espacio para un valor float sería éste: float *p; p= (float *)malloc( sizeof(float) ); if( p==NULL ) { perror("Fallo en malloc()"); exit(1); } else *p= 3.141592; Otro ejemplo, en el que se reserva espacio para un vector de 10 estructuras, sería: struct s { int a; float b; }; struct s *p; p= (struct s *)malloc(10* sizeof(struct s)) if( p==NULL ) { perror("Fallo en malloc()"); exit(1); } else { p[3].a= 32; p[0].b= 1.45; p[9].a= 4365; } Debe observarse que, al ser un vector de estructuras, no se necesita el operador ’−>’, puesto que la notación p[x] devuelve una estructura, no un puntero. Tema 13 Punteros Pág 11 Una macro que puede ser de utilidad para agilizar las llamadas a malloc es la siguiente: #define NEW(x,n) (x*)malloc(n*sizeof(x)) int *p; struct s *vec; p= NEW(int, 1); vec= NEW(struct s, 120); Sigue siendo necesario, por supuesto, comprobar si malloc ha retornado NULL. La dirección devuelta por malloc está siempre convenientemente alineada para almacenar cualquier tipo de dato (en las arquitecturas que necesitan alineación), evitando así la posibilidad de un error de bus (ver tema de gestión de memoria en tiempo de ejecución). Una función alternativa a malloc es void *calloc( int tam, int cant ) que reserva un número de bytes igual a tam*cant y pone a cero el espacio reservado antes de devolver la dirección de inicio. Si se necesita cambiar el tamaño de una zona de memoria reservada por malloc o calloc, ya sea para ampliarlo o para reducirlo, se puede usar la función Tema 13 Punteros Pág 12 void *realloc(void *ant, int tam) que cambia el tamaño de la zona apuntada por ant a tam bytes y devuelve la posición de la nueva zona. El cambio de tamaño puede implicar mover los datos dentro de la memoria, y la propia función se ocupa de ello. Si no se puede reservar la nueva memoria, realloc devuelve NULL pero los datos originales siguen en su sitio (ant). Cuando se usan pequeñas cantidades de memoria dinámica en un programa que va a ejecutarse durante poco tiempo, no se suelen plantear problemas. Pero si la cantidad de memoria dinámica a usar es grande o el programa debe estar ejecutándose de manera indefinida, puede suceder que se agote la memoria asignada al programa. Por lo tanto, y en cualquier caso, deben liberarse las zonas de memoria dinámica que se dejen de utilizar. Para ello se usa la función void free( void *zona ) Al terminar un programa se libera automáticamente toda la memoria que ocupa, por lo que en programas cortos no sería necesario usar free. Sin embargo, es aconsejable hacerlo para acostumbrarse. Cuando un programa largo olvida liberar parte de la memoria dinámica que reserva, Tema 13 Punteros Pág 13 se produce lo que se llama una ’fuga de memoria’ (memory leak) que puede acabar por agotar la memoria asignada y detener el programa. Existen bibliotecas de funciones que substituyen malloc, calloc, realloc y free por versiones especiales que, aunque más lentas, llevan una contabilidad de las zonas de memoria reservadas y liberadas y permiten detectar las fugas de memoria. Punteros y funciones Paso por referencia: Uno de los usos de los punteros es el paso de parámetros por referencia a una función. Recordemos que los parámetros de una función se pueden pasar por valor o por referencia. Si se pasan por valor, las modificaciones que se hagan sobre ellos no se verán en el programa principal. Si se pasan por referencia, sí se reflejan en el programa principal estas modificaciones. En C, todos los parámetros de las funciones se pasan por valor, sin excepción. Para simular un paso de parámetro por referencia en C, lo que se hace es pasar un puntero al objeto que se pasa. Así, la función tiene acceso no sólo al valor del parámetro sino también a su situación en memoria, lo que permite su modificación. Tema 13 Punteros Pág 14 Un ejemplo de paso de un entero por referencia a una función que lo incremente: int x= 14; inc(&x); ... void inc(int *par) { (*par)++; } El paso por referencia posibilita que una función pueda generar más de un valor. La función devolverá (con return) uno de los valores de retorno y almacenará en parámetros pasados por referencia el resto de los valores. Un ejemplo: int JulianoAdma( int jul, int *dia, int *mes, int *anno ) Esta función convierte una fecha expresada en días Julianos (una forma de representar fechas) a día, mes y año. Devuelve cero si todo ha ido bien y 1 si se produjo la fecha juliana no es válida. Como se ve en este ejemplo, si una función devuelve varios valores, se suele utilizar el principal (el devuelto por return) como código de error y los parámetros por referencia para el resto de los valores. Tema 13 Punteros Pág 15 Devolución de punteros: Una función puede retornar un tipo de datos puntero. La función se declararía así tipo *función( argumentos ) Este tipo de funciones se suelen usar para reservar memoria o crear elementos en estructuras dinámicas de datos. Debe notarse que cuando una función devuelve una dirección de un objeto reservado con malloc no hay por qué declarar nada estático. La memoria dinámica es global, y está accesible desde cualquier punto del programa, siempre que se tenga el puntero adecuado. Vectores/matrices como parámetros: Como se ha dicho más arriba, C considera que un vector (o una matriz, es igual) es un puntero que señala a una zona de memoria reservada en tiempo de compilación y capaz de albergar los elementos del mismo. Y el nombre del vector es equivalente a la dirección de comienzo de esta zona. Por lo tanto, pasar un vector o una matriz a una función es lo mismo que pasarle un puntero al tipo almacenado en el vector o matriz. En la llamada a Tema 13 Punteros Pág 16 la función se colocará como parámetro real el nombre de la variable, y en la declaración y definición se usará, en caso de que se trate de un vector, una de estas notaciones: void funcion( int *vector ) void funcion( int vector[] ) void funcion( int vector[TAMANO] ) Como se ve, en el caso de los vectores se puede obviar el tamaño. No es así en el caso de las matrices, ya que para acceder a un elemento de una matriz, según la fórmula que vimos anteriormente, se deben conocer al menos todas las dimensiones de la matriz salvo la primera. Por lo tanto, si se quiere construir una función que trabaje con matrices de tamaño fijo y conocido, se puede declarar de una de estas formas: void funcion( float mat[FILAS][COLS] ) void funcion( float mat[][COLS] ) Pero si se quiere crear una función genérica (algo muy recomendable) que permita trabajar con cualquier tamaño de matriz, se declararía como void funcion( float *mat, int f, int c ) (se debe especificar obligatoriamente el número de columnas, aunque las filas se suelen pasar también). En el interior de la función, cada elemento mat[fila][col] se accedería así: Tema 13 Punteros Pág 17 *(mat+fila*c+col) Punteros a función: En C se puede también mantener en un puntero la dirección de inicio del código de una función. Este puntero es un puntero a función. Se declara así: retorno (*punt) ( argumentos ); Donde <retorno> es el tipo de datos que devuelve la función y <argumentos> es la lista de parámetros de la misma. Así, para declarar un puntero a una función que devuelve un entero y acepta una cadena, un float y un vector de floats, tendremos: int (*pt)(char v[]); *cad, float x, float Y para asignar un valor a un puntero a función necesitamos a) una función del mismo tipo al que apunta el puntero y b) obtener la dirección de comienzo de dicha función en memoria. Supongamos que tenemos la función int f( char *nom, float vel, float z[]); Tema 13 Punteros Pág 18 Podremos asignar su dirección al puntero pt usando el nombre de la función sin paréntesis: pt= f; y luego podríamos llamar a la función de una de tres formas diferentes: f( "pepe", 123.2, vec ); (*pt)( "pepe", 123.3, vec ); pt( "pepe", 123.3, vec ); Las aplicaciones de los punteros a función son múltiples. Propondremos dos fragmentos de código de ejemplo. En el primero, se crea una función tabula() que acepta un valor inicial, un valor final, un incremento y un puntero a función. Muestra una tabla de valores f(inicial), f(inicial+incremento)... f(final) para la función cuyo puntero se haya pasado: void tabula( double ini, double fin, double inc, double (*fun)(double x) ); double prueba(double arg); ... tabula(1, 100, 1, sin); tabula(−10, 10, 0.1, prueba); tabula(0, 10, 0.2, log); ... void tabula( double ini, double fin, double inc, double (*fun)(double x) ) Tema 13 Punteros Pág 19 { double x, y; for( x=ini; x<=fin; x+=inc ) { y= fun(x); printf("%f −> %f\n", x, y ); } } Se puede, incluso, declarar un vector de punteros a función double (*vec[3])(double)= { sin, prueba, log }; Y llamar a tabula(1, 10, 0.2, vec[loquesea]); El segundo ejemplo utiliza dos funciones de la biblioteca estándar de C: qsort() y bsearch(). Estas funciones aplican los algoritmos de ordenación Quicksort y de búsqueda dicotómica, respectivamente, sobre un vector. Los prototipos son: void qsort( void *vec, int n, int tam, int (*comp)(void *a, void *b) ); void *bsearch( void *clave, void *vec, int n, int tam, int (*comp_c)(void *a, void *b); La función qsort tiene como parámetros un puntero genérico que será la dirección de comienzo del Tema 13 Punteros Pág 20 vector a ordenar, el número de elementos del mismo (n), el tamaño en bytes de cada elemento (tam) y un puntero a la función de comparación comp(). La función de comparación recibe dos punteros a los dos elementos que deben compararse. Devolverá un número negativo si el primero es menor que el segundo, un número positivo si el segundo es mayor que el primero y cero si son iguales. Un ejemplo de función de comparación para números reales es éste: int comparar( void *a, void *b ) { float x, y; x= *( (float*)a ); y= *( (float*)b ); if( x<y ) return −1; else if( x>y ) return +1; else return 0; } Los argumentos de la función bsearch() son un puntero a la clave que se quiere buscar, un puntero al vector en el que se realizará la búsqueda, el número de elementos del mismo, el tamaño de cada uno y un puntero a la función de comparación. Esta función recibe dos parámetros: un puntero a la Tema 13 Punteros Pág 21 clave a buscar y otro puntero a uno de los elementos del vector. Al igual que la función anterior, debe devolver un entero negativo, positivo o cero en función de la comparación de la clave a buscar con la del elemento que se le pasa. Tema 13 Punteros Pág 22