Ponteiros é um conceito que para muitos pode ser confuso no início, principalmente quando se trata de copiar valores de ponteiros e ainda fazer referência ao mesmo bloco de memória.
Descobri que a melhor analogia é considerar o ponteiro como um pedaço de papel com um endereço residencial e o bloco de memória que ele faz referência como a casa real. Todos os tipos de operações podem ser facilmente explicados.
Adicionei algum código Delphi abaixo e alguns comentários, quando apropriado. Eu escolhi o Delphi porque minha outra linguagem de programação principal, C #, não exibe coisas como vazamentos de memória da mesma maneira.
Se você deseja apenas aprender o conceito de alto nível de ponteiros, deve ignorar as partes rotuladas como "Layout da memória" na explicação abaixo. Eles têm como objetivo dar exemplos de como a memória pode parecer após as operações, mas são de natureza mais baixa. No entanto, para explicar com precisão como as excedentes de buffer realmente funcionam, era importante que eu adicionasse esses diagramas.
Isenção de responsabilidade: para todos os efeitos, esta explicação e os layouts de memória de exemplo são bastante simplificados. Há mais sobrecarga e muito mais detalhes que você precisa saber se precisar lidar com a memória em um nível baixo. No entanto, para a intenção de explicar a memória e os ponteiros, é preciso o suficiente.
Vamos supor que a classe THouse usada abaixo seja assim:
type
THouse = class
private
FName : array[0..9] of Char;
public
constructor Create(name: PChar);
end;
Quando você inicializa o objeto de casa, o nome dado ao construtor é copiado no campo privado FName. Há uma razão para ele ser definido como uma matriz de tamanho fixo.
Na memória, haverá alguma sobrecarga associada à alocação da casa, ilustrarei isso abaixo desta forma:
--- [ttttNNNNNNNNNN] ---
^ ^
| |
| + - a matriz FName
|
+ - sobrecarga
A área "tttt" é aérea, normalmente haverá mais disso para vários tipos de tempos de execução e idiomas, como 8 ou 12 bytes. É imperativo que quaisquer valores armazenados nesta área nunca sejam alterados por algo que não seja o alocador de memória ou as rotinas do sistema principal, ou você corre o risco de travar o programa.
Alocar memória
Peça a um empresário que construa sua casa e forneça o endereço da casa. Ao contrário do mundo real, a alocação de memória não pode ser informada de onde alocar, mas encontrará um local adequado com espaço suficiente e reportará o endereço à memória alocada.
Em outras palavras, o empreendedor escolherá o local.
THouse.Create('My house');
Layout da memória:
--- [ttttNNNNNNNNNN] ---
Minha casa
Mantenha uma variável com o endereço
Escreva o endereço da sua nova casa em um pedaço de papel. Este documento servirá como referência para sua casa. Sem esse pedaço de papel, você está perdido e não consegue encontrar a casa, a menos que já esteja nela.
var
h: THouse;
begin
h := THouse.Create('My house');
...
Layout da memória:
h
v
--- [ttttNNNNNNNNNN] ---
Minha casa
Copiar valor do ponteiro
Basta escrever o endereço em um novo pedaço de papel. Agora você tem dois pedaços de papel que o levarão à mesma casa, não duas casas separadas. Qualquer tentativa de seguir o endereço de um jornal e reorganizar os móveis daquela casa fará parecer que a outra casa foi modificada da mesma maneira, a menos que você possa detectar explicitamente que na verdade é apenas uma casa.
Nota Geralmente, esse é o conceito que eu tenho mais problemas para explicar às pessoas: dois ponteiros não significam dois objetos ou blocos de memória.
var
h1, h2: THouse;
begin
h1 := THouse.Create('My house');
h2 := h1; // copies the address, not the house
...
h1
v
--- [ttttNNNNNNNNNN] ---
Minha casa
^
h2
Liberando a memória
Demolir a casa. Mais tarde, você poderá reutilizar o papel para um novo endereço, se desejar, ou limpá-lo para esquecer o endereço da casa que não existe mais.
var
h: THouse;
begin
h := THouse.Create('My house');
...
h.Free;
h := nil;
Aqui, primeiro construo a casa e pego o endereço. Depois faço algo em casa (use-o, o código ... deixado como exercício para o leitor) e depois libero-o. Por fim, apago o endereço da minha variável.
Layout da memória:
h <- +
v + - antes de grátis
--- [ttttNNNNNNNNNN] --- |
Minha casa <- +
h (agora aponta para lugar nenhum) <- +
+ - grátis
---------------------- | (note que a memória ainda pode
xx34Minha casa <- + contém alguns dados)
Ponteiros pendurados
Você diz ao seu empresário para destruir a casa, mas esquece de apagar o endereço do seu pedaço de papel. Quando, mais tarde, você olha o pedaço de papel, esquece que a casa não está mais lá e vai visitá-lo, com resultados fracassados (veja também a parte sobre uma referência inválida abaixo).
var
h: THouse;
begin
h := THouse.Create('My house');
...
h.Free;
... // forgot to clear h here
h.OpenFrontDoor; // will most likely fail
O uso h
após a chamada para .Free
pode funcionar, mas isso é pura sorte. Provavelmente, falhará, no local do cliente, no meio de uma operação crítica.
h <- +
v + - antes de grátis
--- [ttttNNNNNNNNNN] --- |
Minha casa <- +
h <- +
v + - depois de grátis
---------------------- |
Minha casa <- +
Como você pode ver, h ainda aponta para os restos dos dados na memória, mas como eles podem não estar completos, usá-los como antes pode falhar.
Vazamento de memória
Você perde o pedaço de papel e não consegue encontrar a casa. Porém, a casa ainda está em algum lugar, e quando você mais tarde quiser construir uma nova casa, não poderá reutilizar esse local.
var
h: THouse;
begin
h := THouse.Create('My house');
h := THouse.Create('My house'); // uh-oh, what happened to our first house?
...
h.Free;
h := nil;
Aqui, substituímos o conteúdo da h
variável pelo endereço de uma casa nova, mas a antiga ainda está de pé ... em algum lugar. Após esse código, não há como chegar a essa casa e ela ficará em pé. Em outras palavras, a memória alocada permanecerá alocada até o fechamento do aplicativo, momento em que o sistema operacional a derrubará.
Layout de memória após a primeira alocação:
h
v
--- [ttttNNNNNNNNNN] ---
Minha casa
Layout de memória após a segunda alocação:
h
v
--- [ttttNNNNNNNNNN] --- [ttttNNNNNNNNNNN]
1234 Minha casa 5678 Minha casa
Uma maneira mais comum de obter esse método é esquecer de liberar algo, em vez de substituí-lo como acima. Em termos de Delphi, isso ocorrerá com o seguinte método:
procedure OpenTheFrontDoorOfANewHouse;
var
h: THouse;
begin
h := THouse.Create('My house');
h.OpenFrontDoor;
// uh-oh, no .Free here, where does the address go?
end;
Após a execução desse método, não há lugar em nossas variáveis que exista o endereço da casa, mas a casa ainda está lá fora.
Layout da memória:
h <- +
v + - antes de perder o ponteiro
--- [ttttNNNNNNNNNN] --- |
Minha casa <- +
h (agora aponta para lugar nenhum) <- +
+ - depois de perder o ponteiro
--- [ttttNNNNNNNNNN] --- |
Minha casa <- +
Como você pode ver, os dados antigos são deixados intactos na memória e não serão reutilizados pelo alocador de memória. O alocador controla quais áreas da memória foram usadas e não as reutilizará, a menos que você a libere.
Liberando a memória, mas mantendo uma referência (agora inválida)
Demolir a casa, apagar um dos pedaços de papel, mas você também tem outro pedaço de papel com o endereço antigo; quando você vai para o endereço, não encontra uma casa, mas pode encontrar algo que se assemelha às ruínas de Um.
Talvez você até encontre uma casa, mas não é a casa para a qual foi originalmente fornecido o endereço e, portanto, qualquer tentativa de usá-la como se pertencesse a você pode falhar terrivelmente.
Às vezes, você pode até achar que um endereço vizinho possui uma casa bastante grande, que ocupa três endereços (Main Street 1-3), e seu endereço fica no meio da casa. Qualquer tentativa de tratar essa parte da grande casa de três endereços como uma única casa pequena também pode falhar terrivelmente.
var
h1, h2: THouse;
begin
h1 := THouse.Create('My house');
h2 := h1; // copies the address, not the house
...
h1.Free;
h1 := nil;
h2.OpenFrontDoor; // uh-oh, what happened to our house?
Aqui a casa foi demolida, através da referência h1
e, embora tenha h1
sido limpa também, h2
ainda tem o endereço antigo e desatualizado. O acesso à casa que não está mais em pé pode ou não funcionar.
Esta é uma variação do ponteiro pendente acima. Veja seu layout de memória.
Saturação de buffer
Você move mais coisas para dentro da casa do que pode caber, derramando na casa ou no quintal dos vizinhos. Quando o dono daquela casa vizinha mais tarde voltar para casa, ele encontrará todo tipo de coisa que considerará sua.
Foi por esse motivo que escolhi uma matriz de tamanho fixo. Para preparar o cenário, suponha que a segunda casa que alocamos será, por algum motivo, colocada antes da primeira na memória. Em outras palavras, a segunda casa terá um endereço mais baixo que o primeiro. Além disso, eles são alocados um ao lado do outro.
Assim, este código:
var
h1, h2: THouse;
begin
h1 := THouse.Create('My house');
h2 := THouse.Create('My other house somewhere');
^-----------------------^
longer than 10 characters
0123456789 <-- 10 characters
Layout de memória após a primeira alocação:
h1
v
----------------------- [ttttNNNNNNNNNN]
Minha casa
Layout de memória após a segunda alocação:
h2 h1
vv
--- [ttttNNNNNNNNNN] ---- [ttttNNNNNNNNNNN]
Minha outra casa em algum lugar
^ --- + - ^
|
+ - substituído
A parte que mais frequentemente causa falha é quando você substitui partes importantes dos dados armazenados que realmente não devem ser alteradas aleatoriamente. Por exemplo, pode não ser um problema que partes do nome da h1-house tenham sido alteradas, em termos de travamento do programa, mas a substituição da sobrecarga do objeto provavelmente travará quando você tentar usar o objeto quebrado, como também substituindo links armazenados em outros objetos no objeto.
Listas vinculadas
Quando você segue um endereço em um pedaço de papel, chega a uma casa, e nessa casa há outro pedaço de papel com um novo endereço, para a próxima casa da cadeia, e assim por diante.
var
h1, h2: THouse;
begin
h1 := THouse.Create('Home');
h2 := THouse.Create('Cabin');
h1.NextHouse := h2;
Aqui, criamos um link da nossa casa para nossa cabine. Podemos seguir a cadeia até que uma casa não tenha NextHouse
referência, o que significa que é a última. Para visitar todas as nossas casas, poderíamos usar o seguinte código:
var
h1, h2: THouse;
h: THouse;
begin
h1 := THouse.Create('Home');
h2 := THouse.Create('Cabin');
h1.NextHouse := h2;
...
h := h1;
while h <> nil do
begin
h.LockAllDoors;
h.CloseAllWindows;
h := h.NextHouse;
end;
Layout da memória (adicionado NextHouse como um link no objeto, anotado com os quatro LLLLs no diagrama abaixo):
h1 h2
vv
--- [ttttNNNNNNNNNNLLLL] ---- [ttttNNNNNNNNNNNLLLL]
1234 Casa + 5678Cabine +
| ^ |
+ -------- + * (sem link)
Em termos básicos, o que é um endereço de memória?
Um endereço de memória é, em termos básicos, apenas um número. Se você pensa na memória como uma grande matriz de bytes, o primeiro byte tem o endereço 0, o próximo o endereço 1 e assim por diante. Isso é simplificado, mas bom o suficiente.
Portanto, este layout de memória:
h1 h2
vv
--- [ttttNNNNNNNNNN] --- [ttttNNNNNNNNNNN]
1234 Minha casa 5678 Minha casa
Pode ter esses dois endereços (o mais à esquerda - é o endereço 0):
O que significa que nossa lista vinculada acima pode se parecer com a seguinte:
h1 (= 4) h2 (= 28)
vv
--- [ttttNNNNNNNNNNLLLL] ---- [ttttNNNNNNNNNNNLLLL]
O que você quer tocar hoje?
| ^ |
+ -------- + * (sem link)
É comum armazenar um endereço que "aponte para lugar nenhum" como um endereço zero.
Em termos básicos, o que é um ponteiro?
Um ponteiro é apenas uma variável que contém um endereço de memória. Normalmente, você pode pedir à linguagem de programação que lhe forneça seu número, mas a maioria das linguagens e tempos de execução tenta ocultar o fato de que há um número abaixo, apenas porque o número em si não tem nenhum significado para você. É melhor pensar em um ponteiro como uma caixa preta, ou seja. você realmente não sabe nem se importa com como ele é realmente implementado, desde que funcione.