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.