PUNTEROS, REFERENCIAS Y GESTIÓN DE LA MEMORIA

Un puntero es una variable especial que almacena la dirección de memoria de otro objeto o variable. En otras palabras, un puntero «apunta» a la ubicación en memoria de otro objeto.

Los punteros son útiles cuando se trabaja con estructuras de datos dinámicas, como arreglos, listas enlazadas y objetos dinámicamente asignados en el montón (heap). También son comúnmente utilizados para trabajar con funciones que modifican directamente los datos en memoria.

Temario:

Creación de punteros

Para declarar un puntero, se utiliza el operador de asterisco (*) antes del nombre de la variable. Por ejemplo:

En este caso, punteroEntero es un puntero a un entero. Puedes asignarle la dirección de memoria de una variable utilizando el operador de dirección (&). Por ejemplo:

En este caso, &numero devuelve la dirección de memoria de la variable numero, y esa dirección es asignada al puntero punteroEntero.

Puedes acceder al valor al que apunta un puntero utilizando el operador de desreferenciación (*). Por ejemplo:

En este caso, *punteroEntero devuelve el valor almacenado en la dirección de memoria apuntada por punteroEntero.

Los punteros son una característica poderosa pero también pueden ser propensos a errores si no se manejan correctamente. Es recomendable usarlos con precaución y comprender bien su funcionamiento antes de utilizarlos en un programa.

Referencias

Una referencia es un alias o nombre alternativo para una variable existente. Proporciona una forma de acceder a una variable utilizando un nombre diferente, pero sin crear una copia de la variable original.

Las referencias se definen utilizando el operador de referencia (&) al declarar la variable. Por ejemplo:

En este caso, refNumero es una referencia a la variable numero. Ahora podemos utilizar refNumero para acceder y manipular el valor de numero de la misma forma que si estuviéramos utilizando directamente numero. Cualquier cambio realizado en refNumero se reflejará en numero y viceversa.

Las referencias son particularmente útiles en los siguientes escenarios:

Paso de parámetros por referencia: Al pasar una variable como referencia a una función, los cambios realizados en la función se reflejarán en la variable original. Esto evita la necesidad de pasar grandes estructuras de datos o clases por valor, lo que podría ser ineficiente.

Retorno de valores por referencia: Una función puede devolver una referencia a una variable local, permitiendo que el resultado de la función se asigne directamente a una variable existente en el código de llamada.

Evitar la copia de objetos grandes: En lugar de realizar una copia completa de un objeto grande, se puede utilizar una referencia para acceder y manipular directamente el objeto original.

Es importante tener en cuenta que una referencia siempre debe estar inicializada con una variable existente y no puede ser modificada para referenciar a otro objeto después de su inicialización. Además, no se pueden tener referencias nulas, lo que significa que siempre deben referenciar a una variable válida.

Aquí tienes un ejemplo que ilustra el uso de referencias:

En este ejemplo, tenemos una función llamada incrementar que recibe un parámetro por referencia. La función incrementa el valor de la referencia en 1 (ref++).

En el main(), declaramos la variable numero y la inicializamos con el valor 10.

Luego, mostramos el valor original de numero.

Después, llamamos a la función incrementar pasando numero como argumento.

Como numero se pasa por referencia, cualquier cambio realizado en la función incrementar se reflejará en la variable original. En este caso, se incrementa el valor de numero en 1.

Finalmente, mostramos el valor de numero después de llamar a la función incrementar y verificamos que se haya modificado correctamente.

La salida del programa será:

Gestión de la memoria

Para gestionar la memoria, se utilizan los operadores new y delete (y sus variantes new[] y delete[]) para la asignación y liberación dinámica de memoria respectivamente.

Operador new.- El operador new se utiliza para asignar memoria dinámicamente y crear objetos en el montón (heap). La sintaxis básica es la siguiente:

Aquí, tipo representa el tipo de dato para el que se está asignando memoria y nombrePuntero es el puntero donde se almacenará la dirección de memoria asignada. Por ejemplo:

Este código asigna memoria para un entero y el puntero punteroEntero apunta a esa memoria asignada. Si se necesita asignar memoria para un arreglo, se utiliza la siguiente sintaxis:

Donde tamano es el número de elementos en el arreglo. Por ejemplo:

Este código asigna memoria para un arreglo de 5 enteros y el puntero arregloEnteros apunta a la memoria asignada.


Operador delete.- El operador delete se utiliza para liberar la memoria asignada dinámicamente previamente. La sintaxis básica es la siguiente:

Aquí, nombrePuntero es el puntero que apunta a la memoria que se desea liberar. Por ejemplo:

Este código libera la memoria a la que apunta punteroEntero.

Para liberar la memoria asignada para un arreglo, se utiliza la siguiente sintaxis:

Por ejemplo:

Este código libera la memoria asignada para el arreglo de enteros.

Es importante destacar que se debe utilizar delete o delete[] para liberar toda la memoria asignada dinámicamente, de lo contrario se producirá una fuga de memoria.

Recuerda que la asignación y liberación dinámica de memoria debe realizarse con cuidado para evitar fugas de memoria y comportamientos indefinidos.

Además, se recomienda considerar el uso de punteros inteligentes (como unique_ptr y shared_ptr) para gestionar automáticamente la memoria asignada dinámicamente y evitar problemas de liberación de memoria.

