GUIA PRACTICA DE PUNTEROS: ========================== Suponed que tenemos una variable local declarada en una funcion (tambien puede ser el programa principal) y queremos llamar a otra funcion para que esta variable (la memoria donde se almacena) se modifique desde dentro de esta funcion a la que llamamos. La unica forma de hacerlo con el lenguaje C es pasando la direccion de memoria donde se almacena la variable local a la funcion que se llama. Esta funcion (la llamada) usa esta direccion para acceder y modificar la memoria de la variable local de la funcion que le ha llamado. No podemos pasar la direccion de memoria de la variable como parametro de cualquier funcion. En la declaracion de la funcion llamada este parametro debe permitir pasar direcciones de memoria. De hecho, debe permitir pasar direcciones de memoria del mismo tipo que el de la variable local que nosotros queremos pasar. Es decir, este parametro debe ser de tipo PUNTERO al tipo de la variable local que se le pasa en la llamada. VEAMOS UN EJEMPLO!!!! Tenemos una funcion que tiene un parametro que es un puntero a una posicion de memoria ocupada por un entero. La funcion asigna un cero a esa memoria. void inicializar_a_cero ( int * puntero) { *puntero = 0; /* Asigno un cero a la posicion de memoria donde apunta puntero. */ } DECLARACION: La declaracion, como habeis visto, es un puntero ( * ) a entero (int) . USO: Dentro de la funcion, para acceder a la memoria a la cual apunta la direccion de memoria guardada en puntero tenemos que poner un * delante del nombre del puntero. De esta forma estamos accediendo a la memoria donde apunta el puntero y desde entonces es como si trabajaramos con un entero. En la funcion le asignamos un 0 a esa memoria. Ahora... que posicion de memoria estamos modificando? eso no lo sabemos todavia. La direccion de memoria que almacena el puntero sera la direccion que nosotros le pasemos en la llamada a la funcion. En cada llamada, que puede ser desde cualquier sitio del programa, nosotros le podemos pasar direcciones de memoria de diferentes enteros. Suponed este programa principal. void main () { int numero; /* Variable local numero de tipo entero */ Persona per; /* Variable local per de tipo Persona */ numero = 7; per.edad = 10; printf("%d\n",numero); inicializar_a_cero(&numero); printf("%d\n",numero); inicializar_a_cero( &per.edad ); printf("%d\n",per.edad); } En este programa principal se quiere modificar el valor de la variable numero e inicializarlo a cero ( llamada a la funcion inicializar_a_cero(&numero) ). Al llamar a la funcion debemos pasarle la direccion de memoria de la variable. Por esto mismo se le llama con &numero ( & == direccion de, siempre y cuando este delante de una variable). Dentro de la funcion inicializar_a_cero, puntero, despues de esta llamada, tiene la direccion memoria de la variable entera numero. Entonces, cuando hagamos *puntero estaremos accediendo a la memoria donde se guarda numero. Asi pues, antes de la llamada a la funcion, el printf escribira por pantalla un 7, despues de la llamada el printf escribira un 0 por pantalla. El mismo programa programa principal llama a la funcion inicializar_a_cero pero pasandole la direccion de otra variable entera, la edad de la persona per que tenemos declarada en el programa principal. Como lo hacemos? pues nosotros queremos pasarle la direccion de memoria del campo edad de la variable persona per. Para hacerlo : 1. - accedemos al campo edad de la persona. 2. - queremos pasarle la direccion del campo persona, pues le ponemos el & delante de per.edad, es decir, &per.edad. Observar que he puesto &per.edad y NO per.&edad, NI &edad. La edad de la persona per se indentifica por todo el conjunto per.edad Una vez hecho esto, la edad de la persona vale cero y si la escribimos por pantalla se mostrara un cero. NOTA IMPORTANTE!!! - podriamos hacer la siguiente llamada a la funcion? inicializar_a_cero( &5); NO!!!!!!!! . 5 es un valor constante entero que no tiene zona de memoria para almacenarse por lo que no existe direccion de memoria donde se almacena. Esto os daria un error de compilacion. Pero si nos lo diera podrian pasar cosas de expediente X ;-)) - Y si hubieramos tenido este programa principal... void main () { int *p; inicializar_a_cero(p); } Crees que esto funcionaria? Compilar compilaria porque a la funcion le tenemos que pasar un puntero a entero y p es un puntero a entero. No habria errores de compilacion. Pero donde apunta p????????? no apunta a ninguna zona de memoria donde se almacene un entero. Cuando dentro de la funcion se accediera a *puntero podria pasar que accedieramos a una posicion no valida de memoria y el programa PETARIA! Como solucionarlo? void main () { int numero; int *p; p = № inicializar_a_cero(p); } Ahora p apunta a una zona valida de memoria, la memoria para la variable entera numero. Aunque para hacer esto hariamos lo que habiamos hecho antes no? inicializar_a_cero(&numero); COSAS A RECORDAR: - El parametro de la funcion debe ser de tipo puntero (*) al tipo que queremos modificar, en este caso entero (int). Si no fuera asi, no podriamos pasarle la direccion de memoria de una variable de tipo entero. void inicializar_a_cero( int *puntero); - Cuando queremos modificar la memoria apuntada por un puntero tenemos que poner el * delante de la variable puntero. De esta forma accedemos al contenido de memoria donde se apunta. *puntero = valor; - Al llamar a la funcion, como tenemos una variable entera numero y queremos pasar la direccion, la llamamos pasandole como parametro la & de la variable. inicializar_a_cero(&numero); CASOS "ESPECIALES" : Vectores y Struct's. Vectores: --------- En el lenguaje C no se puede devolver un vector de ningun tipo con el return, ni tampoco podemos pasar todos los elementos de un vector en la llamada a una funcion. La unica forma de poder modificar los elementos de un vector declarado local en una funcion desde dentro de otra funcion es pasandole la direccion de memoria del primer elemento del vector en la llamada. Como estamos pasando la direccion de memoria de un elemento del vector, el parametro de la funcion llamada debe ser de tipo puntero al tipo de los elementos del vector. Es mas, aunque nosotros no quisieramos modificar los elementos del vector, la unica forma de pasar un vector como parametro es pasando la direccion de memoria del primer elemento del vector, es decir, de la misma forma que cuando queremos modificar. Sera responsabilidad del que hace la funcion de no modificar los elementos del vector. VEAMOS OTRO EJEMPLO!! Supongamos que tenemos la funcion que inicializa todos los elementos de un vector enteros a un valor entero que tambien pasamos como parametro de la funcion. Ademas de estos dos parametros tambien pasaremos un parametro para indicar el numero de elementos del vector. void inicializa_vector_a_valor( int *vector, int n_elems, int valor ) { int i; for (i=0; i = 0 ) { vector[n_elems] = valor; n_elems --; } /* Antes de acabar la funcion, n_elems vale -1, pero este valor SOLO SE VE AQUI. */ } Esta forma de implementarlo tambien es correcta, pero ahora modificamos el valor de n_elems. PERO EL VALOR QUE MODIFICAMOS SOLO AFECTA A DENTRO DE ESTA FUNCION, ya que el parametro n_elems tiene una zona de memoria propia para guardar el entero y no apunta a ningun zona de memoria de alguna variable en otra funcion. n_elems NO ES PUNTERO. Pero a que vector de enteros estamos haciendo referencia? y que valores tienen n_elems y valor? . Eso dependera, cada vez, de los valores que le pasemos a la funcion en la llamada. Suponed esta declaracion de tipo y este programa principal: #define MAX_ELEMS 100 typedef struct { int valores[MAX_ELEMS]; int n_elems; } TValores; void main() { int ENTEROS[100]; TValores v; v.n_elems = 20; inicializa_vector_a_valor( ENTEROS, 100, 5 ); inicializa_vector_a_valor( v.valores, v.n_elems, 7); } Bueno, aqui tenemos dos llamadas a la funcion inicializa_vector_a_valor. 1.- En la primera hacemos la llamada para inicializar todos los elementos del vector de enteros ENTEROS (int ENTEROS[100]) a 5 . Como lo hacemos? PRIMER PARAMETRO: el primer parametro hemos dicho que deberiamos pasarle la direccion de memoria del primer elemento de un vector de enteros. RECORDAD que el nombre de una variable que es un vector YA ES LA DIRECCION DE MEMORIA DEL PRIMER ELEMENTO DE UN VECTOR. Por consiguiente, solo hace falta poner el nombre de la variable. Por ejemplo: inicializa_vector_a_valor( ENTEROS, 100, 5 ); Como veis no le he puesto el & delante y es que el nombre ENTEROS es ya un puntero al primer elemento (ENTEROS == &ENTEROS[0]). Por lo tanto, NADA DE PONER EL & DELANTE si queremos pasar vector como parametro de una funcion. SEGUNDO Y TERCER PARAMETRO: Lo que tenemos que pasarle son dos valores enteros. En este caso, le pasamos un 100 para indicarle que queremos asignar un 5 a los 100 elementos enteros del vector ENTEROS. Entonces, con esta llamada lo que sucede es que todos los elementos del vector ENTEROS se inicializan a 5 dentro de la funcion inicializa_vector_a_valor. 2.- Segunda llamada: inicializa_vector_a_valor( v.valores, v.n_elems, 7); Ahora le queremos pasar el vector de enteros de la variable v declarada en el programa principal (el campo valores). v es una estructura al estilo TVector visto en clase (aquĦ la hemos llamado TValores), por lo que tiene un vector de elementos (en este caso de enteros, campo int valores[MAX_ELEMS]) y el numero de elementos utiles que tiene (campo int n_elems). PRIMER PARAMETRO: Si queremos pasarle el vector de enteros que tiene v como lo hacemos? 1.- Accedemos al campo valores de v. Es decir, v.valores. 2.- Ponemos un & delante de v.valores, es decir, &v.valores para pasarle la direccion de memoria del primer elemento. NO!!!!!!! ESO NO ESTA BIEN! recordad que el nombre de un vector es la direccion de memoria al primer elemento, por lo que NO HAY QUE PONER &. Solo hay que poner v.valores SEGUNDO Y TERCER PARAMETRO: Pues queremos indicarle a la funcion que tenemos n_elems elementos utiles. Pues tenemos que pasarle el campo n_elems de la estructura v. Como se hace? v.n_elems No hay que poner & porque queremos pasarle el valor y no la direccion de memoria donde se almacena. Finalmente, le indicamos que el valor con el que queremos inicializar todo el vector es 7. NOTA IMPORTANTE: En ambas llamadas a funcion yo he pasado como tercer parametro un valor constante ( 5 y 7) pero yo podria haber puesto en este parametro cualquier expresion entera. Expresion entera: variables, constantes, funcion que devolviera un entero, variables y/o constantes y/o funciones operando con operadores de enteros, Como primer y tercer parametro tambien podriamos haberles pasado muchas cosas diferentes: inicializa_vector_a_valor( ENTEROS, v.n_elems, 5 ); inicializa_vector_a_valor( v.valores, 100, v.n_elems-5*funcionquedevuelve2()); Y todas las posibles combinaciones que se os ocurran. Structs: -------- La forma de indicar que pasamos un puntero a una estructura a una funcion es de la misma forma que hacemos con el entero. Ejemplo practico: Necesitaremos de esta definicion: typedef struct { int vector[100]; int n_elems; } TVector; /* La funcion asigna un cero al campo n_elems de la estructura apuntada por el * puntero v. */ void ini_TVector (TVector *v) { v->n_elems = 0; } DECLARACION: En el ejemplo estamos declarando una funcion que tiene un parametro de tipo PUNTERO a TVector. Es decir, v guarda la direccion de memoria donde se almacena una variable de tipo TVector. USO: la forma de acceder a la memoria de un estructura aputnada por un vector es muy sencillo. Antes poniamos un punto, ahora tenemos que poner una flecha. v.n_elems AHORA NO!! v->n_elems Y YATA! En el caso de la funcion asignamos un cero al campo n_elems de la estructura apuntada por v. v->n_elems = 0; OTRA FORMA DE ACCEDER AL CAMPO n_elems DE LA ESTRUCTURA: (*v).n_elems = 0; Paso a paso: *v para acceder al contenido de memoria donde apunta v, es decir la estructura de tipo TVector. Ahora solo tenemos que acceder al campo que nosotros queramos, por ejemplo, n_elems. *v.n_elems pero!!!!! si no ponemos los parenteis como hemos hecho antes (*v).n_elems, *v.n_elems seria lo mismo que *(v.n_elems)= 0 lo cual es incorrecto. Esto es debido a que . tiene mayor prioridad que el * . Asi pues,poner el (*v).n_elems. Ahora, mejor la flecha no? Pero a que hace referencia el puntero v?. La direccion de memoria que almacena el puntero sera la direccion que nosotros le pasemos en la llamada a la funcion. En cada llamada, que puede ser desde cualquier sitio del programa, nosotros le podemos pasar direciones de memoria diferentes de diferentes estructuras TVector. Suponed este programa principal: void main() { TVector v1; TVector vectores[100]; ini_TVector(&v1); ini_TVector(&vectores[5]); } En este programa principal se quieren inicializar los TVector v1 y el elemento 5 del vector (TVector vectores[100]) , que es otro TVector. 1.- En la primera llamada: ini_TVector(&v1); Es sencillo, es como trabajabamos con int * en los primero ejemplos. Que es lo que queremos pasarle a la funcion? la direccion de memoria de la variable v1. Por lo tanto, le tendremos que llamar con &v1. Donde & indica la direccion de y v1, indica la variable. 2.- Ahora bien, que pasa con el segundo caso, cuando queremos inicializar el elemento 5 del vector llamado vectores? Como lo hacemos? 1.- Queremos acceder al elemento 5 del vector vectores. Es decir, vectores[5] En este momento es como si trabajaramos con un TVector que ahora, en vez llamarlo v1, es vectores[5], todo junto. 2.- Queremos pasarle la direccion de este elemento. Como lo hacemos? pues igual que antes tenemos que ponerle & delante. Es decir, &vectores[5]. Con esto estariamos pasando la direccion de memoria (&) del elemento 5 del vector vectores. En cada caso, dentro de la funcion ini_TVector, nosotros trabajaremos con el puntero v que apuntara a: - Si es la primera llamada, apuntara al contenido de memoria donde se almacena v1 - Si es la segunda llamada, apuntara al contenido de memoria donde se alamcena vectores[5] SE OS OCURRE OTRA FORMA DE HACER LA SEGUNDA LLAMADA??? - Que os parece esta? ini_TVector(vectores+5); Estaria bien? SI! . vectores es un puntero a una variable de tipo TVector no? , de hecho al primer elemento del vector. Ahora recordad la aritmetica de punteros. Cuando yo sumo un numero a un puntero, no es una suma normal, es la suma de un numero * el tamanyo del elemento al cual apuntan. Encontes vectores+5 en realidad es vectores+5*tamano_de_TVector. Esto significa que: vectores+5 es equivalente a &vectores[5] ATENCION: vectores+5 NO ES EQUIVALENTE a vectores[5]. Por que? porque vectores+5 sigue siendo una direccion,la direccion de memoria de vectores[5] ya que estamos operando con vectores. Por consiguiente, vectores+5 ya seria correcto. OTRA COSA YA PARA ACABAR: Como inicializariais todos los elementos TVector del vector vectores? Pensarlo. Pienso, luego existo... Agradecimientos: Dos companyeros (David y Juan) vuestros sufrieron leyendo el primer documento y me dieron algunos consejos como quitar los acentos puestos con mi teclado. No es lo mismo par metro que parametro eh? . Asi que los quite todos! Y otros comentarios muy buenos que creo que han ayudado al documento.