Se debe tener en cuenta que, en el caso de C ++, es una idea errónea común de que "es necesario realizar una gestión manual de la memoria". De hecho, normalmente no realiza ninguna gestión de memoria en su código.
Objetos de tamaño fijo (con duración de alcance)
En la gran mayoría de los casos cuando necesita un objeto, el objeto tendrá una vida útil definida en su programa y se creará en la pila. Esto funciona para todos los tipos de datos primitivos incorporados, pero también para instancias de clases y estructuras:
class MyObject {
public: int x;
};
int objTest()
{
MyObject obj;
obj.x = 5;
return obj.x;
}
Los objetos de la pila se eliminan automáticamente cuando finaliza la función. En Java, los objetos siempre se crean en el montón y, por lo tanto, deben eliminarse mediante algún mecanismo, como la recolección de basura. Esto no es un problema para los objetos de la pila.
Objetos que administran datos dinámicos (con duración de alcance)
El uso del espacio en la pila funciona para objetos de un tamaño fijo. Cuando necesita una cantidad variable de espacio, como una matriz, se utiliza otro enfoque: la lista se encapsula en un objeto de tamaño fijo que administra la memoria dinámica por usted. Esto funciona porque los objetos pueden tener una función de limpieza especial, el destructor. Se garantiza que se llamará cuando el objeto salga del alcance y haga lo contrario al constructor:
class MyList {
public:
// a fixed-size pointer to the actual memory.
int* listOfInts;
// constructor: get memory
MyList(size_t numElements) { listOfInts = new int[numElements]; }
// destructor: free memory
~MyList() { delete[] listOfInts; }
};
int listTest()
{
MyList list(1024);
list.listOfInts[200] = 5;
return list.listOfInts[200];
// When MyList goes off stack here, its destructor is called and frees the memory.
}
No hay administración de memoria en absoluto en el código donde se utiliza la memoria. Lo único que tenemos que asegurarnos es que el objeto que escribimos tenga un destructor adecuado. No importa cómo dejemos el alcance de listTest
, ya sea a través de una excepción o simplemente regresando de él, se llamará al destructor ~MyList()
y no necesitamos administrar ninguna memoria.
(Creo que es una decisión de diseño graciosa usar el operador binary NOT , ~
, para indicar el destructor. Cuando se usa en números, invierte los bits; en analogía , aquí indica que lo que hizo el constructor está invertido.)
Básicamente, todos los objetos C ++ que necesitan memoria dinámica utilizan esta encapsulación. Se le ha llamado RAII ("la adquisición de recursos es la inicialización"), que es una forma bastante extraña de expresar la idea simple de que los objetos se preocupan por sus propios contenidos; lo que adquieren es de ellos para limpiar.
Objetos polimórficos y vida útil más allá del alcance
Ahora, ambos de estos casos fueron para memoria que tiene una vida útil claramente definida: la vida útil es la misma que el alcance. Si no queremos que un objeto caduque cuando dejemos el alcance, hay un tercer mecanismo que puede administrar la memoria para nosotros: un puntero inteligente. Los punteros inteligentes también se utilizan cuando tiene instancias de objetos cuyo tipo varía en el tiempo de ejecución, pero que tienen una interfaz común o una clase base:
class MyDerivedObject : public MyObject {
public: int y;
};
std::unique_ptr<MyObject> createObject()
{
// actually creates an object of a derived class,
// but the user doesn't need to know this.
return std::make_unique<MyDerivedObject>();
}
int dynamicObjTest()
{
std::unique_ptr<MyObject> obj = createObject();
obj->x = 5;
return obj->x;
// At scope end, the unique_ptr automatically removes the object it contains,
// calling its destructor if it has one.
}
Hay otro tipo de puntero inteligente, std::shared_ptr
, para compartir objetos entre varios clientes. Solo eliminan el objeto contenido cuando el último cliente queda fuera del alcance, por lo que se pueden usar en situaciones en las que no se sabe cuántos clientes habrá ni cuánto tiempo usarán el objeto.
En resumen, vemos que realmente no realiza ninguna gestión de memoria manual. Todo está encapsulado y luego se cuida mediante una gestión de memoria completamente automática y basada en el alcance. En los casos en que esto no es suficiente, se utilizan punteros inteligentes que encapsulan la memoria en bruto.
Se considera una práctica extremadamente mala utilizar los punteros sin formato como propietarios de recursos en cualquier lugar en el código C ++, las asignaciones en bruto fuera de los constructores y las llamadas en bruto delete
fuera de los destructores, ya que son casi imposibles de administrar cuando se producen excepciones, y en general difícil de usar de forma segura.
Lo mejor: esto funciona para todos los tipos de recursos
Uno de los mayores beneficios de RAII es que no se limita a la memoria. En realidad, proporciona una forma muy natural de administrar recursos como archivos y sockets (apertura / cierre) y mecanismos de sincronización como mutexes (bloqueo / desbloqueo). Básicamente, todos los recursos que pueden adquirirse y deben liberarse se administran exactamente de la misma manera en C ++, y nada de esta gestión se deja al usuario. Todo está encapsulado en clases que se adquieren en el constructor y se liberan en el destructor.
Por ejemplo, una función que bloquea un mutex generalmente se escribe así en C ++:
void criticalSection() {
std::scoped_lock lock(myMutex); // scoped_lock locks the mutex
doSynchronizedStuff();
} // myMutex is released here automatically
Otros idiomas hacen esto mucho más complicado, ya sea requiriéndole que lo haga manualmente (por ejemplo, en una cláusula finally
) o generen mecanismos especializados que resuelvan este problema, pero no de una manera especialmente elegante (generalmente más adelante en su vida, cuando suficientes personas han sufrido de la deficiencia). Dichos mecanismos son try-with-resources en Java y la declaración utilizando en C #, que son aproximaciones de RAII de C ++.
Entonces, para resumir, todo esto fue un relato muy superficial de RAII en C ++, pero espero que ayude a los lectores a comprender que la memoria e incluso la gestión de recursos en C ++ no suele ser "manual", sino en su mayoría automático.