Muchos novatos o gente que viene de lenguajes de alto nivel no quieren ni oír hablar de C/C++. se oyen cosas como que es muy complicado porque hay que manejar la memoria a bajo nivel, no tiene recolector de basura, etc. Vamos a intentar explicar claramente el manejo de memoria en C/C++ y los temidos punteros y referencias.
La memoria principal
Como sabemos cuando declaramos una variable lo que estamos haciendo es reservar una zona de memoria, imaginemos que la memoria es una tabla con muchas celdas:
Imaginemos que cada celda de memoria puede ser ocupada por un dato. En realidad según el tipo dato se precisan más o menos casillas, pero para entender el concepto imaginemos que en cada casilla se puede almacenar un dato. Nuestra memoria ficticia está compuesta de mil casillas numeradas desde el 0 al 999. Ésta es la dirección de memoria de cada casilla, su identificador único, como pudieran ser en la vida real una dirección de una calle, un número y un piso.
Si nosotros hacemos algo como esto en nuestro código:
int x = 4;
Lo que estamos haciendo es reservar una zona de memoria para una variable x y dándole el valor 4. Imaginemos que en nuestra memoria ficticia se almacena en la posición 998, se vería así.
Bien, en C++ hay un operador para obtener la dirección de memoria de una variable y es &
si hiciéramos algo como imprimir el valor de la dirección de memoria:
int x = 4; cout << "Valor de x: " << x << endl; cout << "Direccion de x: " << &x << endl;
Nos saldría por pantalla:
Valor de x: 4 Direccion de x: 998
Realmente no te saldrá un número entero sino que será un número hexadecimal bastante más largo, pero hay que tener bastante claro que ni el número hexadecimal que te de el compilador ni el 998 son números enteros es una dirección de memoria un tipo de dato como cualquier otro.
Punteros
Como es un tipo de dato las direcciones de memoria se pueden almacenar en variables. Variables del tipo que almacenan direcciones de memoria que son los llamados punteros. Por tanto un puntero es tan solo un tipo de datos como int almacena enteros, char caracteres o float almacena números de como flotante pues los punteros almacenan direcciones de memoria.
La clave está en que no hay una palabra clave, valga la redundancia, para definir a los tipos punteros sino que según sea el tipo de dirección que va a almacenar ese sera el tipo añadiendo el operador *
.
Por ejemplo si yo quiero crear un puntero que almacene una dirección de memoria de un entero debo hacer lo siguiente:
int *px;
con esto estoy declarando una variable que puede almacenar la dirección de una variable de tipo int. Podriamos entonces asignarle una dirección de memoria mediante el operador que vimos antes.
int x = 4; int *px = &x;
Imaginemos que la variable px se crea en la posición 2 de nuestra memoria ficticia. Nos quedaría el siguiente esquema:
Como vemos ocupa una posición como una variable normal, pero en lugar de contener un entero como 4 contiene una dirección de memoria (998). Como posición de memoria que ocupa también tiene su dirección de memoria que se puede obtener con el operador &
es lo que se llama un puntero de puntero.
Estos se declaran igual que los demás anteponiendo el operador *
:
int x = 4; int *px = &x; int **ppx = &px // es un puntero de puntero por lo que solo puede guardar direcciones de punteros de tipo int
Siguiendo con el ejemplo de nuestra memoria ficticia supongamos que la nueva variable ppx se crea en la posición 997. Este sería el equema resultante.
El operador de indirección
Bien, ya sabemos almacenar en memoria direcciones de memoria de otras variables ya sean variables enteras o de cualquier tipo como punteros de las mismas (punteros de punteros). Ahora necesitamos algo para poder usar esas direcciones de memoria, poder usarlas para acceder al contenido original, esto se hace a través del operador de indirección *
y no, no me he equivocado es el mismo que se usa para declarar los punteros es lo que tiene C y por tanto C++ que reutiliza mucho los operadores y basa su significado en el contexto. Veamos un ejemplo de uso.
int x = 4; int *px = &x; cout << "Valor de x: " << x << endl; cout << "Valor de x: " *px << endl;
Esto mostrará el siguiente resultado:
Valor de x: 4 Valor de x: 4
Como vemos anteponer *
a una variable puntero nos devuelve el contenido de la dirección que contiene. Lo mismo sucede con los punteros de punteros:
int x = 4; int *px = &x; int **ppx = &px; cout << "Valor de x: " << x << endl; cout << "Valor de x: " << *px << endl; cout << "Valor de x: " << **ppx << endl;
La última sentencia el asterisco más pegado a la variable devuelve la que contiene la dirección de ppx que es px y el siguiente asterisco devuelve lo que contiene la dirección de px que es x y por tanto 4.
Valor de x: 4 Valor de x: 4 Valor de x: 4
Paso por valor o por referencia
Una de las cosas más útiles de los punteros es el paso por referencia en lugar del paso por valor. Vamos a explicar como funciona esto en C/C++.
Ámbito de variables
Lo primero es entender bien lo que es el ámbito de las variables, una variable solo existe en la función en la que se crea y nada más, a través de los parámetros podemos pasar el valor de una determinada variable a otra función, pero lo que pasamos es el valor y no la variable en sí. Veamos un ejemplo.
#includevoid sumar(int); int main() { int n = 4; sumar(n); std::cout << n << std::endl; system("pause"); return 0; } void sumar(int x) { x++; std::cout << x << std::endl; }
Tenemos la función main que contiene la variable n, esta variable solo existe en este contexto al llamar a la función sumar que tiene como parámetro la variable x
lo que hacemos es darle a ese valor del parámetro x
el valor de n
, pero es la única relación que hay entre n
y x
por tanto la salida de nuestro programa es la siguiente:
5 4
La variable x
que valía 4 por el valor que le dio n
es aumentada en una unidad y se muestra en pantalla, se sale de la función sumar y se vuelve a la función main que muestra el valor de n
que sigue siendo 4.
Paso por referencia
Hay veces que nos interesa que una función modifique una variable que no pertenece a ella, es decir, fuera de su ámbito. En C/C++ es imposible pasar una variable por referencia como en otros lenguajes y hay que hacerlo a través de punteros (o referencias como veremos más adelantes). La idea es que como solo se puede pasar el valor de una variable a una función lo que hacemos es pasar la dirección de una variable a través de un parámetro de puntero y luego con el operador de indirección podemos acceder al contenido de la variable original.
#includevoid sumar(int *); int main() { int n = 4; sumar(&n); std::cout << n << std::endl; system("pause"); return 0; } void sumar(int *x) { *x = *x + 1; std::cout << *x << std::endl; }
Como vemos el parámetro de sumar ahora es un puntero que recibe una dirección de memoria en este caso le pasamos la dirección de la variable x
y dentro de la función con operador de indirección podemos acceder al contenido de la variable n
.
Referencias
Además de los punteros heredados de C el lenguaje C++ añadió una nueva característica que son las referencias, una referencia es por así decirlo un alias o etiqueta de una variable.
Las referencias se deben inicializar al declararse ya que no son en sí una variable sino una etiqueta de otra y se declaran poniendo el operador &
después del tipo de dato.
int n = 4; int &ref_n = n; std::cout << ref_n << std::endl;
A efectos prácticos n
y ref_n
se refieren a la misma variable de hecho si con el operador &
obtenemos la dirección de memoria de n
y ref_n
obtendríamos la misma en ambos casos. En nuestra memoria ficticia se vería así:
Paso por referencia... con referencias
Las referencias son una buena forma de pasar un valor a otra función sin ser por valor, veamos el ejemplo anterior de paso por punteros, pero está vez usando referencias.
#includevoid sumar(int &); int main() { int n = 4; sumar(n); std::cout << n << std::endl; system("pause"); return 0; } void sumar(int &x) { x = x + 1; std::cout << x << std::endl; }
Espero que este artículo ayude a todos los que tengas dudas aún sobre como usar los punteros y las referencias de C/C++.