Apuntadores en C++ ¿Amigos o enemigos?

Edson Armando Vázquez Cruz
7 min readNov 23, 2020

--

fuente de imagen: Gray Laptop Computer Showing Html Codes in Shallow Focus Photography · Free Stock Photo (pexels.com)

Si hay algo que se aprende cuando empezamos a estudiar un nuevo lenguaje de programación es que a veces lo más complejo de comprender es clave fundamental para que todo lo demás sé nos haga sencillo. Los apuntadores en C++ es uno de los ejemplos más evidentes para explicar este fenómeno ya que para muchos esta característica del lenguaje suele ser temida o ignorada debido al estigma que existe al llamarla una de las cosas más difíciles de entender para dominar C++.

En este artículo, intentaremos presentar de la manera más sencilla y entendible lo importante que es considerar el uso los apuntadores explicando su estructura y posibles implementaciones. También el cómo se relacionan con el concepto de estructura de datos ya que, al no usarse, se pierde la potencia y flexibilidad que C++ ofrece.

¿Qué son los apuntadores?

fuente de imagen: Pointers in C and C++ | Set 1 (Introduction, Arithmetic and Array) — GeeksforGeeks

Las estructuras de datos son con frecuencia implementadas en productos y aplicaciones cuyo propósito es optimizar el uso de memoria, y si en algo son útiles los apuntadores es precisamente eso.

Como sabemos, al programar podemos declarar variables con un simple:

int num = 24;

Recordemos que al hacer esto, estamos reservando un bloque de memoria ahora llamado “num” que guardará un valor “int” de valor “24”. Este bloque ahora será asignado aleatoriamente una dirección por parte del compilador y aunque el valor de esta dirección sea irrelevante para nosotros, este puede ser accedido por medio del operador unario ‘&’ ampersand. Por otro lado, para acceder al valor al cual el apuntador está apuntando se usa el operador de indirección ‘*’ asterisco:

std::cout << “La dirección de num= “ <<&num;

/* Esto imprimiría el valor de la dirección de num. Digamos que es 2468*/

std::cout << “La dirección de num= “ <<*(&num);

/* Esto imprimiría el valor de la variable num, siendo 24*/

Los apuntadores nos permiten el acceso a los bloques de memoria dándonos un canal directo para modificar el valor de las variables declaradas. Así como una variable cualquiera, debe declararse antes de ser utilizado.

Notación y puntos que recordar.

Se declara un apuntador para una variable de la siguiente manera:

<TipoDeVariable> *<identificador> = &<nombreDeVaribleParaApuntar>;

Ejemplo: int *apuntador= &num;

/* Este es un apuntador hacia la dirección de nuestra variable de ejemplo*/

Como mencionamos anteriormente, el operador de indirección ‘*’ sirve para acceder a valores a los cuales apunta una dirección, y ahora utilizando la notación que acabamos de presentar de apuntadores podemos hacer lo siguiente utilizando nuestra variable y apuntadores ejemplo:

std::cout << “La dirección de num= “ <<apuntador;

/* devuelve “La dirección de num = 2468” */

std::cout << “El valor de num= “ <<*apuntador;

/* devuelve “El valor de num = 24” */

Se debe considerar que los apuntadores se enlazas a tipos de datos específicos, de manera que al correr código que los contenga, el compilador verifica si se asigna la dirección de un tipo de dato al tipo correcto de apuntador.

int ∗apuntador;

float num2;

pointer = &num2;

/*Esta última línea es incorrecta ya que ‘num2’ es un float y ‘apuntador es un apuntador a un entero*/

Esto NO significa que ‘pointer’ esté guardando un valor de tipo int. Un apuntador de ints como lo es ‘pointer’ solo es capaz de guardar direcciones de variables del tipo int.

Void Pointer

Una manera de “solucionar” la restricción que mencionamos en la sección anterior acerca de cómo ciertos tipos de apuntadores solo pueden apuntar a cierto tipo de variables es utilizar los void pointers. Estos pueden ser usados para apuntar hacia cualquier tipo de variable y también son conocidos como apuntadores genéricos:

void *<NombreDelApuntador> = NULL;

Sin embargo, son un arma de doble filo ya que, con su flexibilidad de uso, también vienen restricciones. Este tipo de apuntadores no son compatibles con el operador de indirección ‘*’ de la misma manera que un apuntador normal. Se deben hacer adiciones al código para que no nos regresen errores de compilación:

void *apuntador = NULL;

int num = 54;

char letra = ‘z’;

apuntador = &num;

std::cout << “El valor de num= “ <<*apuntador;

/* Regresaría error de compilación*/

/* Método correcto */

std::cout << “El valor de num= “ <<*(int*)apuntador;

/* Imprime “El valor de num= 54”*/

apuntador = &letra;

std::cout << “El valor de letra= “ <<*apuntador;

/*Regresaría error de compilación */

/* Método correcto */

std::cout << “El valor de letra= “ <<*(char*)apuntador;

Llamadas por referencia

Cuando nosotros declaramos una función, las cual recibe como parámetros alguna(s) variable(s), por lo regular nos vamos por la ruta segura e introducimos dentro del paréntesis simplemente el mismo nombre de la variable que necesitamos procesar. Veamos estos 2 ejemplos de código y comentemos sus diferencias:

Ejemplo 1:

#include <iostream>

int suma(int x, int y){

int z;

z = x + y;

return z;

}

