En los tres posts anteriores de esta serie hemos visto el concepto de puntero, su relación con los arrays, así como la gestión manual de memoria. En este post de hoy daremos una vuelta más al tema y vamos a ver el concepto de puntero a puntero.
Recuerda que un puntero es una variable que contiene una dirección de memoria. Todas las variables tienen una dirección de memoria (el byte donde empieza a almacenarse su valor, ya que vimos que hay valores que ocupan más de un byte). De estas dos afirmaciones se puede deducir que dado que los punteros son variables su valor (que es una dirección de memoria) estará almacenado en otra dirección de memoria, por lo que podría ser contenida en… otro puntero: un puntero a puntero.
El concepto de puntero a puntero suena innecesario al principio pero veremos que realmente es muy potente y permite solventar fácilmente problemas que de otro modo serían peliagudos. Pero empecemos por el principio y veamos como se declaran:
int one = 1; int* pone = &one; int** ppone = &pone; **ppone = -1; std::cout << "one is " << one << "\n";
Este código declara una variable entera (int) llamada one, un puntero a int (int *) llamado pone que se inicializa a la dirección de la variable one y un puntero a puntero a int (int**) llamado ppone que se inicializa a la dirección de pone. Dado que ppone es un «puntero a puntero a int» debe inicializarse a la dirección de memoria donde haya un «puntero a int».
La penúltima línea dereferencia ppone, por lo que obtiene su valor (pone) que a su vez es un puntero por lo que lo vuelve a dereferenciar para obtener su valor (que es la variable one). De ahí que la última línea imprima que el valor de one es -1. Si el código **ppone = -1 te parece un poco «dificil» de leer, tranquilo, que ya te acostumbrarás, pero es equivalente a *(*ppone) = -1 pero los paréntesis no son necesarios, así que… ¿para qué ponerlos?
Paramétros out
Veamos ahora un escenario que habilitan los punteros a punteros. Imagina un método que nos tenga que obtener una cerveza, pero que por cualquier razón no podemos usar el valor de retorno de dicho método. En C++ muchos métodos usan el valor de retorno para informar si ha habido o no errores (C++ tiene soporte para excepciones pero no se usan tanto como en otros lenguajes). Así, p. ej. quiero un método que le pase un nombre y me devuelva la cerveza con dicho nombre. Además el valor de retorno del método debe usarse para indicar si dicha cerveza existe o no.
Empecemos por definir la clase Beer:
class Beer { char _name[64]; public: Beer(const char* name) { strncpy(_name, name, sizeof(_name)); } const char* getName() { return _name; } };
Esta clase es distinta a la clase Beer que vimos en el post anterior, ya que esta clase hace una copia del nombre dentro de un array de carácteres interno. Fíjate en el constructor en el uso del método strncpy,que copia N carácteres desde un char* hasta otro char* (recuerda que los punteros y los arrays son intercambiables).
Para crear una cerveza debo pasar un parámetro al constructor:
auto sg = Beer("Sang de Gossa"); std::cout << sg.getName();
Veamos ahora un primer intento de crear el método loadBeer que comentábamos antes:
Beer* findBeerByName(const char* n) { return new Beer(n); } int loadBeer(const char* nameToFind, Beer* retBeer) { // Usar nameToFind para encontrar la cerveza retBeer = findBeerByName(nameToFind); return 1; } int _tmain(int argc, _TCHAR* argv[]) { Beer* beer = nullptr; loadBeer("Hoegaarden", beer); std::cout << beer->getName(); return 0; }
El método findBeerByName «simula» el código que habría para encontrar la cerveza dado su nombre (en este caso crea una nueva cerveza con el nombre indicado). Vale, la pregunta es: ¿cuál es el nombre de cerveza que se imprime por pantalla? Un análisis rápido nos puede llevar a pensar que se imprimirá «Hoegaarden», ya que el parámetro retBeer se asigna al puntero Beer* devuelto por findBeerByName. Pero recuerda algo que comentamos en el primer post de esta serie: En C++ todos los parámetros se pasan siempre por valor. Cuando pasamos un puntero a una función, la función recibe una copia del valor del puntero. ¡Ojo! una copia del valor del puntero (es decir, la dirección de memoria), no del contenido del puntero (el objeto apuntado por el puntero). O dicho de otro modo, los punteros habilitan el paso por referencia del dato al que apuntan, pero los propios punteros son pasados por valor. De ahí que la línea retBeer = findBeerByName(nameToFind); no tenga ningún efecto fuera de la función loadBeer, ya que el puntero que asignamos es una copia del puntero pasado.
El puntero «original» vale nullptr (se asigna a dicho valor en la primera línea de _tmain), por lo que al ejecutar el cout el puntero sigue valiendo nullptr (recuerda que loadBeer ha asignado el valor devuelto por findBeerByName a una copia del puntero). Este programa no imprime nada por pantalla si no que da un error de ejecución.
De hecho lo que tenemos en este caso es lo que se conoce como un parámetro out, es decir un parámetro de salida, y lo que realmente queremos es pasar el puntero a Beer por referencia… y sabemos que en C++ usamos punteros para el paso por referencia, ¿no? Pues ahí está un uso de los punteros a puntero. El código arreglado quedaría de la siguiente manera:
int loadBeer(const char* nameToFind, Beer** retBeer) { // Usar nameToFind para encontrar la cerveza *retBeer = findBeerByName(nameToFind); return 1; } int _tmain(int argc, _TCHAR* argv[]) { Beer* beer = nullptr; loadBeer("Hoegaarden", &beer); std::cout << beer->getName(); return 0; }
Ahora el código funciona correctamente… por cierto, que tal y como está implementado findBeerByName tenemos un memory leak, aunque quizá ya te hayas dado cuenta, ¿no? ;-)
Por último solo comentar que no estamos limitados a «punteros a punteros» podemos tener «punteros a punteros a punteros» o «punteros a punteros a punteros a punteros» y de hecho cualquier nivel de indirección que sea necesario (aunque ver más de dos es muy raro).
En el siguiente post introduciremos el concepto de referencias y veremos en que se diferencian las referencias de los punteros en C++ y por qué se introdujeron en el lenguaje. ¡Saludos!