Para gestionar la memoria dinámica, C++ proporciona dos operadores new
y delete
para reservar y liberar memoria, respectivamente.
En C++, la memoria se puede gestionar de dos maneras principales:
Memoria automática: Se asigna automáticamente cuando se declara una variable local o global y se libera automáticamente cuando la variable sale de su ámbito.
Memoria dinámica: Se asigna y se libera manualmente por el programador durante la ejecución del programa utilizando los operadores
new
ydelete
.
Básicamente, la memoria dinámica es espacio de almacenamiento que necesitamos, pero no sabemos cuanto necesitamos hasta tiempo de ejecución (por ejemplo, porque depende de una entrada del sistema).
A diferencia de la memoria automática, la memoria asignada con new
persiste hasta que se libera explícitamente mediante delete
.
Qué es new
El operador new
en C++ se utiliza para reservar memoria dinámica (es decir, para decirle al sistema “eh!, dame un sitio donde guardar esto”).
tipo_dato* puntero = new tipo_dato;
- tipo_dato: Es el tipo de dato para el cual se está asignando memoria.
- puntero: Es un puntero que almacenará la dirección de la memoria asignada.
Vamos a verlo con un ejemplo,
int* p = new int; // Asigna memoria para un entero
*p = 5; // Almacena el valor 5 en la memoria asignada
std::cout << *p << std::endl; // Imprime 5
En este ejemplo,
- Se asigna memoria para un entero
- El puntero
p
apunta a esa memoria - Se almacena el valor
5
en esa ubicación de memoria. - A través de él podemos acceder al valor almacenado.
Asignación de arrays dinámicos
El operador new
también puede utilizarse para asignar memoria para arrays dinámicos. La sintaxis es ligeramente diferente:
tipo_dato* puntero = new tipo_dato[tamaño];
Es decir, básicamente es igual, pero ponemos []
indicando el tamaño de la variable a reservar. Vamos a verlo con un ejemplo,
int* arr = new int[5]; // Asigna memoria para un array de 5 enteros
for(int i = 0; i < 5; ++i) {
arr[i] = i * 2; // Inicializa el array con valores
}
for(int i = 0; i < 5; ++i) {
std::cout << arr[i] << " "; // Imprime los valores del array
}
std::cout << std::endl;
Este ejemplo muestra cómo asignar y usar un array dinámico. Aquí, arr
es un puntero a un bloque de memoria que puede contener 5 enteros.
Qué es delete
El operador delete
se utiliza para liberar la memoria que fue asignada dinámicamente con new
.
delete puntero;
- puntero: Es el puntero que apunta a la memoria que debe ser liberada.
Vamos a verlo con un ejemplo
int* p = new int; // Asigna memoria para un entero
*p = 5; // Almacena el valor 5 en la memoria asignada
delete p; // Libera la memoria
p = nullptr; // Evita que el puntero apunte a memoria liberada
En este ejemplo,
- Después de utilizar la memoria asignada, se libera con
delete
- Se asigna
nullptr
al puntero para evitar que apunte a una dirección de memoria que ya no es válida.
Liberación de arrays dinámicos
Cuando se asigna memoria para un array dinámico, es importante liberar esa memoria correctamente utilizando el operador delete[]
.
delete[] puntero;
Por ejemplo
int* arr = new int[5]; // Asigna memoria para un array de 5 enteros
for(int i = 0; i < 5; ++i) {
arr[i] = i * 2;
}
// Uso del array
delete[] arr; // Libera la memoria del array
arr = nullptr; // Previene acceso a memoria liberada
En este ejemplo, la memoria asignada para el array se libera utilizando delete[]
.
Es importante usar delete[]
en lugar de delete
para evitar comportamientos indefinidos (a.k.a que te pete algo)
Errores comunes al usar new y delete
El error más habitual es olvidarnos de hacer un delete
cuando ya no necesitamos una variable, o cuando vamos a volver a reservarla.
void fugaMemoria() {
int* p = new int; // Asigna memoria para un entero
// Se olvida liberar la memoria
p = new int; // Volvermos a reservar memoria
}
En este caso, el puntero p
se destruye al salir de la función, pero la memoria asignada no se libera, lo que provoca una fuga.
A esto se le llama fuga de memoria (memory leak) y es un error muy habitual.
Esto lleva a que el programa vaya consumiendo más y más memoria y, eventualmente, haga catapum 💥 (y además es una guarrada como programadores).
La memoria se liberará en todo caso al cerrar el programa. No os penséis que es una cosa permanente al sistema.
Otro error habitual es acceder a memoria que ya ha sido liberada con delete
, y que igualmente va a ocasiones un error en tu programa.
int* p = new int(10);
delete p; // Libera la memoria
*p = 5; // Error: acceso a memoria liberada
Este ejemplo muestra un acceso a memoria después de que ha sido liberada, lo que puede causar un otro catapum de tu programa 💥.
Liberar la misma memoria más de una vez es otro error común, conocido como doble liberación.
void dobleLiberacion() {
int* p = new int(10);
delete p;
delete p; // Error: liberación doble
}
En este caso, se intenta liberar la misma memoria dos veces, lo cual es un error grave.
Buenas prácticas
Emparejar new y delete
Cada vez que se usa new
para asignar memoria, lo más probable es que deba haber en algún sitio su correspondiente a delete
para liberar esa memoria.
Usar nullptr después de delete
Después de liberar memoria con delete
, es una buena práctica asignar nullptr
al puntero. Esto evita el acceso accidental a memoria liberada.
delete p;
p = nullptr;
Usar punteros inteligentes
En lugar de manejar manualmente la memoria dinámica con new
y delete
, se recomienda usar punteros inteligentes (smart pointers
) como std::unique_ptr
y std::shared_ptr
, que gestionan automáticamente la memoria y ayudan a prevenir fugas de memoria y accesos inválidos.
No sobre-reservar memoria
Es importante reservar cuidadosamente la cantidad de memoria necesaria (no reservar a lo loco).
Al final es un recurso limitado, y como en todo recurso, hay que reducir su uso si no es necesario.