Noções básicas sobre ponteiros para iniciantes

1. Introdução


Hoje, devido ao aprimoramento e baixo custo da tecnologia, a quantidade de memória e o poder de processamento estão aumentando constantemente.

De acordo com a Lei de Moore:
O número de transistores colocados em um chip de circuito integrado dobra a cada 24 meses.
Note-se que dois parâmetros foram alterados:

  • Número de transistores
  • Dimensões do módulo

Os mesmos princípios são projetados na quantidade de RAM (DRAM).

Agora, a questão da memória não é aguda, pois a quantidade de memória nos últimos 10 anos aumentou 16 vezes por dado.

A maioria das linguagens de programação de alto nível (PL) já está "pronta para uso", ocultando o trabalho com a memória do programador. E, como essa pergunta estava adormecida, aparece uma nova casta de programadores que não entendem ou não querem entender como funciona o trabalho com memória.

Neste tópico, consideraremos os principais pontos de trabalho com memória usando o exemplo da linguagem C ++, porque é uma das poucas linguagens imperativas que suporta o trabalho direto com memória e suporta OOP.

Para que serve a TI?


Vale ressaltar aqui, este artigo foi desenvolvido para pessoas que estão iniciando seu caminho em C ++ ou que desejam ter uma idéia sobre memória dinâmica.

Em tempo de execução, qualquer programa reserva um pedaço de memória para si próprio na DRAM. Todo o outro espaço livre de DRAM é chamado de "Heap" (inglês "Heap"). A alocação de memória durante a execução para as necessidades do programa ocorre precisamente a partir da pilha e é chamada alocação de memória dinâmica.

O problema é que, se você não limpar a memória alocada quando ela não for mais necessária, poderá ocorrer um vazamento de memória, no qual o sistema (programa) simplesmente trava. Parecido com um carro que parou no meio da estrada porque alguém se esqueceu de reabastecer a tempo.

O que você já deve saber
Os PLs mais modernos são equipados com coletores de lixo e limpam sua memória por conta própria.
No entanto, o C ++ se estabeleceu como uma das APIs de desempenho mais rápido, em parte porque todo o trabalho com memória é feito manualmente.


novo e excluir


A alocação de memória pode ser estática e dinâmica. A alocação estática da memória é chamada de alocação única de memória durante a compilação do programa, e a quantidade de memória estática não muda no tempo de execução. Um exemplo clássico é a declaração de uma variável inteira ou matriz. Mas e se o programador não souber antecipadamente quantos elementos são necessários no contêiner?
O uso da memória dinâmica é aconselhável quando for necessário organizar a alocação de memória para as necessidades do programa, conforme necessário.
O novo operador é responsável por alocar memória dinâmica em C ++ e a exclusão é responsável por limpá-la.
O novo operador retorna o resultado de sua operação como um ponteiro para uma nova instância da classe.
A sintaxe é esta:

| ponteiro de tipo de dados (T1) | * nome do ponteiro | = novo | tipo T1 |;

Após o novo operador, você pode usar o construtor, por exemplo, para inicializar os campos da classe.
Vale ressaltar que o mesmo vazamento de memória ocorre exatamente quando o programador perde o controle sobre sua alocação.
É importante lembrar:
Se você se esqueceu de limpar a memória dinâmica dos elementos desnecessários "gastos", mais cedo ou mais tarde chegará um momento crítico em que simplesmente não haverá lugar para tirar a memória.

