Tema 4: Punteros Universidad de Santiago – Departamento de Ingeniería Informática 1 PUNTEROS Y ESTRUCTURAS DE DATOS DINÁMICAS Dato: Porción de información manejable por el computador. Tipos de datos: Formados por el conjunto de espacio de representación y conjunto de operaciones asociadas. ü Simples ü Caracter ü Entero ü Real ü Puntero Estructurados o compuestos: Estáticos: Su espacio en memoria es reservado en tiempo de compilación. Dinámicos: Su espacio en memoria es reservado en tiempo de ejecución. Contiguos: Se guardan en memoria de forma consecutiva (vectores, matrices, arrays...). Enlazados: No se guardan en memoria de forma consecutiva (listas, árboles, grafos...). TIPOS ESTÁTICOS Se almacenan de forma estática en la memoria del ordenador, destinando memoria para este tipo de variables en tiempo de compilación. La dirección de memoria asignada se puede conocer con el operador de dirección &. Ventajas Universidad de Santiago – Departamento de Ingeniería Informática 2 - Sencillos de manejar y no dan problemas en tiempo de ejecución. Desventajas - No pueden crecer o menguar durante la ejecución del programa (hay que hacer estimaciones previas a la codificación). - Las zonas de memoria asignadas a estas variables son fijas (no son adecuadas para representar listas, árboles, etc.). LOS PUNTEROS Es una variable de tipo simple, que almacena como valor una dirección de memoria, que suele estar ocupada por otro dato diferente. El puntero, pués, apunta o referencia a otro dato. Se declaran mediante el operador * y el tipo de dato al que apuntará (para que el compilador sepa cómo leer los datos de las dirección apuntadas). También es posible usar punteros sin tipo definido (void *) indicando luego durante la asignación a qué tipo de dato apuntan. Pueden usarse arrays de punteros o punteros a/dentro de estructuras. Los punteros que no apuntan a ningún sitio deben direccionarse a NULL (0x0000 en C) y se representan mediante una raya que los tacha. Acceso al contenido del puntero: Se puede conocer el contenido de la variable a la que referencia un puntero mediante el operador de contenido *, es decir, se puede acceder al dato apuntado por el puntero, tanto para lectura como para escritura: Universidad de Santiago – Departamento de Ingeniería Informática 3 Ejemplo: int *punt; int var; punt = &var; *punt = 10; /* hacemos que punt apunte a var */ /* estamos cambiando el valor de var */ Así pues: punt = Valor guardado dentro del puntero punt (dirección). &punt = dirección de memoria donde punt guarda sus valores. *punt = valor almacenado en la posición de memoria cuya dirección se guarda en punt. USO DE PUNTEROS A). Para referenciar a variables simples: Apuntado a variables de tipo simple: int *punt; int numero punt = &numero; *punt = 123; / *hacemos numero=123*/ B). Paso por referencia de variables simples: Consiste en usar punteros como parámetros: /*-- Llamar con ModificaVariable(&var, valor); --- */ void ModificaVariable( int *a, int valor ) { *a = valor; } C). Para referenciar a variables compuestas: Universidad de Santiago – Departamento de Ingeniería Informática 4 Consiste en la utilización de punteros para referenciar a variables compuestas como vectores y registros. En este caso se puede acceder mediante el puntero a la totalidad de campos de la estructura: float vect[4] = { 1, 2, 3, 4}; struct pieza nueva = {234, "bobina"}; float *pf; struct pieza *psp; pf = vect; printf("%f = %f", *pf, pf[0]); psp = &nueva; printf("%d", psp->codigo ); NOTA: El operador flecha -> sustituye a * y () psp->codigo = (*psp).codigo D). Paso por referencia de variables compuestas: Para simulación de paso por referencia de variables compuestas: void funcion1( float v[], struct fecha *actual) { v[0] = v[1]+5; actual->mes = 5; } void main() { struct fecha hoy; float vector[4]={1,2,3,4}; funcion1(vector, &hoy ); } Universidad de Santiago – Departamento de Ingeniería Informática 5 E). Punteros que referencian a punteros: Los punteros pueden referenciar a otros punteros. Para ello se declara la variable de tipo puntero a puntero (**): int a = 7; int *pi; int **ppi; enteros. // entero, puntero a entero y puntero a puntero de pi = &a; // pi referencia a "a" ppi = &pi ; // ppi referencia a pi printf("El valor de a es %d", **pi ); F). Paso por referencia de punteros: void main( void ) { int a=3, *pi; pi = &a; // pi = dirección de la variable "a". funcion1( pi, &pi ); } void funcion1( int *p_entero, int **p_puntero ) { *p_entero = 5; p_entero = NULL; pero no el de "pi". *p_puntero = NULL; // cambia el valor de "a" // cambia el valor de "p_entero" // cambia el valor de "pi" } USO DE PUNTEROS EN LA CREACIÓN DE VARIABLES DINÁMICAS #include <alloc.h> Los punteros también se utilizan en la reserva dinámica de memoria, utilizándose para la creación de estructuras cuyo tamaño se Universidad de Santiago – Departamento de Ingeniería Informática 6 decide en tiempo de ejecución (son creadas y destruidas cuando el programa las necesite), adecuado para programas donde no se pueda hacer una estimación inicial eficiente de necesidades de memoria. El lenguaje C utiliza para la reserva dinámica de memoria una zona de espacio diferente del segmento de datos y de la pila del programa, llamada montículo (heap), para que ésta no depende de los movimientos de la pila del programa y se puedan reservar bloques de cualquier tamaño (otros lenguajes usan space-lists). a). La función malloc(): void * malloc( size ); Reserva un bloque de memoria de tamaño "size" bytes y devuelve la dirección de comienzo de dicha zona (sin valor inicial dentro de ella). Esta dirección suele asignarse a una variable de tipo puntero previamente declarada. En caso de error o de no existir tanta memoria disponible, malloc devuelve NULL: float *p; p = (float *) malloc( sizeof(float)); *p = 5.0; b). La función calloc(): void * calloc( size_t n, size_t size ); Reserva un bloque de memoria para ubicar n elementos contiguos de "size" bytes cada uno (ej: un vector). int *p; Universidad de Santiago – Departamento de Ingeniería Informática 7 p = (int *) calloc( 5, sizeof(int)); p[3] = 2; c). La función realloc(): void * realloc( void *pant, size_t size ); Cambia el tamaño de un área de memoria reservada con anterioridad, a un tamaño de "size" bytes contiguos. Si size es mayor que el anterior tamaño, realloc() busca una nueva zona, copia allí los datos y destruye la zona anterior. Si size es menor, truncará el bloque actual al nuevo tamaño. int *p, *nuevo p = (int *) calloc( 5, sizeof(int)); nuevo = realloc(p, 10); d). La función free(): void free( void *punt ); Libera la memoria asignada previamente con malloc() o calloc() al puntero punt, evitando que los bloques de memoria permanezcan ocupados hasta la salida del programa al liberar estos bloques para su posterior utilización. Tras el uso de free es recomendable anular el puntero poniéndolo a NULL. Free() no puede usarse para liberar la memoria de las variables globales o locales (están en el segmento de datos o pila, no en el montículo). float *p; p = (float *) malloc( sizeof(float)); Universidad de Santiago – Departamento de Ingeniería Informática 8 // libera la zona apuntada por p free(p); ARITMÉTICA DE PUNTEROS Aparte de las operaciones de asignación (=) y de comparación (==), sobre los punteros están definidas las operaciones aritméticas (-,-,+,++), aunque de un modo diferente a como están definidas para los otros tipos. Estas operaciones sólo pueden usarse cuando los punteros apuntan a matrices o vectores ( de cualquier tipo). Cuando incrementamos o decrementamos un puntero en una cantidad "n", el compilador da como resultado la dirección situada a n*sizeof(tipo_base_del_array) bytes de la dirección original. Es decir, al sumar 1 a un puntero, estamos accediendo al siguiente elemento del vector, como puede verse en el siguiente ejemplo: int *p; int vector[4]={ 5,6,1,4}; p = &vector[0]; p++; // p apunta a vector[1] p=p+2; // p apunta a vector[3] p=p-1; // p apunta a vector[2] PUNTEROS Y ARRAYS Existe una gran relación entre los punteros y los arrays estáticos, ya que en realidad estos últimos son punteros encubiertos que apuntan al elemento inicial del vector. Al declarar un vector, el nombre del mismo es un puntero a vector[0]. Al apuntar a un vector podemos utilizar sobre él las operaciones del apartado anterior: int *p, a; Universidad de Santiago – Departamento de Ingeniería Informática 9 int vector[4]={ 5,6,1,4}; p = vector; p[3] = 10; a *(p+3)=10) // escribimos 10 en vector[3] (equivale p++; // p apunta a vector[1] a = *(p+2) + *(vector+1); p=p+2; // a vale 1+6=7 // p apunta a vector[3] p = vector+2; // p apunta a vector[2] VECTORES DINÁMICOS Son colecciones de datos del mismo tipo almacenados en posiciones contiguas de memoria, pero definidos mediante memoria dinámica, y que son liberados cuando no se necesitan (mediante free()). int *p; p = (int *) calloc( 5, sizeof(int)); Se puede indexar este vector de igual forma que los estáticos: a) Mediante índice: p[0] = primer elemento = *p; (ej: p[5] = 32 ) p[1] = segundo elemento (etc...). b) Mediante aritmética de punteros: *p = primer elemento = *p; *(p+1) = segundo elemento (etc...). (ej: *(p+5) = 32 ) Para recorrer un vector dinámico se hace de igual forma que mediante vectores estáticos: int n = 10; Universidad de Santiago – Departamento de Ingeniería Informática 10 int *p; p = (int *) calloc(n, sizeof(int)); for(i=0; i<n; i++) q[i] = 1; /* o bien *(q+i)=1 */ o bien: #define n = 10; int *p, *q; q = p; for(i=0; i<n; i++) { *q = 1; q++; } Es recomendable mantener siempre una referencia (puntero) al inicio del vector para poder volver al principio ( y liberarlo) en cualquier momento, así como una variable que almacene la cantidad de elementos del vector: MATRICES DINÁMICAS En las matrices dinámicas, a diferencia de las estáticas, no todos los elementos están guardados en posiciones consecutivas de memoria. La matriz dinámica se define como un vector de punteros a vectores de datos (ej: 5x3): // asignación: int *matriz[5]; for( i=0; i<5; i++ ) matriz[i] = (int *) calloc( 3, sizeof(int)); // liberación: for( i=0; i<5; i++ ) free(matriz[i]); Universidad de Santiago – Departamento de Ingeniería Informática También puede crearse definiendo 11 el vector de punteros dinámicamente (mejor forma): // asignación: int **matriz; matriz = (int **) calloc(5,sizeof(int *)); for( i=0; i<5; i++ ) matriz[i] = (int *) calloc( 3, sizeof(int)); // liberación: for( i=0; i<5; i++ ) free(matriz[i]); free(matriz); Métodos de acceso al elemento (i,j): a). Por índices: matriz[i][j]; b). Por aritmética: *(*(matriz+i)+j); c). Mezcla de ambos: *(matriz[i]+j), (*(matriz+i))[j]; ERRORES CON PUNTEROS a). Asignación correcta e incorrecta de valores: int *p, *q; int numero, copia; numero=15; p = &numero; p = numero; // correcto: p apunta a numero // incorrecto, p apunta la dirección 15. // -> Apunta a zonas incontroladas. *p = 5; // correcto, numero ahora vale 5 p = 5; // incorrecto, p apunta a la dirección 5. q = (int *) malloc(sizeof(int)); *q = *p; // correcto, pedimos memoria para otro entero, Universidad de Santiago – Departamento de Ingeniería Informática 12 // y almacenamos numero allí p=q; // correcto, p apunta a q, es decir, // a la nueva memoria. copia = *p; q = p; // Incorrecto: si perdemos la referencia a q, // ya no podremos liberar la memoria asignada. b). Punteros no inicializados o apuntando a zonas de memoria no controladas: char *letra, mi_letra; mi_letra = *letra; //incorrecto: mi_letra tendrá un valor desconocido // (letra no inicializado). *letra = 'A'; // incorrecto: letra no inicializado, estamos // escribiendo en algun lugar de la memoria. (etc...) free(puntero); puntero=NULL, *puntero = 100; // incorrecto, puntero estaba apuntando a NULL. // (Punteros sueltos). También es incorrecto, y peligroso, en los arrays y punteros que apunta a arrays, utilizar como índice valores que se salgan del rango del vector. float vector[10]; vector[-2] = etc... // incorrecto, igual que vector[11], p+11,