Cabe señalar que es, en el caso de C ++, una idea errónea común de que "es necesario hacer una gestión manual de la memoria". De hecho, generalmente no realiza ninguna gestión de memoria en su código.
Objetos de tamaño fijo (con alcance de por vida)
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 integrados, 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 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 pila.
Objetos que administran datos dinámicos (con alcance de por vida)
Usar 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 esté fuera de alcance y haga lo contrario del 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 usa la memoria. Lo único que debemos asegurarnos es que el objeto que escribimos tiene un destructor adecuado. No importa cómo dejemos el alcance de listTest
, ya sea a través de una excepción o simplemente al regresar de él, ~MyList()
se llamará al destructor y no necesitamos administrar ninguna memoria.
(Creo que es una decisión divertida de diseño usar el operador NOT binario~
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 usan esta encapsulación. Se le ha llamado RAII ("adquisición de recursos es inicialización"), que es una forma bastante extraña de expresar la simple idea de que los objetos se preocupan por sus propios contenidos; lo que adquieren es suyo para limpiarlo.
Objetos polimórficos y vida útil más allá del alcance
Ahora, ambos 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 dejamos el alcance, existe un tercer mecanismo que puede administrar la memoria para nosotros: un puntero inteligente. Los punteros inteligentes también se usan cuando tiene instancias de objetos cuyo tipo varía en tiempo de ejecución, pero que tienen una interfaz común o 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 su objeto contenido cuando el último cliente queda fuera de alcance, por lo que pueden usarse en situaciones en las que se desconoce por completo cuántos clientes habrá y durante cuánto tiempo usarán el objeto.
En resumen, vemos que realmente no se realiza ninguna gestión manual de la memoria. Todo se encapsula y se resuelve 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 sin procesar.
Se considera una práctica extremadamente mala usar punteros sin formato como propietarios de recursos en cualquier parte del código C ++, asignaciones sin formato fuera de los constructores y delete
llamadas sin formato fuera de los destructores, ya que son casi imposibles de administrar cuando se producen excepciones y, en general, difíciles de usar de manera segura.
Lo mejor: esto funciona para todo tipo 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, cada recurso que puede adquirirse y liberarse se administra exactamente de la misma manera en C ++, y nada de esta administració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 que esto sea mucho más complicado, ya que requieren que lo haga manualmente (por ejemplo, en una finally
cláusula) o generan mecanismos especializados que resuelven este problema, pero no de una manera particularmente elegante (generalmente más adelante en su vida, cuando suficientes personas tienen sufrió de la deficiencia). Dichos mecanismos son de prueba con recursos en Java y la declaración de uso en C #, los cuales son aproximaciones de la RAII de C ++.
Entonces, para resumir, todo esto fue una explicación muy superficial de RAII en C ++, pero espero que ayude a los lectores a comprender que la memoria e incluso la administración de recursos en C ++ generalmente no es "manual", sino que en realidad es principalmente automática.