Ponteiros, Liberar memoria excluindo objetos e variáveis não utilizadas e mais algumas coisas [RESOL

1. Ponteiros, Liberar memoria excluindo objetos e variáveis não utilizadas e mais algumas coisas [RESOL

Silas Henrique
silash35

(usa Arch Linux)

Enviado em 26/03/2018 - 17:32h

Se tem uma coisa que não entra na minha cabeça é os comandos relacionados a memoria em C++. Oque eu queria era
criar uma variável/objeto
Usar a variável/Objeto
e depois deletar a Variável/objeto de modo a liberar espaço na memoria, ou seja, tornar possível que uma outra nova Variável/Objeto use o mesmo espaço de memoria do antigo que foi excluído.
Então, após pesquisar muito sobre isso,descobrir que se usar o comando delete em um ponteiro, oque ele estiver apontando vai desaparecer. Só que o ponteiro não é deletado e eu nem sei se o espaço da memoria que foi deletado poderá ser usado de novo.
Outra coisa que eu não entendo, porque o comando Delete só funciona com ponteiros ? Seria bem mais eficiente se pudesse usar
delete &variavel; 

Quando eu aprendi sobre ponteiros, eles eram simplesmente uma variável que armazenavam a posição na memoria de outra variável, mas pelo jeito eles tem varias outras funções que eu ainda não entendi direito. Por exemplo a diferença entre "std::string" e "const char*" Como que criar uma variável char do tipo const (Que não muda) como ponteiro pode do nada fazer o trabalho e uma string, e oque isso tem a ver com alocação de memoria ? E oque muda se eu colocar um ponteiro com o valor NULL ?
Desculpe se não fui claro, qualquer duvida sobre a pergunta eu respondo


  


2. MELHOR RESPOSTA

Paulo
paulo1205

(usa Ubuntu)

Enviado em 27/03/2018 - 14:24h

silas333 escreveu:

Se tem uma coisa que não entra na minha cabeça é os comandos relacionados a memoria em C++. Oque eu queria era
criar uma variável/objeto
Usar a variável/Objeto
e depois deletar a Variável/objeto de modo a liberar espaço na memoria, ou seja, tornar possível que uma outra nova Variável/Objeto use o mesmo espaço de memoria do antigo que foi excluído.


Seria bom se você esclarecesse sua dúvida. É mera questão de como funciona? Precisa de algum exemplo, para ilustrar?

Você pode fazer em C++ tudo isso que você falou, e de mais de uma maneira.

O C++, bem como C, tem três tipos de áreas de armazenamento, a saber:

  • estático, que é aquele reservado pelo compilador desde o momento da compilação, e que permanecerá alocado e referenciado pelos mesmos objetos desde o início da execução do programa até seu encerramento. São estáticos todos os objetos e variáveis globais, os declarados em nível de namespace e aqueles explicitamente declarados com o modificador static dentro de classes ou de funções. Objetos estáticos são inicializados apenas uma vez, mesmo que tenham sido declarados dentro de um laço de repetição, e eventuais alterações de valor são persistentes, mesmo após o fluxo de execução sair do escopo em que tais objetos são visíveis.

  • automático, que é aquele cujo espaço é automaticamente criado e liberado durante a execução do programa. Esse é o caso de variáveis declaradas dentro de funções ou de blocos sem o modificador static. A memória para tais objetos pode ser considerada como alocada no momento em que o fluxo de execução atravessa o ponto da declaração desses objetos, e eles permanecem visíveis e com valores válidos apenas até o fim do bloco que contém sua declaração. Se a declaração especificar valores iniciais (ou se a classe do objeto oferecer um construtor default), tal inicialização será feita tantas vezes quantas o fluxo de execução atravessar o ponto da inicialização (ou seja, não existe persistência entre iterações ou invocações sucessivas). Variáveis automáticas costumam ter exatamente o tipo dos dados que elas contêm, não sendo necessário referir-se à área alocada por meio de ponteiros. [Como detalhe de implementação, nos nossos PCs, objetos automáticos costumam ser alocados na pilha do processador, pois a alocação e a liberação feitas dessa forma são extremamente eficientes (apenas uma instrução em Assembly). Contudo, como o espaço na pilha costuma ser mais limitado do que o que se pode obter com alocação dinâmica, o armazenamento automático não é indicado para objetos muito grandes.]

  • dinâmico, que é quando o programador tem de explicitamente solicitar memória para objetos que ele pretende manipular e, do mesmo modo, devolver explicitamente essa memória quando tais objetos não forem mais necessários. [Nos nossos PCs, geralmente a alocação de objetos dinâmicos não ocorre na pilha do processador, mas numa região de memória separada, entregue ao programa pelo sistema operacional, e chamada geralmente de memória para livre armazenamento (free store).]

Tanto a memória automática quanto a dinâmica permitem ao programa o reaproveitamento de regiões da memória por diferentes objetos ao longo da execução do programa, e geralmente você não precisa de se preocupar com em que região de memória cada objeto reside: tendo sido devidamente programados, geralmente os próprios objetos cuidam de fazer as alocações e desalocações de memória para você. [A exceção notável é quando você mesmo implementa os tipos de dados que terão de fazer internamente essas alocações e desalocações de recursos — que não necessariamente serão apenas memória. A técnica de RAII (Resource Acquisition Is Initialization), ajuda muito a conseguir tal efeito, e também o ajuda a estruturar a eventual implementação de alocações/desalocações que você eventualmente tiver de fazer.]

Então, após pesquisar muito sobre isso,descobrir que se usar o comando delete em um ponteiro, oque ele estiver apontando vai desaparecer. Só que o ponteiro não é deletado e eu nem sei se o espaço da memoria que foi deletado poderá ser usado de novo.


Aqui se há de ser cauteloso com a terminologia.

Você está certo ao dizer que “o ponteiro não é deletado”, mas eu não sei se compreendi o que você quis dizer com isso, nem se você entende os mecanismos e as implicações da operação (nem, aliás, se você tem por objetivo entender tais mecanismos e implicações).

Um ponto, que eu até mencionei ontem em outra mensagem deste fórum, é que um ponteiro não é um tipo especial de variável; um ponteiro é um valor. Esse valor pode vir de uma variável (que tem um tipo específico para guardar valores de ponteiros, que é distinto dos tipos de valores numéricos, de arrays e de outros tipos compostos), mas pode também ser calculado ou obtido por decaimento (a partir de arrays).

E, de fato, delete não apaga ponteiros. delete libera o conteúdo referenciado por um ponteiro, desde que tal ponteiro indique um objeto que tenha sido criado por meio do operador new (em particular, não faz sentido chamar delete para devolver ao sistema memória estática, que foi feita para não ser devolvida enquanto o programa permanecer em execução, nem de memória automática, cuja devolução já é garantida quando ela sai de escopo).

E é interessante saber que o operando do operador delete é um valor. Se p é uma variável de tipo ponteiro e eu mando executar o comando “delete p;”, o objeto referenciado por p será desalocado, mas o valor da variável p não será modificado, ainda que, a partir desse momento, ele não indique mais um objeto válido para o programa.

(A discussão acima, feita em termos de new e delete, usados para alocar objetos individuais, tem seus análogos com a alocação e desalocação de múltiplos objetos numa só operação, por meio dos operadores new [] e delete[].)

Outra coisa que eu não entendo, porque o comando Delete só funciona com ponteiros ? Seria bem mais eficiente se pudesse usar
delete &variavel; 


De fato, isso é tão mais eficiente que, se variavel indicar um objeto automático, o compilador faz essa operação para você sem que você precise nem mesmo pedir.

Quando eu aprendi sobre ponteiros, eles eram simplesmente uma variável que armazenavam a posição na memoria de outra variável, mas pelo jeito eles tem varias outras funções que eu ainda não entendi direito.


De novo, ponteiros são valores que indicam endereços. Variáveis podem ser de tipos de dados próprios para guardar valores que são ponteiros, mas não é necessário ter uma variável de tipo ponteiro para se ter ou obter um valor que seja um ponteiro.

Além disso, variáveis ponteiro não apontam necessariamente apenas para o endereço de outras variáveis; esse é apenas um dos usos que eles podem ter. Outros usos possíveis são apontar para objetos criados dinamicamente, apontar para endereços constantes (para, por exemplo, ter acesso a um dispositivo de hardware mapeado em memória) e para deliberadamente apontar para endereços inválidos (tais como nullptr), a fim de indicar que o objeto que seria de se esperar não existe.

Por exemplo a diferença entre "std::string" e "const char*" Como que criar uma variável char do tipo const (Que não muda) como ponteiro pode do nada fazer o trabalho e uma string, e oque isso tem a ver com alocação de memoria ?


Não é “do nada”.

O C++ herdou do C (com algumas pequenas modificações) convenções para representação de strings. Em C, em nível de linguagem, uma string é uma constante literal expressa entre aspas, cujo tipo de dados é um array de caracteres com tantos elementos quantos caracteres houver entre aspas mais um byte nulo, disposto após esses caracteres. Então, por exemplo, o tipo de dados de "Paulo" é char [6] (isto é: array com 6 elementos do tipo char), e tal array é armazenado pelo compilador junto com as constantes do programa como seis bytes consecutivos com os valores 'P', 'a', 'u', 'l', 'o' e '\0'. Contudo, quando usado numa expressão que não envolva a aplicação direta dos operadores sizeof ou & sobre o array, esse array decai para o tipo char * (ponteiro para dado do tipo char), com um valor que aponta para o primeiro elemento do array (no caso, para o caráter 'P').

Em C++ é quase a mesma coisa, com a diferença que que o tipo da constante é const char [6] (array com 6 elementos do tipo const char) e o decaimento é para const char * (ponteiro para caráter constante).

A classe std::string da biblioteca do C++ tem construtores que aceitam argumentos do tipo const char *, e que providenciam a cópia dos caracteres apontados por tais argumentos dentro de um novo array, dinamicamente alocado por esses construtores como parte do objeto que está sendo construído.

Desse modo, imagine o seguinte programa, em que a classe my_string apresenta uma versão reduzida e simplificada do que a classe std::string também faz.

#include <cstring>

class my_string {
private:
char *_string_data;
size_t _string_length;

public:
// Construtor: constrói um objeto a partir de uma string do C (ou, por default, uma string vazia).
my_string(const char *cstring=""): _string_length(strlen(cstring)) {
_string_data=new char[_string_length+1]; // Aloco um byte a mais para também acomodar um byte nulo (útil para função c_str(), abaixo).
char *p_strdata=_string_data;
do
*p_strdata++ = *cstring;
while(*cstring++);
}

// Outros construtores (não detalhados porque não é o foco aqui).
my_string(const my_string &other){ /* ... */ } // Construtor de cópia.
my_string(my_string &&old){ /* ... */ } // Construtor de movimentação.
// ... etc.

// Destrutor (note que eu o declaro como virtual).
virtual ~my_string(){
delete[] _string_data;
}

/**** Outras operações, para tornar o objeto útil. ****/
// Referência a caracteres individuais da string.
char &operator[](size_t offset){ return _string_data[offset]; } // Objetos não-constantes.
const char &operator[](size_t offset) const { return _string_data[offset; } // Objetos constantes.

// Modifica conteúdo da string, acrescentando novos caracteres ao final.
my_string &append(const my_string &other){
// Implementação nada otimizada, para ser bem didática.
size_t new_size=_string_length+other._string_length;
char *new_data=new char[new_size+1];
char *p_newdata=new_data;
for(size_t n=0; n<_string_length; n++)
*p_newdata++=_string_data[n];
for(size_t n=0; n<other._string_length; n++)
*p_newdata++=other._string_data[n];
*p_new_data='\0';

delete[] _string_data; // Libera conteúdo anterior.
_string_data=new_data; // Conteúdo, agora, é aquele da concatenação das duas strings originais.
_string_length=new_size; // E o mesmo para o tamanho.

return *this;
}
// Idem, mas na forma de operador (+=).
inline my_string operator+=(const my_string &other){ return append(other); }

// Limpa conteúdo da string.
my_string &clear(){
char *empty=new char[1];
*empty='\0';

delete[] _string_data;
_string_data=empty;
_string_length=0;

return *this;
}

// Outras operações constantes e não-constantes (não mostradas, porque não é o foco aqui).
my_string &insert(size_t offset, const my_string &other){ /* ... */ } // Inserção em posição arbitrária.
my_string &replace(size_t offset, size_t length, const my_string &other){ /* ... */ } // Substituição de conteúdo.
bool operator==(const my_string &other) const { /* ... */ } // Comparação de valor.
bool operator!=(const my_string &other) const { /* ... */ }
// ... etc.
};

int main(){
my_string nome{"Paulo"}; // Linha N
/* ... */ // Linhas N+1 a M-1
} // Linha M


Na definição da classe você já pode ver algumas coisas interessantes, mas quero aqui colocar o foco no corpo da função main(). Assim sendo, tem-se o seguinte:

  • O compilador, durante a compilação, identifica na linha N uma constante literal string com seis bytes de comprimento, e reserva um espaço para essa constante entre os dados estáticos e constantes do programa.

  • Também na linha N, o compilador identifica uma variável automática do tipo my_string dentro do bloco que constitui o corpo da função main(), e emite código para, na hora em que o programa for executado, reservar espaço para essa variável e para chamar o construtor, com um argumento do tipo const char *, cujo valor é obtido pelo decaimento do array constante, o qual indica o endereço do primeiro elemento do array. A quantidade de espaço na memória automática é exatamente igual a sizeof(my_string), que é um tamanho suficiente para acomodar todos os membros de dados (i.e. não-funções) não-estáticos da classe, mais um espaço para um ponteiro para a tabela de funções virtuais (que, no nosso caso, inclui apenas o destrutor). Residirão na memória automática, portanto, o valor do ponteiro para os dados que compõem a string (não os dados em si, que podem ter tamanho variável, mas apenas um endereço que apontará para os dados), correspondente ao membro de dados _string_data, o valor do comprimento da string, indicado pelo campo _string_length, e mais um ponteiro, usado internamente pelo compilador, para a tabela de funções virtuais.

  • O construtor do novo objeto my_string, também no momento da execução, recebe explicitamente o ponteiro passado como argumento e assume que ele se refere a um array, invocando strlen() sobre ele para calcular o tamanho que será passado como argumento para o operador new [], a fim de alocar uma quantidade suficiente de bytes obtidos da memória livre para acomodar a cópia do conteúdo do array (e eventualmente alguns outros valores de controle usados internamente por new e delete, mas invisíveis para o programador C++).

  • Ao chegar à linha N+1, pode-se dizer que a string "Paulo" existe em dois lugares distintos da memória: a original, entre os dados estáticos e constantes do programa, e uma cópia, que foi feita em uma região obtida a partir da memória livre no momento em que o construtor do objeto foi executado. Além disso, a memória automática teve de alocar espaço para o objeto identificado por nome (o que corresponde a alocar espaço para cada um de seus membros (um ponteiro para caracteres e um inteiro que representa tamanhos de dados) e para um ponteiro para a tabela de funções virtuais). Lembre-se que nome não é um ponteiro.

  • Ao longo das linhas N+1 a M-1, o valor de nome continuará a representar o objeto do tipo my_string que foi construído. Esse objeto continua residindo no mesmo espaço dentro da área automática que lhe foi reservada, mas os valores dentro dessa área podem sofrer alterações em função das operações realizadas sobre a string. Se for feita uma concatenação à string, por exemplo, é certo que os valores dos campos _string_data e _string_length serão alterados, mas vão continuar gravados nos mesmos lugares em que estavam gravados antes. Por outro lado, os dados que compõem a string podem mudar de posição dentro da memória livre a cada operação não-constante que for realizada sobre o objeto nome.

  • Ao chegar à linha M, o compilador emite código para que todos os objetos que tenha sido alocados na memória automática sejam liberados. Se esses objetos dispuserem de destrutores (tanto próprios quanto de seus membros de dados), tais destrutores serão chamados. No nosso caso, só temos um objeto visível, que é nome, que possui um destrutor. Ao ser invocado, o destrutor devolve à memória livre o atual espaço alocado, indicado pelo valor de _string_data. Após a execução dos destrutores, a memória automática é liberada e a execução segue a diante.

  • Nesse momento, antes de encerrar-se o bloco da função (e depois de encerrar-se, mais ainda), será inválido qualquer ponteiro que porventura aponte para o antigo objeto que estava na memória automática ou para a antiga região obtida da memória livre (e agora já devolvida) para acomodar dados da string.

E oque muda se eu colocar um ponteiro com o valor NULL ?


NULL é usado em C, mas é contraindicado em C++. Em seu lugar, prefere-se nullptr.

Um ponteiro nulo geralmente indica que algum dado está indisponível. Às vezes, indica a uma função que ela pode trabalhar com dados default, o que ela pode providenciar uma nova alocação. O que não se pode fazer com um ponteiro nulo é tentar chegar a um conteúdo de dados.

———
(*) Pelo menos, em teoria. Às vezes, um compilador pode decidir retardar o momento da efetiva liberação dessa memória, a fim de produzir código executável de maior desempenho. De qualquer forma, o efeito para o programa é tal como se aquela área não mais fosse válida para ele, uma vez que os objetos já não estão mais disponíveis, e qualquer referência a eles que porventura tenha “vazado” para fora do bloco deve ser considerada inválida para uso.

3. Re: Ponteiros, Liberar memoria excluindo objetos e variáveis não utilizadas e mais algumas coisas

Fernando
phoemur

(usa Debian)

Enviado em 26/03/2018 - 21:11h

Uma coisa que você tem que ter em mente é que o new/new[] do C++ não apenas aloca um endereço de memória (assim como malloc). Ele também chama o construtor do tipo, criando um objeto naquele local.
O mesmo vale para o delete/delete[] que não apenas libera a memória (assim como free), mas antes disso chama o destrutor do tipo naquele local.

Dito isso você não pode chamar um delete em uma variável alocada com malloc, e nem chamar free em uma variável alocada com new. Pois eles fazem coisas diferentes.
Você só chama delete no que foi usado new, para simplificar...

Note que existem new e new[], o primeiro para tipos simples e o segundo para arrays,
Assim como também existem delete/delete[].

Além deles existe o placement new / delete que já é um tópico mais avançado e provavelmente você não irá utilizar tão cedo.
Isso porque não mencionamos ponteiros inteligentes, allocators, etc... que são outras formas de gerenciar a memória em C++.


Agora se a variável está no Stack, ou seja, não foi usado nem new nem malloc ou coisa parecida, então o gerenciamento de memória dela é automático e você não precisa se preocupar com isso.

Geralmente quem tem dificuldade com esses conceitos são pessoas que vem da linguagem Java, pois em Java se usa new de forma mais "libertina", pois lá ele tem um significado/uso um pouco diferente...



4. Reutilizando o mesmo endereço alocado pelo operador new


oxidante

(usa Debian)

Enviado em 27/03/2018 - 22:52h

... e depois deletar a Variável/objeto de modo a liberar espaço na memoria, ou seja, tornar possível que uma outra nova Variável/Objeto use o mesmo espaço de memoria do antigo que foi excluído.

Você pode reutilizar o mesmo endereço previamente alocado pelo operador new e escrever um novo objeto para este espaço. Nesse caso, ao invés de chamar o operador delete para destruir o objeto antigo, vc irá chamar manualmente seu método destrutor (ex: pObj->~Objeto()), para que ele libere a memória alocada internamente. Também será preciso chamar manualmente os métodos destrutores de todos os seus membros que sejam objetos, para impedir vazamento de memória. Agora é só copiar o conteúdo do novo objeto para o espaço referenciado pela variável alocada anteriormente, usando memcpy() ou um simples *pObj = novoObj. A partir daí vc poderá manipular o novo objeto no mesmo endereço. Veja abaixo um exemplo:

// aloca dinamicamente espaco para conter um objeto do tipo Pagina
Pagina *pag = new Pagina();
pag->setTitulo("Primeira Pagina");
pag->getParagrafo()->setTexto("Primeiro paragrafo.");

// cria o novo objeto que será copiado para pag
Pagina novapag;
novapag.setTitulo("Segunda Pagina");
novapag.getParagrafo()->setTexto("Segundo paragrafo.");

// antes de realizar a cópia do novo obj, chamamos manualmente os destrutores do obj atual que se encontra em pag
pag->getParagrafo()->~Paragrafo(); // getParagrafo() retorna um objeto do tipo Paragrafo, que contém o método destrutor ~Paragrafo()
pag->~Pagina();

// copia o conteúdo do novo objeto, substituindo o conteúdo anterior
*pag = novapag;

// agora vc já pode manipular o novo objeto estando no mesmo endereço
pag->setTitulo("Terceira Pagina");
pag->getParagrafo()->setTexto("Terceiro paragrafo.");



5. Re: Ponteiros, Liberar memoria excluindo objetos e variáveis não utilizadas e mais algumas coisas

Paulo
paulo1205

(usa Ubuntu)

Enviado em 28/03/2018 - 01:01h

oxidante escreveu:

Você pode reutilizar o mesmo endereço previamente alocado pelo operador new e escrever um novo objeto para este espaço. Nesse caso, ao invés de chamar o operador delete para destruir o objeto antigo, vc irá chamar manualmente seu método destrutor (ex: pObj->~Objeto()), para que ele libere a memória alocada internamente. Também será preciso chamar manualmente os métodos destrutores de todos os seus membros que sejam objetos, para impedir vazamento de memória. Agora é só copiar o conteúdo do novo objeto para o espaço referenciado pela variável alocada anteriormente, usando memcpy() ou um simples *pObj = novoObj. A partir daí vc poderá manipular o novo objeto no mesmo endereço.


Há vários problemas com essa sugestão.

Em primeiro lugar, geralmente não se deve chamar explicitamente um destrutor. A única exceção geralmente aceitável é quando você usa uma versão do operador new que especifica diretamente o endereço de memória em que o objeto deve ser colocado (placement new). Nesse caso, a única coisa que o new faz é chamar o devido construtor, usando como base a região de memória informada diretamente. Como a alocação não é feita com new, também a desalocação não deve ser feita com delete, e a desconstrução tem de ser explícita.

O que você diz quanto a sobrescrever o objeto original prescinde — ou deveria prescindir — de desconstrução explícita. Uma classe que eventualmente mantenha recursos poderia usar uma versão customizada do operador de atribuição e uma sobrecarga da função swap(), numa implementação do copy-and-swap idiom (cf. https://stackoverflow.com/questions/3279543/what-is-the-copy-and-swap-idiom/3279550#3279550) e fazer a coisa de modo muito mais limpo, sem quebrar boas práticas de POO.

Sua sugestão vai no sentido oposto: desfaz o encapsulamento e arrisca corromper memória ao usar a versão padrão do operador de atribuição (ou você não lembrou de avisar que se deveria usar uma versão customizada), que, ao copiar possíveis membros de dados ponteiros literalmente, induz ao potencial erro de que cada um dos objetos, ao ser destruído, tente desalocar o mesmo ponteiro mais de uma vez (uma vez em cada objeto).

Se você costuma usar esse tipo de coisa no seu dia-a-dia (em vez de ser um mero brinquedo, ou para estudar como as coisas funcionam por dentro), sugiro que pare imediatamente e se volte para técnicas melhores, tais como a referida acima ou as Regras de 3, de 5 ou de 0 (cf. https://en.wikipedia.org/wiki/Rule_of_three_(C%2B%2B_programming), https://web.archive.org/web/20161023192637/https://rmf.io/cxx11/rule-of-zero).


6. Esclareceu minhas duvidas

Silas Henrique
silash35

(usa Arch Linux)

Enviado em 28/03/2018 - 11:35h

Muito obrigado, vou marcar como melhor resposta, vou reler essas repostas mais umas 10 vezes pra ver se não perdi nada, Muito obrigado mesmo.


7. Re: Ponteiros, Liberar memoria excluindo objetos e variáveis não utilizadas e mais algumas coisas [RESOL


oxidante

(usa Debian)

Enviado em 28/03/2018 - 21:04h

paulo1205

Você tem razão, minha solução é uma gambiarra que deve ser evitada em C++. E eu fiz de maneira proposital apenas para mostrar ao criador do tópico que é possível usar o mesmo endereço de memória para armazenar objetos diferentes. Como ele mencionou os termos variável/objeto, achei que não estivesse preocupado com a linguagem a ser usada (C ou C++), e que talvez fosse interessante mostrar como armazenar esses dados diretamente na memória. Fico agradecido pelo seu alerta e pelas informações adicionais, tenho certeza que foram de grande contribuição para o tópico.






Patrocínio

Site hospedado pelo provedor RedeHost.
Linux banner

Destaques

Artigos

Dicas

Tópicos

Top 10 do mês

Scripts