int main() {

int x = 3, y = 5;

int producto = suma(x,y);

std::cout << “Suma= “ << producto;

}

Ejemplo 2:

#include <iostream>

int suma(int *x, int *y){

int z;

z = (*x) + (*y);

return z;

}

int main() {

int x = 3, y = 5;

int producto = suma(&x,&y);

std::cout << “Suma= “ << producto;

}

En el ejemplo 1, tenemos un código en donde la función declarada es llamada usando las variables ‘x’ y ‘y’, dicha función tiene 2 parámetros del mismo nombre, sin embargo, al hacer la llamada de esta manera lo que el compilador hace es copiar los valores de las anteriores variables xy sobre otro set de variable xy dentro de la función, duplicando el numero de bloques de memoria utilizados para esta función. Por otro lado, en el ejemplo 2, al usar una sintaxis que aprovecha los apuntadores, podemos pasarle a la función las direcciones de las variables a la función lo cual NO crea una copia de dicha variable, sino que simplemente la referencia.

Arrays, strings y su relación con los apuntadores

Para sorpresa de muchos, hay una relación clave entre los apuntadores, los strings y los arrays. Declaremos un array ejemplo int Arr [3];. Si nosotros llegásemos a escribir el siguiente código:

std::cout << “Resultado al imprimir Arr= “ <<Arr;

Lo que nos regresaría es la dirección del array declarado, lo cual quiere decir que, el nombre de un array por si mismo es un apuntador hacia su primer valor, siendo en este ejemplo Arr[0]. Esto nos facilita el acceder a los datos dentro un array cuando estamos haciendo iteraciones, al poder reemplazar el uso de una variable extra que represente una iteración por el mismo nombre del array para poder regresar, actualizar o desplegar la información almacenada en él. Esta misma ley aplica para los strings.

Presencia de apuntadores en estructura de datos

A continuación, presentaremos un par estructuras de datos en donde son implementados los apuntadores.

Vectores

Observando el siguiente código ejemplo de la creación de una clase ‘Vector’ podemos comentar lo siguiente con respecto a su implementación de los apuntadores.

template <class T>

class Vector {

private:

unsigned int size;

T *data;

public:

Vector(unsigned int, T&);

};

En esta estructura de datos, se esta utilizando un apuntador de tipo T (siendo el template para el tipo de valores que guardara el vector) el cual apuntará a los nuevos vectores que el programador quisiera crear utilizando el constructor establecido:

template <class T>

Vector<T>::Vector(unsigned int numberOfElements, T &initialValue){

size = numberOfElements;

data = new T[size];

for(unsigned int i =0; i < size; i++){

data[i] = initialValue;

}

Nodos, Listas Ligadas y Listas doblemente ligadas.

fuente de la imagen: Estructuras de datos — Listas ligadas — Oscar Blancarte — Software Architecture (oscarblancarteblog.com)

Los apuntadores son clave para una buena construcción de una clase de una lista ligada y sus nodos:

template <class T>

class Nodo {

private:

Nodo(T);

Nodo(T, Nodo<T>*);

Nodo(const Nodo<T>&);

T value;

Nodo<T> *next;

/*Al ser un nodo dentro de una lista ligada, se necesitaría un apuntador al siguiente nodo*/

friend class Lista<T>;

};

template <class T>

class Lista {

public:

Lista();

Lista(const Lista<T>&);

private:

Nodo<T> *head;

/*Dentro del modelo de estructura de datos de una lista ligada, es necesario tener también un apuntador al inicio de dicha lista, por lo que head nos ayudará a definir el inicio de esta*/

int size;

}

Los apuntadores incluso nos ayudan a crear nodos temporales que nos permitan recorrer la lista ligada y así poder hacer inserciones en la misma. Cabe recalcar que la implementación de apuntadores en las listas doblemente ligas es muy similar, solo que en este caso la carga de nodos y apuntadores es mas ya que necesitamos un apuntador al último valor de la fila y cada nodo ahora tendría un apuntador extra apuntando al nodo anterior.

Incluso, este modelo mencionado anteriormente también se puede adaptar a estructuras de datos como lo son los arboles binarios ya que el comprender la estructura de una lista ligada es el fundamento para empezar a estudiar arboles binarios.

Conclusión

No cabe duda qué los apuntadores son fundamentalmente lo que hace de C++ un lenguaje tan potente y flexible apoyándonos de múltiples maneras con el manejo de la memoria de nuestro código y facilitación al crear estructuras de datos. Si bien el concepto puede ser un tanto complejo de implementar con facilidad, no se puede negar el hecho del potencial que tienen para ayudarnos a mejorar como programadores. Así que nuestra recomendación es darles un intento e investigar mas a fondo las utilidades que puede llegar a tener.

Bibliografía

Programación en C, C++, Java y UML (2a. ed.) Capítulo 11: Apuntadores

Dra. Maria de Lourdes López García, ‘Apuntadores’, Apuntadores (uaemex.mx)

Gilberto Díaz, ‘Programación Digital 2’, webdelprofesor.ula.ve/ingenieria/gilberto/pr2/01_Apuntadores.pdf

GeeksforGeeks, ‘Pointers in C and C++ | Set 1 (Introduction, Arithmetic and Array)’, Pointers in C and C++ | Set 1 (Introduction, Arithmetic and Array) — GeeksforGeeks.

--

--