Deve-se notar que, no caso do C ++, é um equívoco comum que "você precisa executar o gerenciamento manual de memória". De fato, você geralmente não faz nenhum gerenciamento de memória no seu código.
Objetos de tamanho fixo (com vida útil do escopo)
Na grande maioria dos casos em que você precisa de um objeto, o objeto terá uma vida útil definida em seu programa e é criado na pilha. Isso funciona para todos os tipos de dados primitivos internos, mas também para instâncias de classes e estruturas:
class MyObject {
public: int x;
};
int objTest()
{
MyObject obj;
obj.x = 5;
return obj.x;
}
Os objetos de pilha são removidos automaticamente quando a função termina. Em Java, os objetos são sempre criados no heap e, portanto, precisam ser removidos por algum mecanismo como a coleta de lixo. Este não é um problema para objetos de pilha.
Objetos que gerenciam dados dinâmicos (com vida útil do escopo)
Usar espaço na pilha funciona para objetos de tamanho fixo. Quando você precisa de uma quantidade variável de espaço, como uma matriz, outra abordagem é usada: A lista é encapsulada em um objeto de tamanho fixo que gerencia a memória dinâmica para você. Isso funciona porque os objetos podem ter uma função de limpeza especial, o destruidor. É garantido que será chamado quando o objeto sair do escopo e fizer o oposto do construtor:
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.
}
Não há gerenciamento de memória no código em que a memória é usada. A única coisa que precisamos ter certeza é que o objeto que escrevemos tem um destruidor adequado. Não importa como deixemos o escopo listTest
, seja por uma exceção ou simplesmente retornando, o destruidor ~MyList()
será chamado e não precisamos gerenciar nenhuma memória.
(Eu acho que é uma decisão engraçada de design usar o operador binário NOT ,, ~
para indicar o destruidor. Quando usado em números, ele inverte os bits; por analogia, aqui indica que o que o construtor fez é invertido.)
Basicamente, todos os objetos C ++ que precisam de memória dinâmica usam esse encapsulamento. Foi chamado RAII ("aquisição de recursos é inicialização"), que é uma maneira bastante estranha de expressar a idéia simples de que os objetos se preocupam com seu próprio conteúdo; o que eles adquirem é deles para limpar.
Objetos polimórficos e vida útil além do escopo
Agora, os dois casos foram para memória com uma vida útil claramente definida: a vida útil é igual ao escopo. Se não queremos que um objeto expire ao sair do escopo, existe um terceiro mecanismo que pode gerenciar a memória para nós: um ponteiro inteligente. Ponteiros inteligentes também são usados quando você tem instâncias de objetos cujo tipo varia em tempo de execução, mas que possuem uma interface ou classe base comum:
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.
}
Há outro tipo de ponteiro inteligente std::shared_ptr
, para compartilhar objetos entre vários clientes. Eles apenas excluem seu objeto contido quando o último cliente fica fora do escopo, para que possam ser usados em situações em que é completamente desconhecido quantos clientes haverá e quanto tempo eles usarão o objeto.
Em resumo, vemos que você realmente não faz nenhum gerenciamento manual de memória. Tudo é encapsulado e, então, resolvido por meio de um gerenciamento de memória completamente automático e baseado em escopo. Nos casos em que isso não é suficiente, são usados indicadores inteligentes que encapsulam a memória bruta.
É uma prática extremamente ruim usar ponteiros brutos como proprietários de recursos em qualquer lugar do código C ++, alocações brutas fora dos construtores e delete
chamadas brutas fora dos destruidores, pois são quase impossíveis de gerenciar quando ocorrem exceções e geralmente difíceis de usar com segurança.
O melhor: isso funciona para todos os tipos de recursos
Um dos maiores benefícios do RAII é que ele não se limita à memória. Na verdade, fornece uma maneira muito natural de gerenciar recursos, como arquivos e soquetes (abertura / fechamento) e mecanismos de sincronização, como mutexes (bloqueio / desbloqueio). Basicamente, todos os recursos que podem ser adquiridos e devem ser liberados são gerenciados exatamente da mesma maneira em C ++, e nada desse gerenciamento é deixado para o usuário. Tudo é encapsulado em classes que são adquiridas no construtor e liberadas no destruidor.
Por exemplo, uma função que bloqueia um mutex geralmente é escrita assim em C ++:
void criticalSection() {
std::scoped_lock lock(myMutex); // scoped_lock locks the mutex
doSynchronizedStuff();
} // myMutex is released here automatically
Outras línguas tornam isso muito mais complicado, exigindo que você faça isso manualmente (por exemplo, em uma finally
cláusula) ou geram mecanismos especializados que resolvem esse problema, mas não de uma maneira particularmente elegante (geralmente mais tarde na vida, quando há pessoas suficientes). sofria da deficiência). Esses mecanismos são try-with-resources em Java e a instrução using em C #, ambas aproximações do RAII do C ++.
Então, para resumir, tudo isso foi um relato muito superficial do RAII em C ++, mas espero que ajude os leitores a entender que a memória e até o gerenciamento de recursos em C ++ não são geralmente "manuais", mas na maioria das vezes automáticos.