Um exemplo de alocação de memória e sua limpeza:
int main{ // ,       new int *ptr = new int(); //   cout<<*ptr<<endl; // ,     delete ptr; //  delete     ,         return 0; } 


Este artigo não discutirá os chamados ponteiros "inteligentes", pois o tópico é muito extenso, mas, resumindo: "Os ponteiros inteligentes automatizam parcialmente o processo de limpeza de memória para o programador".

Ponteiros


Os ponteiros são responsáveis ​​por trabalhar com memória dinâmica em C ++. Este é um tópico do qual o apetite estraga os iniciantes.

Você pode declarar um ponteiro usando o operador * . Por padrão, ele apontará para alguma região aleatória da memória. Para que possamos acessar a área de memória de que precisamos, precisamos passar um link (operador & ) para a variável desejada.

O ponteiro em si é simplesmente o endereço de uma célula de memória e, para acessar os dados armazenados nessa célula, deve ser desreferenciado.

Retiro importante


Se você tentar exibir o ponteiro sem referenciar, em vez do valor da área de memória para a qual aponta, o endereço dessa área de memória será exibido.
Para remover a referência de um ponteiro, basta colocar o operador * na frente do nome.


 int main() { // ,          int* pNum= new int(1) ; cout<<*pNum<<endl; //    ,        ,       (   int   ) pNum++; cout<<*pNum<<endl; // ,         return 0; } 



Olhando para esses exemplos, eu gostaria de perguntar: "Por que isso é necessário se você pode derivar imediatamente uma variável?"

Outro exemplo:

Temos uma classe de programadores que descreve os membros de uma equipe de programadores que não sabem sobre ponteiros.

  class Programmers{ public: Programmers(){} Programmers(int iWeight, int iAge){ this->weight = iWeight; this->age = iAge; } int weight; int age; }; int main() { //     Programmers int size = 9; Programmers *prog [size]; //  Programmers Programmers *ptr = nullptr; //     Programmers       //          for (int i =0;i<size;i++) { ptr=new Programmers(i+100,i); prog[i]=ptr; } return 0; } 

Dessa maneira, a memória pode ser manipulada como quisermos. E é por isso que, ao trabalhar com memória, você pode "dar um tiro no próprio pé". Deve-se observar que trabalhar com o ponteiro é muito mais rápido, pois o valor em si não é copiado, mas apenas um link para um endereço específico é atribuído a ele.

A propósito, uma palavra-chave tão popular fornece um ponteiro para o objeto de classe atual. Esses ponteiros estão por toda parte.

Um exemplo de ponteiro na vida cotidiana:

Imagine uma situação quando você pede um prato em um restaurante. Para fazer um pedido, basta apontar para o prato no menu e você estará preparado. Da mesma forma, outros visitantes do restaurante indicam o item desejado no menu. Assim, cada linha do menu é um ponteiro para a função de cozimento de um prato, e esse ponteiro foi criado no estágio de design deste próprio menu.

Exemplo de ponteiro de função
 //      void Chicken(){ cout<<"Wait 5 min...Chicken is cooking"<<endl; } void JustWater(){ cout<<"Take your water"<<endl; } int main() { //    void   void (*ptr)(); ptr = Chicken; ptr(); ptr=JustWater; ptr(); return 0; } 



De volta aos nossos programadores. Suponha que agora precisamos levar os campos de classe para a seção privada , conforme convém ao princípio de encapsulamento do OOP, então precisamos do getter para obter acesso de leitura a esses campos. Mas imagine que não temos 2 campos, mas 100, e para isso precisamos escrever nosso próprio acessador para cada um?

Spoiler
Bem, claro que não, eu nem entendo por que você abriu esse spoiler.

Para fazer isso, tornaremos um "acessador" do tipo nulo e passaremos argumentos a ele por referência. O significado de passar um argumento por referência é que o valor do argumento não é copiado, mas apenas o endereço do argumento real é transmitido. Assim, ao alterar o valor desse argumento, os dados na célula de memória do argumento atual também serão alterados.
Isso também afeta o desempenho geral, já que passar um argumento por referência é mais rápido que passar por valor. E isso sem mencionar as grandes coleções de elementos.

Por exemplo, o método getParams interno alterará os argumentos recebidos e os valores, inclusive no escopo, de onde foi chamado.
Um ponteiro nos ajudará a navegar na matriz. A partir da teoria das estruturas de dados, sabemos que uma matriz é uma região contínua da memória cujos elementos são organizados um após o outro.
Isso significa que, se você alterar o valor do ponteiro para o número de bytes que o elemento ocupa na matriz, poderá alcançar cada elemento até que o ponteiro ultrapasse os limites da matriz.
Crie outro ponteiro que aponte para o primeiro elemento da matriz dos programadores .

 class Programmers{ public: Programmers(){} Programmers(int iWeight, int iAge){ this->weight = iWeight; this->age = iAge; } //    ,   main     void getParams(int &w, int &a){ w=weight; a=age; } private: int weight; int age; }; int main() { int size = 9; Programmers *prog [size]; Programmers *ptr=nullptr; for (int i =0;i<size;i++) { ptr=new Programmers(i+100,i); prog[i]=ptr; } int w,a; int count = 9; //    //        Programmers **iter = prog; for (int i=0;i<count;i++) { ptr = *iter++; ptr->getParams(w,a); if(*(iter-1) != nullptr){ delete *(iter-1); ptr = nullptr; } cout<<w<<"\t"<<a<<endl; } return 0; } 



Neste exemplo, quero transmitir a você a essência do fato de que, quando você altera o valor do endereço do ponteiro, pode acessar outra área da memória.

Estruturas de dados, como listas, vetores, etc. com base em ponteiros e, portanto, denominadas estruturas de dados dinâmicas. E para iterar sobre eles, é mais correto usar iteradores. Um iterador é um ponteiro para um elemento da estrutura de dados e fornece acesso ao elemento do contêiner.

Em conclusão


Tendo entendido o tópico dos ponteiros, trabalhar com memória torna-se uma parte agradável da programação e, como um todo, um entendimento detalhado de como a máquina trabalha com a memória e como gerenciá-la. Em certo sentido, há uma filosofia por trás do próprio conceito de "Trabalhar com a memória". Na ponta dos dedos, você altera a carga nas placas de capacitores muito pequenos.

Source: https://habr.com/ru/post/pt456318/


All Articles