Reasignar un bloque de memoria

En C++, no puedes cambiar directamente el tamaño de un bloque de memoria asignado previamente con new o new[]. Una vez que se ha asignado un bloque de memoria, su tamaño no se puede modificar.

Si necesitas cambiar el tamaño de un bloque de memoria, la solución más común es asignar un nuevo bloque de memoria con el tamaño deseado y luego transferir o copiar los datos del bloque de memoria original al nuevo bloque.

Después, puedes liberar el bloque de memoria original.

Aquí tienes un ejemplo de cómo puedes cambiar el tamaño de un bloque de memoria:

En este ejemplo, se asigna inicialmente un bloque de memoria de tamaño 10 utilizando new[] y se guarda su dirección de memoria en el puntero ptr.

Luego, se crea un nuevo bloque de memoria de tamaño 20 utilizando new[] y se guarda su dirección de memoria en el puntero newPtr.

A continuación, se copian los datos del bloque original al nuevo bloque utilizando un bucle for.

Después, se libera la memoria del bloque original utilizando delete[] y se puede utilizar el nuevo bloque de memoria con el tamaño cambiado para lo que sea necesario.

Finalmente, cuando ya no necesites el nuevo bloque de memoria, debes liberarlo utilizando delete[] para evitar fugas de memoria.

Operaciones con punteros

A las variables de tipo puntero, además de los operadores &, * y el operador de asignación =, se les puede aplicar los operadores aritméticos + y (solo con enteros), los operadores unitarios ++ y y los operadores de relación:

Índice:

Operación de asignación

El lenguaje C++ permite que un puntero pueda ser asignado a otro puntero. por ejemplo:

Tras la ejecución del programa obtenemos:

Después de ejecutarse la asignación q = p, q y p apuntan a la misma localización de la memoria, a la variable a. Por lo tanto, a, *p y *q son el mismo dato; es decir, 10 (la dirección varía según el ordenador).

Operaciones aritméticas

A un puntero podemos restar o sumar un entero. La aritmética de punteros difiere de la aritmética normal en que aquí la unidad equivale a un objeto del tipo del puntero; es decir, si sumamos 1, implica que el puntero pasará a apuntar al siguiente objeto, del tipo del puntero, más allá del apuntado actualmente.

Por ejemplo, supongamos que p y q son variables de tipo puntero que apuntan a elementos de una misma matriz llamada x:

La operación p + n, siendo n un entero, avanzará el puntero n enteros más allá de donde estaba apuntando. Por ejemplo p = p + 3, hace que avance tres enteros apuntando ahora a x[6].

Así mismo la operación p – q, después de la operación p = p + 3 anterior dará como resultado 6 (elemento de tipo int).

La operación p – n, siendo n un entero, también es válida, si partimos de que p apunta a x[6], resultando la resta de 3 posiciones:

Si p apunta a x[3], p++ hace que apunte a la siguiente posición o sea x[4].

p– hace que p apunte de nuevo a x[3].

No está permitido sumar, multiplicar, dividir o rotar punteros ni sumarles los tipos real.

Comparación de punteros

Cuando comparamos dos punteros, en realidad se están comparando dos enteros, puesto que una dirección es un número entero. Esta operación tiene sentido si ambos punteros apuntan a elementos de la misma matriz. Por ejemplo:

La primera sentencia if indica que el puntero avanzará n elementos si se cumple que la dirección q + n es menor o igual que p.

La segunda sentencia if indica que q pasará a apuntar al siguiente elemento de la matriz si la dirección por él especificada no es nula y es menor o igual que la especificada por p.

Punteros genéricos

Un puntero a cualquier tipo de objeto puede ser convertido a tipo void *. Por eso, un puntero de tipo void * recibe el nombre de puntero genérico. Por ejemplo:

En cambio el compilador C++ no puede asumir nada sobre la memoria apuntada por void *. Se necesita, por lo tanto, una conversión explícita. El operador dynamic_cast, al examinar el tipo del objeto apuntado, no puede realizar la conversión, por lo que se tiene que utilizar static_cast:

En el ejemplo anterior, vemos que para convertir un puntero genérico a un puntero int se ha realizado una conversión explícita (conversión cast).

Puntero nulo

Generalmente, un puntero se puede iniciar como cualquier otra variable, aunque los únicos valores significativos son 0 o la dirección de un objeto previamente definido. El lenguaje C++ garantiza que un puntero que apunte a un objeto válido nunca tendrá un valor 0. El valor cero se utiliza para indicar que ha ocurrido un error, en otras palabras, que una determinada operación no se ha podido realizar.

En general, no tiene sentido asignar enteros a punteros porque quien gestiona la memoria es el sistema operativo, y por lo tanto es él el que sabe en todo momento que direcciones están libres y cuáles están ocupadas. Por ejemplo:

La asignación de la memoria no tiene sentido porque no sabemos si la dirección será o está usada por nuestro equipo.

Punteros y objetos constantes

Una declaración de un puntero precedida por const hace que el objeto apuntado sea una constante, cosa que no sucede con un puntero. Por ejemplo:

Si lo que pretendemos es declarar un puntero como una constante, lo escribiremos así:

Para hacer que tanto el puntero como el objeto apuntado sean constantes, procederemos como se indica a continuación.