Programación en C/C++: Uso de Punteros
La semana pasada trabajamos con punteros, comprendimos cómo utilizarlos y repasamos algunas funciones. Hoy veremos algunos casos de uso de punteros en programación C++ para ampliar más el tema.
Punteros para pasar argumentos por referencia a funciones
Supongamos que tenemos una función que hace unos cálculos con 2 números que le pasemos, esta es su declaración:
void calculos(int primero, int segundo) { primero += 10; segundo += 20; }
Básicamente esta función suma 10 al primer argumento y 20 al segundo.
Ejecutamos las siguientes líneas de código:
int primero = 0, segundo=0; calculos(primero,segundo); printf("Primero: %i, Segundo: %i",primero,segundo);
Cuya salida es: “Primero: 0, Segundo: 0”.
¿Porque no cambió los valores de nuestras variables como le indicamos? Esto es porque en C los valores que les asignamos a nuestras funciones se pasan por valor, es decir, se le pasa una copia de lo que ingresemos como argumento. Es por esto que no se modifica el valor de nuestras variables, ya que lo que modificamos en la función, se mantendrá modificado sólo en esa función.
Para modificar el valor de nuestras variables, debemos pasar estas variables por referencia, es decir, pasarle a la función una referencia de nuestra variable, en nuestro caso le pasaremos la dirección de memoria de ellas, es decir, un puntero.
Recordemos que para acceder a la dirección de memoria de una variable escribiamos “&variable”, por lo tanto, haremos esto para llamar a la función cálculos:
calculos(&primero,&segundo);
Ahora, lo que le estamos pasando a la función no es de tipo entero (int), si no, de tipo puntero a entero (int *), por lo tanto debemos cambiar su declaración para que quede de esta manera:
void calculos(int * primero, int * segundo) { primero += 10; segundo += 20; }
Ahora analicemos esto:
primero += 10;
Sabemos que primero es un puntero, por lo tanto, escribir lo anterior no tiene mucha utilidad para lo que queremos hacer, ya que estamos sumándole 10 a la dirección de memoria y no al valor que contiene esa dirección, que es lo que queremos. Por lo tanto, la función final debe quedar así:
void calculos(int * primero, int * segundo) { *primero += 10; *segundo += 20; }
Ahora si ejecutamos nuestro código, la salida que produce es: “Primero: 10, Segundo: 20”.
Arreglos con punteros
Acabamos de ver una utilidad de los punteros, ahora veremos otra: Utilizar punteros para crear arreglos y trabajar con ellos. Como sabemos, si queremos declarar un arreglo de 5 enteros, llenarlo con valores y mostrar en pantalla, podemos hacerlo de la siguiente manera:
int numeros[5] = {0,1,2,3,4}; int i; for(i=0;i<5;i++) printf("%i ",numeros[i]);
Este programa produce la siguiente salida: “0 1 2 3 4”.
Ahora, vean el siguiente código:
int numeros[5] = {0,1,2,3,4}; int i; int * puntero = numeros; for(i=0;i<5;i++){ printf("%i ",*puntero); puntero++; }
Si lo ejecutas, verás que produce la misma salida. La explicación de esto radica en que un arreglo son sólo variables que estan dispuestas en la memoria una al lado de otra, como se ve en la siguiente tabla (la cual corresponde a los valores de nuestras variables antes de entrar al bucle for):
Tenemos 5 variables llamadas números (les pongo el mismo nombre para que se entienda la idea). Estas 5 variables dispuestas una al lado de otra corresponden al arreglo números. Si nos damos cuenta, puntero tiene el valor 100, el cual corresponde al primer elemento del arreglo, por lo tanto, números es un puntero, el cual apunta al primer elemento del arreglo que creamos.
Entendido esto, debería tomar sentido que en el primer ciclo del bucle for, el programa nos muestre como salida un 0, debido a que es lo que contiene la dirección 100. Luego de esto, tenemos una línea que dice “puntero++”. Lo que hace esto, es cambiar lo que apunta puntero, por la dirección que se encuentra a su lado, es decir, 110.
Ahora utilizaremos punteros para reservar memoria, que es lo equivalente a crear un arreglo.
La línea de código:
int numeros[5];
Reserva espacio para 5 enteros, puede ser escrita mediante punteros de la siguiente manera:
int * numeros = (int*)malloc(sizeof(int)*5);
[tipexperto titulo = “Nota”]Para utilizar la función malloc es necesario incluir la librería “stdlib.h” en C o “cstdlib” en C++.[/tipexperto]
Analicemos este código
Tenemos un puntero que apunta a un entero, nada que no hayamos visto previamente. Luego de eso, tenemos algo completamente desconocido (o que quizás hayas usado antes sin tener idea de para que servía), la sentencia “(int*)malloc(sizeof(int)*5);”.
Malloc es una función que reserva la cantidad de memoria que le indiquemos, en este caso, estamos reservando 5 veces el peso de un entero, cuyo peso es 4 bytes, te preguntarás ¿porque no escribimos directamente “malloc(20)”?, la respuesta es que un entero no tiene el mismo peso para todos los computadores en los que se utiliza. Tenemos una función que reserva espacio y lo que retorna es un puntero al primer elemento reservador, pero el problema es que es de tipo “void *” (recordemos que void significa que no tiene tipo), por lo cual, le hacemos un cast a “int*” con la línea “(int*)”.
Con nuestro arreglo listo, podemos trabajar de la misma manera que con un arreglo común y corriente, por ejemplo, podemos modificar el primer elemento de la siguiente manera:
numeros[0] = 0; numeros[1] = 1; numeros[2] = 2; numeros[3] = 3; numeros[4] = 4;
Ahora, recordando lo que vimos anteriormente, veamos la siguiente sentencia:
*numeros = 0;
Con esto, estamos diciendo que el primer elemento del arreglo vale 0. Por lo tanto, podríamos hacer esto:
(*numeros) = 0; numeros++; (*numeros) = 1; numeros++; (*numeros) = 2; numeros++; (*numeros) = 3; numeros++; (*numeros) = 4;
Pero el problema ahora radica en que no tenemos un puntero que apunte al primer elemento, ya que el valor de numeros es 110 (recordemos que la dirección del primer elemento es 100). Bueno, ya se, podríamos volver fácilmente escribiendo “numeros–” un par de veces, pero esto podría confundirnos si tenemos un código relativamente largo.
Por lo tanto, la forma anterior no es muy recomendada (a menos que creemos un puntero auxiliar y sea ese puntero el que aumentemos). Una forma equivalente de hacer lo anterior sin perder el inicio del arreglo, es esta:
*numeros = 0; *(numeros+1)= 1; *(numeros+2)= 2; *(numeros+3)= 3; *(numeros+4)= 4;
Para entenderlo, veamos la segunda línea:
*(numeros+1)= 1;
Le estamos diciendo al computador, que lo que apunta numeros más 1 (es decir, una dirección hacia la derecha), valga 1. Hubiera sido incorrecto escribir:
(*numeros+1)= 1;
Ya que estaríamos diciendo que lo que apunta numeros + 1 = 1. Es decir, sería como escribir (suponiendo que la primera posición tiene el valor 5): 5 + 1 = 1. ¿No tiene sentido verdad?
Un punto importante, es que después de reservar memoria con malloc y de utilizarla (y estar seguro que no volveremos a utilizar), debemos liberarla con la función free. Su uso en este caso sería de la siguiente manera:
free(numeros);
Esto se debe hacer debido a que si no lo hacemos, la memoria seguira reservada y estaremos gastando memoria innecesaria.
Como último punto respecto a la utilidad de los punteros para ser utilizados como arreglos, quiero recalcar que podemos usar un puntero como una cadena de caracteres (String), para guardar una cadena simplemente podemos escribir:
char * cadena = "Maestros"; cadena = "del web"; printf("%s",cadena);
Lo cual no da como salida: “del web”. Sin punteros, esto no hubiera sido posible, debido a que si hubiéramos usado arreglos, no se hubiera podido reasignar la cadena de caracteres cadena, debido a que estaríamos intentar cambiar el tamaño del arreglo de 9 a 8 (recuerda que el caracter de fin de cadena “” también cuenta como un caracter más).
Matrices con punteros
Debemos pensar en una matriz como un puntero a un puntero. Anteriormente teníamos un arreglo hecho con punteros, pues bien, ahora tenemos un arreglo de arreglos (puntero a puntero). Un puntero a puntero (de tipo entero), se declara de la siguiente manera:
int ** matriz;
Pero recordemos que debemos reservar el espacio necesario para nuestra matriz, para esto utilizamos la función malloc:
int ** matriz = (int**)malloc(sizeif(int*)*2);
Con esto estamos creando un puntero a puntero de tipo int, es decir, un puntero a un arreglo de tipo int. Pero este arreglo al que estamos apuntando no tiene espacio, no hemos reservador memoria, por lo cual, también debemos inicializarlo:
int ** matriz = (int**)malloc(sizeof(int*)*2); int i=0; for(i=0;i<2;i++) matriz[i] = (int*)malloc(sizeof(int)*5);
Quiero recalcar el hecho de tener que hacer esto, ya que es un error común el no hacerlo. La lógica nos dice que tenemos un arreglo de arreglos. Pero esos arreglos a los que apuntamos, no tienen espacio y ese espacio no se reserva automáticamente, debemos inicializarlo nosotros. Así que tenlo en cuenta y que no se te olvide.
Hecho esto, tenemos una matriz de 2×5 de tipo entero. Para liberar la memoria en este caso, debemos liberar cada arreglo de la matriz, de esta manera:
for(i=0;i<2;i++) free(matriz[i]);
Esto sería todo, espero que les haya quedado claro este tema que es muy complicado de entender la primera vez que se oye de él. Como consejo, practiquen utilizando punteros y así será todo mucho más claro.
Buena dinámica de explicación en un tema un poco difícil de entender para muchos, yo en primer semestre lo pude ver… es interesante su uso en listas enlazadas, es una muy buena practica…
WATON CULIAO ABURRIO
super bn la presentacion…